From e778bd229cb7cc7344cd70afed36eefc81de2a4e Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Tue, 28 Feb 2023 12:03:19 +0100 Subject: [PATCH] fix: MetricKit stack traces (#2723) This PR fixes MetricKits CPU and DiskWrite exception stacktraces. --- CHANGELOG.md | 1 + Sources/Sentry/SentryMetricKitIntegration.m | 253 +++++++++++++----- .../metric-kit-callstack-not-per-thread.json | 61 ++++- .../SentryMXCallStackTreeTests.swift | 17 +- .../SentryMetricKitIntegrationTests.swift | 61 ++++- 5 files changed, 300 insertions(+), 93 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3f13faeb2d..f42689c38a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixes - Crash in AppHangs when no threads (#2725) +- MetricKit stack traces (#2723) ## 8.2.0 diff --git a/Sources/Sentry/SentryMetricKitIntegration.m b/Sources/Sentry/SentryMetricKitIntegration.m index df6372764b6..2179eaf7a3b 100644 --- a/Sources/Sentry/SentryMetricKitIntegration.m +++ b/Sources/Sentry/SentryMetricKitIntegration.m @@ -7,6 +7,7 @@ #import #import #import +#import #import #import #import @@ -25,6 +26,21 @@ NS_ASSUME_NONNULL_BEGIN +@interface SentryMXExceptionParams : NSObject + +@property (nonatomic, assign) BOOL handled; +@property (nonatomic, assign) SentryLevel level; +@property (nonatomic, copy) NSString *exceptionValue; +@property (nonatomic, copy) NSString *exceptionType; +@property (nonatomic, copy) NSString *exceptionMechanism; +@property (nonatomic, copy) NSDate *timeStampBegin; + +@end + +@implementation SentryMXExceptionParams + +@end + @interface SentryMetricKitIntegration () @@ -77,13 +93,15 @@ - (void)didReceiveCrashDiagnostic:(MXCrashDiagnostic *)diagnostic [NSString stringWithFormat:@"MachException Type:%@ Code:%@ Signal:%@", diagnostic.exceptionType, diagnostic.exceptionCode, diagnostic.signal]; - [self captureMXEvent:callStackTree - handled:NO - level:kSentryLevelError - exceptionValue:exceptionValue - exceptionType:@"MXCrashDiagnostic" - exceptionMechanism:@"MXCrashDiagnostic" - timeStampBegin:timeStampBegin]; + SentryMXExceptionParams *params = [[SentryMXExceptionParams alloc] init]; + params.handled = NO; + params.level = kSentryLevelError; + params.exceptionValue = exceptionValue; + params.exceptionType = @"MXCrashDiagnostic"; + params.exceptionMechanism = @"MXCrashDiagnostic"; + params.timeStampBegin = timeStampBegin; + + [self captureMXEvent:callStackTree params:params]; } - (void)didReceiveCpuExceptionDiagnostic:(MXCPUExceptionDiagnostic *)diagnostic @@ -91,6 +109,14 @@ - (void)didReceiveCpuExceptionDiagnostic:(MXCPUExceptionDiagnostic *)diagnostic timeStampBegin:(NSDate *)timeStampBegin timeStampEnd:(NSDate *)timeStampEnd { + // MXCPUExceptionDiagnostics call stacks point to hot spots in code and aren't organized per + // thread. See https://developer.apple.com/videos/play/wwdc2020/10078/?time=224 + if (callStackTree.callStackPerThread) { + SENTRY_LOG_WARN(@"MXCPUExceptionDiagnostics aren't expected to have call stacks per " + @"thread. Ignoring it."); + return; + } + NSString *totalCPUTime = [self.measurementFormatter stringFromMeasurement:diagnostic.totalCPUTime]; NSString *totalSampledTime = @@ -102,13 +128,15 @@ - (void)didReceiveCpuExceptionDiagnostic:(MXCPUExceptionDiagnostic *)diagnostic // Still need to figure out proper exception values and types. // This code is currently only there for testing with TestFlight. - [self captureMXEvent:callStackTree - handled:YES - level:kSentryLevelWarning - exceptionValue:exceptionValue - exceptionType:SentryMetricKitCpuExceptionType - exceptionMechanism:SentryMetricKitCpuExceptionMechanism - timeStampBegin:timeStampBegin]; + SentryMXExceptionParams *params = [[SentryMXExceptionParams alloc] init]; + params.handled = YES; + params.level = kSentryLevelWarning; + params.exceptionValue = exceptionValue; + params.exceptionType = SentryMetricKitCpuExceptionType; + params.exceptionMechanism = SentryMetricKitCpuExceptionMechanism; + params.timeStampBegin = timeStampBegin; + + [self captureMXEvent:callStackTree params:params]; } - (void)didReceiveDiskWriteExceptionDiagnostic:(MXDiskWriteExceptionDiagnostic *)diagnostic @@ -124,13 +152,16 @@ - (void)didReceiveDiskWriteExceptionDiagnostic:(MXDiskWriteExceptionDiagnostic * // Still need to figure out proper exception values and types. // This code is currently only there for testing with TestFlight. - [self captureMXEvent:callStackTree - handled:YES - level:kSentryLevelWarning - exceptionValue:exceptionValue - exceptionType:SentryMetricKitDiskWriteExceptionType - exceptionMechanism:SentryMetricKitDiskWriteExceptionMechanism - timeStampBegin:timeStampBegin]; + + SentryMXExceptionParams *params = [[SentryMXExceptionParams alloc] init]; + params.handled = YES; + params.level = kSentryLevelWarning; + params.exceptionValue = exceptionValue; + params.exceptionType = SentryMetricKitDiskWriteExceptionType; + params.exceptionMechanism = SentryMetricKitDiskWriteExceptionMechanism; + params.timeStampBegin = timeStampBegin; + + [self captureMXEvent:callStackTree params:params]; } - (void)didReceiveHangDiagnostic:(MXHangDiagnostic *)diagnostic @@ -144,41 +175,34 @@ - (void)didReceiveHangDiagnostic:(MXHangDiagnostic *)diagnostic NSString *exceptionValue = [NSString stringWithFormat:@"%@ hangDuration:%@", SentryMetricKitHangDiagnosticType, hangDuration]; - [self captureMXEvent:callStackTree - handled:YES - level:kSentryLevelWarning - exceptionValue:exceptionValue - exceptionType:SentryMetricKitHangDiagnosticType - exceptionMechanism:SentryMetricKitHangDiagnosticMechanism - timeStampBegin:timeStampBegin]; + SentryMXExceptionParams *params = [[SentryMXExceptionParams alloc] init]; + params.handled = YES; + params.level = kSentryLevelWarning; + params.exceptionValue = exceptionValue; + params.exceptionType = SentryMetricKitHangDiagnosticType; + params.exceptionMechanism = SentryMetricKitHangDiagnosticMechanism; + params.timeStampBegin = timeStampBegin; + + [self captureMXEvent:callStackTree params:params]; } - (void)captureMXEvent:(SentryMXCallStackTree *)callStackTree - handled:(BOOL)handled - level:(enum SentryLevel)level - exceptionValue:(NSString *)exceptionValue - exceptionType:(NSString *)exceptionType - exceptionMechanism:(NSString *)exceptionMechanism - timeStampBegin:(NSDate *)timeStampBegin + params:(SentryMXExceptionParams *)params { // When receiving MXCrashDiagnostic the callStackPerThread was always true. In that case, the // MXCallStacks of the MXCallStackTree were individual threads, all belonging to the process - // when the crash occurred. For MXCPUException, the callStackPerThread was always true. In that + // when the crash occurred. For MXCPUException, the callStackPerThread was always false. In that // case, the MXCallStacks stem from CPU-hungry multiple locations in the sample app during an // observation time of 90 seconds of one app run. It's a collection of stack traces that are - // CPU-hungry. They could be from multiple threads or the same thread. + // CPU-hungry. if (callStackTree.callStackPerThread) { - SentryEvent *event = [self createEvent:handled - level:level - exceptionValue:exceptionValue - exceptionType:exceptionType - exceptionMechanism:exceptionMechanism]; + SentryEvent *event = [self createEvent:params]; - event.timestamp = timeStampBegin; + event.timestamp = params.timeStampBegin; event.threads = [self convertToSentryThreads:callStackTree]; SentryThread *crashedThread = event.threads[0]; - crashedThread.crashed = @(!handled); + crashedThread.crashed = @(!params.handled); SentryException *exception = event.exceptions[0]; exception.stacktrace = crashedThread.stacktrace; @@ -191,46 +215,131 @@ - (void)captureMXEvent:(SentryMXCallStackTree *)callStackTree [SentrySDK captureEvent:event]; } else { for (SentryMXCallStack *callStack in callStackTree.callStacks) { + [self buildAndCaptureMXEventFor:callStack.callStackRootFrames params:params]; + } + } +} - for (SentryMXFrame *frame in callStack.callStackRootFrames) { - - SentryEvent *event = [self createEvent:handled - level:level - exceptionValue:exceptionValue - exceptionType:exceptionType - exceptionMechanism:exceptionMechanism]; - event.timestamp = timeStampBegin; - - SentryThread *thread = [[SentryThread alloc] initWithThreadId:@0]; - thread.crashed = @(!handled); - thread.stacktrace = [self - convertMXFramesToSentryStacktrace:frame.framesIncludingSelf.objectEnumerator]; +/** + * If callStackPerThread is false, MetricKit organizes the stacktraces in a tree structure. See + * https://developer.apple.com/videos/play/wwdc2020/10078/?time=224. The stacktrace consists of the + * last sibbling leaf frame plus its ancestors. + * + * The algorithm adds all frames to a list until it finds a leaf frame being the last sibling. Then + * it reports that frame with its siblings and ancestors as a stacktrace. + * + * In the following example, the algorithm starts with frame 0, continues until frame 6, and reports + * a stacktrace. Then it pops all sibling, goes back up to frame 3, and continues the search. + * + * | frame 0 | + * | frame 1 | + * | frame 2 | + * | frame 3 | + * | frame 4 | + * | frame 5 | + * | frame 6 | -> stack trace consists of [0, 1, 3, 4, 5, 6] + * | frame 7 | + * | frame 8 | -> stack trace consists of [0, 1, 2, 3, 7, 8] + * | frame 9 | -> stack trace consists of [0, 1, 9] + * | frame 10 | + * | frame 11 | + * | frame 12 | + * | frame 13 | -> stack trace consists of [10, 11, 12, 13] + */ +- (void)buildAndCaptureMXEventFor:(NSArray *)rootFrames + params:(SentryMXExceptionParams *)params +{ + for (SentryMXFrame *rootFrame in rootFrames) { + NSMutableArray *stackTraceFrames = [NSMutableArray array]; + NSMutableSet *processedFrameAddresses = [NSMutableSet set]; + NSMutableDictionary *addressesToParentFrames = + [NSMutableDictionary dictionary]; - SentryException *exception = event.exceptions[0]; - exception.stacktrace = thread.stacktrace; - exception.threadId = thread.threadId; + SentryMXFrame *currentFrame = rootFrame; + [stackTraceFrames addObject:currentFrame]; - event.threads = @[ thread ]; - event.debugMeta = [self extractDebugMetaFromMXFrames:frame.framesIncludingSelf]; + while (stackTraceFrames.count > 0) { + currentFrame = [stackTraceFrames lastObject]; + [processedFrameAddresses addObject:@(currentFrame.address)]; - [SentrySDK captureEvent:event]; + for (SentryMXFrame *subFrame in currentFrame.subFrames) { + addressesToParentFrames[@(subFrame.address)] = currentFrame; + } + SentryMXFrame *parentFrame = addressesToParentFrames[@(currentFrame.address)]; + + SentryMXFrame *firstUnprocessedSibling = + [self getFirstUnprocessedSubFrames:parentFrame.subFrames + processedFrameAddresses:processedFrameAddresses]; + + BOOL lastUnprocessedSibling = firstUnprocessedSibling == nil; + BOOL noChildren = currentFrame.subFrames.count == 0; + + if (noChildren && lastUnprocessedSibling) { + [self captureEventNotPerThread:stackTraceFrames params:params]; + + // Pop all siblings + for (int i = 0; i < parentFrame.subFrames.count; i++) { + [stackTraceFrames removeLastObject]; + } + } else { + SentryMXFrame *nonProcessedSubFrame = + [self getFirstUnprocessedSubFrames:currentFrame.subFrames + processedFrameAddresses:processedFrameAddresses]; + + // Keep adding sub frames + if (nonProcessedSubFrame != nil) { + [stackTraceFrames addObject:nonProcessedSubFrame]; + } // Keep adding siblings + else if (firstUnprocessedSibling != nil) { + [stackTraceFrames addObject:firstUnprocessedSibling]; + } // Keep popping + else { + [stackTraceFrames removeLastObject]; + } } } } } -- (SentryEvent *)createEvent:(BOOL)handled - level:(enum SentryLevel)level - exceptionValue:(NSString *)exceptionValue - exceptionType:(NSString *)exceptionType - exceptionMechanism:(NSString *)exceptionMechanism +- (nullable SentryMXFrame *)getFirstUnprocessedSubFrames:(NSArray *)subFrames + processedFrameAddresses: + (NSSet *)processedFrameAddresses +{ + return [subFrames filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL( + SentryMXFrame *frame, + NSDictionary *bindings) { + return ![processedFrameAddresses containsObject:@(frame.address)]; + }]].firstObject; +} + +- (void)captureEventNotPerThread:(NSArray *)frames + params:(SentryMXExceptionParams *)params +{ + SentryEvent *event = [self createEvent:params]; + event.timestamp = params.timeStampBegin; + + SentryThread *thread = [[SentryThread alloc] initWithThreadId:@0]; + thread.crashed = @(!params.handled); + thread.stacktrace = [self convertMXFramesToSentryStacktrace:frames.objectEnumerator]; + + SentryException *exception = event.exceptions[0]; + exception.stacktrace = thread.stacktrace; + exception.threadId = thread.threadId; + + event.threads = @[ thread ]; + event.debugMeta = [self extractDebugMetaFromMXFrames:frames]; + + [SentrySDK captureEvent:event]; +} + +- (SentryEvent *)createEvent:(SentryMXExceptionParams *)params { - SentryEvent *event = [[SentryEvent alloc] initWithLevel:level]; + SentryEvent *event = [[SentryEvent alloc] initWithLevel:params.level]; - SentryException *exception = [[SentryException alloc] initWithValue:exceptionValue - type:exceptionType]; - SentryMechanism *mechanism = [[SentryMechanism alloc] initWithType:exceptionMechanism]; - mechanism.handled = @(handled); + SentryException *exception = [[SentryException alloc] initWithValue:params.exceptionValue + type:params.exceptionType]; + SentryMechanism *mechanism = [[SentryMechanism alloc] initWithType:params.exceptionMechanism]; + mechanism.handled = @(params.handled); mechanism.synthetic = @(YES); exception.mechanism = mechanism; event.exceptions = @[ exception ]; diff --git a/Tests/Resources/metric-kit-callstack-not-per-thread.json b/Tests/Resources/metric-kit-callstack-not-per-thread.json index 40f682a17b0..7ca79039080 100644 --- a/Tests/Resources/metric-kit-callstack-not-per-thread.json +++ b/Tests/Resources/metric-kit-callstack-not-per-thread.json @@ -19,9 +19,61 @@ "offsetIntoBinaryTextSegment": 46370, "sampleCount": 1, "binaryName": "iOS-Swift", - "address": 4310988026 + "address": 4310988026, + "subFrames": [] + }, + { + "binaryUUID": "CA12CAFA-91BA-3E1C-BE9C-E34DB96FE7DF", + "offsetIntoBinaryTextSegment": 46360, + "sampleCount": 1, + "binaryName": "iOS-Swift", + "address": 4310988056, + "subFrames": [ + { + "binaryUUID": "CA12CAFA-91BA-3E1C-BE9C-E34DB96FE7DF", + "offsetIntoBinaryTextSegment": 46330, + "sampleCount": 1, + "binaryName": "iOS-Swift", + "address": 4310988066 + }, + { + "binaryUUID": "56C020FC-0369-3775-B947-148F388A65B3", + "offsetIntoBinaryTextSegment": 4081167, + "sampleCount": 1, + "binaryName": "libswiftCore.dylib", + "address": 6669985296 + }, + { + "binaryUUID": "56C020FC-0369-3775-B947-148F388A65B3", + "offsetIntoBinaryTextSegment": 4108728, + "sampleCount": 1, + "binaryName": "libswiftCore.dylib", + "address": 6670012856 + } + ] + }, + { + "binaryUUID": "CA12CAFA-91BA-3E1C-BE9C-E34DB96FE7DF", + "offsetIntoBinaryTextSegment": 46300, + "sampleCount": 1, + "binaryName": "iOS-Swift", + "address": 4310988046 + }, + { + "binaryUUID": "45AC734E-6649-3EE2-A096-3FD66441AB78", + "offsetIntoBinaryTextSegment": 2988, + "sampleCount": 1, + "binaryName": "libsystem_pthread.dylib", + "address": 8080116652 } ] + }, + { + "binaryUUID": "45AC734E-6649-3EE2-A096-3FD66441AB78", + "offsetIntoBinaryTextSegment": 2998, + "sampleCount": 1, + "binaryName": "libsystem_pthread.dylib", + "address": 8080116642 } ], "binaryName": "Sentry", @@ -45,6 +97,13 @@ "sampleCount": 1, "binaryName": "iOS-Swift", "address": 4310988026 + }, + { + "binaryUUID": "45AC734E-6649-3EE2-A096-3FD66441AB78", + "offsetIntoBinaryTextSegment": 46360, + "sampleCount": 1, + "binaryName": "libsystem_pthread.dylib", + "address": 4310988036 } ] } diff --git a/Tests/SentryTests/Integrations/MetricKit/SentryMXCallStackTreeTests.swift b/Tests/SentryTests/Integrations/MetricKit/SentryMXCallStackTreeTests.swift index 445f4e62503..1e402a6b646 100644 --- a/Tests/SentryTests/Integrations/MetricKit/SentryMXCallStackTreeTests.swift +++ b/Tests/SentryTests/Integrations/MetricKit/SentryMXCallStackTreeTests.swift @@ -15,21 +15,21 @@ final class SentryMXCallStackTreeTests: XCTestCase { let contents = try contentsOfResource("metric-kit-callstack-per-thread") let callStackTree = try SentryMXCallStackTree.from(data: contents) - try assertSimpleCallStackTree(callStackTree, callStackCount: 2) + try assertCallStackTree(callStackTree, callStackCount: 2) } func testDecodeCallStackTree_NotPerThread() throws { let contents = try contentsOfResource("metric-kit-callstack-not-per-thread") let callStackTree = try SentryMXCallStackTree.from(data: contents) - try assertSimpleCallStackTree(callStackTree, perThread: false, framesAmount: 6, threadAttributed: nil) + try assertCallStackTree(callStackTree, perThread: false, framesAmount: 14, threadAttributed: nil, subFrameCount: [2, 4, 0]) } func testDecodeCallStackTree_UnknownFieldsPayload() throws { let contents = try contentsOfResource("metric-kit-callstack-tree-unknown-fields") let callStackTree = try SentryMXCallStackTree.from(data: contents) - try assertSimpleCallStackTree(callStackTree) + try assertCallStackTree(callStackTree) } func testDecodeCallStackTree_RealPayload() throws { @@ -49,7 +49,10 @@ final class SentryMXCallStackTreeTests: XCTestCase { XCTAssertThrowsError(try SentryMXCallStackTree.from(data: contents)) } - private func assertSimpleCallStackTree(_ callStackTree: SentryMXCallStackTree, perThread: Bool = true, callStackCount: Int = 1, framesAmount: Int = 3, threadAttributed: Bool? = true) throws { + private func assertCallStackTree(_ callStackTree: SentryMXCallStackTree, perThread: Bool = true, callStackCount: Int = 1, framesAmount: Int = 3, threadAttributed: Bool? = true, subFrameCount: [Int] = [1, 1, 0]) throws { + + assert(subFrameCount.count == 3, "subFrameCount must contain 3 elements.") + XCTAssertNotNil(callStackTree) XCTAssertEqual(perThread, callStackTree.callStackPerThread) @@ -66,7 +69,7 @@ final class SentryMXCallStackTreeTests: XCTestCase { XCTAssertEqual(1, firstFrame.sampleCount) XCTAssertEqual("Sentry", firstFrame.binaryName) XCTAssertEqual(4_312_798_220, firstFrame.address) - XCTAssertEqual(1, firstFrame.subFrames?.count) + XCTAssertEqual(subFrameCount[0], firstFrame.subFrames?.count) let secondFrame = try XCTUnwrap(callStack.flattenedRootFrames[1]) XCTAssertEqual(UUID(uuidString: "CA12CAFA-91BA-3E1C-BE9C-E34DB96FE7DF"), secondFrame.binaryUUID) @@ -74,7 +77,7 @@ final class SentryMXCallStackTreeTests: XCTestCase { XCTAssertEqual(1, secondFrame.sampleCount) XCTAssertEqual("iOS-Swift", secondFrame.binaryName) XCTAssertEqual(4_310_988_076, secondFrame.address) - XCTAssertEqual(1, secondFrame.subFrames?.count) + XCTAssertEqual(subFrameCount[1], secondFrame.subFrames?.count) let thirdFrame = try XCTUnwrap(callStack.flattenedRootFrames[2]) XCTAssertEqual(UUID(uuidString: "CA12CAFA-91BA-3E1C-BE9C-E34DB96FE7DF"), thirdFrame.binaryUUID) @@ -82,7 +85,7 @@ final class SentryMXCallStackTreeTests: XCTestCase { XCTAssertEqual(1, thirdFrame.sampleCount) XCTAssertEqual("iOS-Swift", thirdFrame.binaryName) XCTAssertEqual(4_310_988_026, thirdFrame.address) - XCTAssertNil(thirdFrame.subFrames) + XCTAssertEqual(subFrameCount[2], thirdFrame.subFrames?.count ?? 0) XCTAssertEqual(try XCTUnwrap(firstFrame.subFrames?[0]), secondFrame) } diff --git a/Tests/SentryTests/Integrations/MetricKit/SentryMetricKitIntegrationTests.swift b/Tests/SentryTests/Integrations/MetricKit/SentryMetricKitIntegrationTests.swift index 1310f23bc2c..73d0cc8f543 100644 --- a/Tests/SentryTests/Integrations/MetricKit/SentryMetricKitIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/MetricKit/SentryMetricKitIntegrationTests.swift @@ -90,7 +90,7 @@ final class SentryMetricKitIntegrationTests: SentrySDKIntegrationTestsBase { let mxDelegate = sut as SentryMXManagerDelegate mxDelegate.didReceiveCpuExceptionDiagnostic(TestMXCPUExceptionDiagnostic(), callStackTree: callStackTreePerThread, timeStampBegin: timeStampBegin, timeStampEnd: timeStampEnd) - try assertPerThread(exceptionType: "MXCPUException", exceptionValue: "MXCPUException totalCPUTime:2.2 ms totalSampledTime:5.5 ms", exceptionMechanism: "mx_cpu_exception") + assertNothingCaptured() } } @@ -167,29 +167,64 @@ final class SentryMetricKitIntegrationTests: SentrySDKIntegrationTestsBase { } } + private func assertNothingCaptured() { + guard let client = SentrySDK.currentHub().getClient() as? TestClient else { + XCTFail("Hub Client is not a `TestClient`") + return + } + + XCTAssertEqual(0, client.captureEventWithScopeInvocations.count, "No events should be captured") + } + private func assertNotPerThread(exceptionType: String, exceptionValue: String, exceptionMechanism: String) throws { guard let client = SentrySDK.currentHub().getClient() as? TestClient else { XCTFail("Hub Client is not a `TestClient`") return } - XCTAssertEqual(2, client.captureEventWithScopeInvocations.count, "Client expected to capture 2 events.") - let firstEvent = client.captureEventWithScopeInvocations.invocations[0].event - let secondEvent = client.captureEventWithScopeInvocations.invocations[1].event + let invocations = client.captureEventWithScopeInvocations.invocations + XCTAssertEqual(4, client.captureEventWithScopeInvocations.count, "Client expected to capture 2 events.") + + let firstEvent = invocations[0].event + let secondEvent = invocations[1].event + let thirdEvent = invocations[2].event + let fourthEvent = invocations[3].event + + invocations.map { $0.event }.forEach { + XCTAssertEqual(timeStampBegin, $0.timestamp) + XCTAssertEqual(false, $0.threads?[0].crashed) + } - XCTAssertEqual(timeStampBegin, firstEvent.timestamp) - XCTAssertEqual(timeStampBegin, secondEvent.timestamp) + let allFrames = try XCTUnwrap(callStackTreeNotPerThread.callStacks.first?.flattenedRootFrames, "CallStackTree has no call stack.") - XCTAssertEqual(false, firstEvent.threads?[0].crashed) - XCTAssertEqual(false, secondEvent.threads?[0].crashed) + // Overview of stacktrace + // | frame 0 | + // | frame 1 | + // | frame 2 | + // | frame 3 | + // | frame 4 | + // | frame 5 | + // | frame 6 | -> stack trace consists of [0,1,3,4,5,6] + // | frame 7 | + // | frame 8 | -> stack trace consists of [0,1,2,3,7,8] + // | frame 9 | -> stack trace consists of [0,1,9] + // | frame 10 | + // | frame 11 | + // | frame 12 | + // | frame 13 | -> stack trace consists of [10,11,12,13] - let frames = try XCTUnwrap(callStackTreeNotPerThread.callStacks.first?.callStackRootFrames, "CallStackTree has no call stack.") + let firstEventFrames = [0, 1, 2, 3, 4, 5, 6].map { allFrames[$0] } + let secondEventFrames = [0, 1, 2, 3, 7, 8].map { allFrames[$0] } + let thirdEventFrames = [0, 1, 9].map { allFrames[$0] } + let fourthEventFrames = [10, 11, 12, 13].map { allFrames[$0] } - try assertFrames(frames: frames[0].framesIncludingSelf, event: firstEvent, exceptionType, exceptionValue, exceptionMechanism) - try assertFrames(frames: frames[1].framesIncludingSelf, event: secondEvent, exceptionType, exceptionValue, exceptionMechanism) + try assertFrames(frames: firstEventFrames, event: firstEvent, exceptionType, exceptionValue, exceptionMechanism, debugMetaCount: 3) + try assertFrames(frames: secondEventFrames, event: secondEvent, exceptionType, exceptionValue, exceptionMechanism, debugMetaCount: 3) + try assertFrames(frames: thirdEventFrames, event: thirdEvent, exceptionType, exceptionValue, exceptionMechanism, debugMetaCount: 3) + try assertFrames(frames: fourthEventFrames, event: fourthEvent, exceptionType, exceptionValue, exceptionMechanism, debugMetaCount: 3) } - private func assertFrames(frames: [SentryMXFrame], event: Event?, _ exceptionType: String, _ exceptionValue: String, _ exceptionMechanism: String, handled: Bool = true) throws { + private func assertFrames(frames: [SentryMXFrame], event: Event?, _ exceptionType: String, _ exceptionValue: String, _ exceptionMechanism: String, handled: Bool = true, debugMetaCount: Int = 2) throws { let sentryFrames = try XCTUnwrap(event?.threads?.first?.stacktrace?.frames, "Event has no frames.") XCTAssertEqual(frames.count, sentryFrames.count) @@ -214,7 +249,7 @@ final class SentryMetricKitIntegrationTests: SentrySDKIntegrationTestsBase { XCTAssertEqual(true, exception.mechanism?.synthetic) XCTAssertEqual(event?.threads?.first?.threadId, exception.threadId) - XCTAssertEqual(2, event?.debugMeta?.count) + XCTAssertEqual(debugMetaCount, event?.debugMeta?.count) guard let debugMeta = event?.debugMeta else { XCTFail("Event has no debugMeta.") return