From d978079844155bb1eb83f829b88d14ee645a8b41 Mon Sep 17 00:00:00 2001 From: Noah Martin Date: Tue, 25 Nov 2025 13:05:11 -0500 Subject: [PATCH] fix: Use correct parsing for stackframes --- Sentry.xcodeproj/project.pbxproj | 8 + Sources/Sentry/SentryFormatterSwift.m | 7 + Sources/Sentry/SentryMetricKitIntegration.m | 278 +----------------- Sources/Sentry/include/SentryFormatterSwift.h | 1 + Sources/Sentry/include/SentryPrivate.h | 2 + .../MetricKit/SentryMXCallStackTree.swift | 129 ++++++-- 6 files changed, 131 insertions(+), 294 deletions(-) create mode 100644 Sources/Sentry/SentryFormatterSwift.m create mode 100644 Sources/Sentry/include/SentryFormatterSwift.h diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 78a4e2aa17..f9197dee35 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -1102,6 +1102,8 @@ FA90FAA82E06614E008CAAE8 /* SentryExtraPackages.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA90FAA72E06614B008CAAE8 /* SentryExtraPackages.swift */; }; FA90FAFD2E070A3B008CAAE8 /* SentryURLRequestFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA90FAFC2E070A3B008CAAE8 /* SentryURLRequestFactory.swift */; }; FA914E6D2ECFD7D800C54BDD /* SentryFeedbackAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA914E6C2ECFD7D800C54BDD /* SentryFeedbackAPI.swift */; }; + FA914E9B2ED61AA800C54BDD /* SentryFormatterSwift.h in Headers */ = {isa = PBXBuildFile; fileRef = FA914E952ED61AA300C54BDD /* SentryFormatterSwift.h */; }; + FA914E9E2ED61BA800C54BDD /* SentryFormatterSwift.m in Sources */ = {isa = PBXBuildFile; fileRef = FA914E9C2ED61AB900C54BDD /* SentryFormatterSwift.m */; }; FA94E6912E6B92C100576666 /* SentryClientReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA94E68B2E6B92BE00576666 /* SentryClientReport.swift */; }; FA94E6B22E6D265800576666 /* SentryEnvelope.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA94E6B12E6D265500576666 /* SentryEnvelope.swift */; }; FA94E7242E6F339400576666 /* SentryEnvelopeItemType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA94E7232E6F32FA00576666 /* SentryEnvelopeItemType.swift */; }; @@ -2485,6 +2487,8 @@ FA90FAA72E06614B008CAAE8 /* SentryExtraPackages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryExtraPackages.swift; sourceTree = ""; }; FA90FAFC2E070A3B008CAAE8 /* SentryURLRequestFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryURLRequestFactory.swift; sourceTree = ""; }; FA914E6C2ECFD7D800C54BDD /* SentryFeedbackAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryFeedbackAPI.swift; sourceTree = ""; }; + FA914E952ED61AA300C54BDD /* SentryFormatterSwift.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryFormatterSwift.h; path = include/SentryFormatterSwift.h; sourceTree = ""; }; + FA914E9C2ED61AB900C54BDD /* SentryFormatterSwift.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryFormatterSwift.m; sourceTree = ""; }; FA94E68B2E6B92BE00576666 /* SentryClientReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryClientReport.swift; sourceTree = ""; }; FA94E6B12E6D265500576666 /* SentryEnvelope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryEnvelope.swift; sourceTree = ""; }; FA94E7232E6F32FA00576666 /* SentryEnvelopeItemType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryEnvelopeItemType.swift; sourceTree = ""; }; @@ -2950,6 +2954,8 @@ 639889D51EDF10BE00EA7442 /* Helper */ = { isa = PBXGroup; children = ( + FA914E952ED61AA300C54BDD /* SentryFormatterSwift.h */, + FA914E9C2ED61AB900C54BDD /* SentryFormatterSwift.m */, FA4FB8252ECB7D27008C9EC3 /* SentryLevel.h */, 63AA76951EB9C1C200D153DE /* SentryDefines.h */, 627E7588299F6FE40085504D /* SentryInternalDefines.h */, @@ -5243,6 +5249,7 @@ FAB359982E05D7E90083D5E3 /* SentryEventSwiftHelper.h in Headers */, 7B31C291277B04A000337126 /* SentryCrashPlatformSpecificDefines.h in Headers */, D452FC732DDB553100AFF56F /* SentryWatchdogTerminationBreadcrumbProcessor.h in Headers */, + FA914E9B2ED61AA800C54BDD /* SentryFormatterSwift.h in Headers */, D456B4382D706BFE007068CB /* SentrySpanDataKey.h in Headers */, 7B77BE3527EC8445003C9020 /* SentryDiscardReasonMapper.h in Headers */, 7B610D602512390E00B0B5D9 /* SentrySDK+Private.h in Headers */, @@ -5833,6 +5840,7 @@ D48891CC2E98F22A00212823 /* SentryInfoPlistWrapperProvider.swift in Sources */, F458D1172E186DF20028273E /* SentryScopePersistentStore+Fingerprint.swift in Sources */, D8CB7417294724CC00A5F964 /* SentryEnvelopeAttachmentHeader.m in Sources */, + FA914E9E2ED61BA800C54BDD /* SentryFormatterSwift.m in Sources */, D84793262788737D00BE8E99 /* SentryByteCountFormatter.m in Sources */, FAEFA12F2E4FAE1900C431D9 /* SentrySDKSettings.swift in Sources */, 63AA769E1EB9C57A00D153DE /* SentryError.mm in Sources */, diff --git a/Sources/Sentry/SentryFormatterSwift.m b/Sources/Sentry/SentryFormatterSwift.m new file mode 100644 index 0000000000..d61daa22c9 --- /dev/null +++ b/Sources/Sentry/SentryFormatterSwift.m @@ -0,0 +1,7 @@ +#import "SentryFormatter.h" + +NSString * +sentry_formatHexAddressUInt64Swift(uint64_t value) +{ + return sentry_formatHexAddressUInt64(value); +} diff --git a/Sources/Sentry/SentryMetricKitIntegration.m b/Sources/Sentry/SentryMetricKitIntegration.m index 3c5160ba1e..64d80d8ebe 100644 --- a/Sources/Sentry/SentryMetricKitIntegration.m +++ b/Sources/Sentry/SentryMetricKitIntegration.m @@ -204,187 +204,12 @@ - (void)didReceiveHangDiagnostic:(MXHangDiagnostic *)diagnostic - (void)captureMXEvent:(SentryMXCallStackTree *)callStackTree params:(SentryMXExceptionParams *)params diagnosticJSON:(NSData *)diagnosticJSON -{ - // 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 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. - if (callStackTree.callStackPerThread) { - SentryEvent *event = [self createEvent:params]; - - event.threads = [self convertToSentryThreads:callStackTree]; - - SentryThread *crashedThread = event.threads[0]; - crashedThread.crashed = @(!params.handled); - - SentryException *exception = event.exceptions[0]; - exception.stacktrace = crashedThread.stacktrace; - exception.threadId = crashedThread.threadId; - - event.debugMeta = [self extractDebugMetaFromMXCallStacks:callStackTree.callStacks]; - - // The crash event can be way from the past. We don't want to impact the current session. - // Therefore we don't call captureFatalEvent. - [self captureEvent:event withDiagnosticJSON:diagnosticJSON]; - } else { - for (SentryMXCallStack *callStack in callStackTree.callStacks) { - [self buildAndCaptureMXEventFor:callStack.callStackRootFrames - params:params - diagnosticJSON:diagnosticJSON]; - } - } -} - -/** - * If @c callStackPerThread is @c NO , 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 sibling 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 frames, goes back up to frame 3, and continues the search. - * - * It is worth noting that for the first stacktrace [0, 1, 3, 4, 5, 6] frame 2 is not included - * because the logic only includes direct siblings and direct ancestors. Frame 3 is an ancestors of - * [4,5,6], frame 1 of frame 3, but frame 2 is not a direct ancestors of [4,5,6]. It's the sibling - * of the direct ancestor frame 3. Although this might seem a bit illogical, that is what - * observations of MetricKit data unveiled. - * - * @code - * | 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] - * @endcode - * - * The above stacktrace turns into the following two trees. - * @code - * 0 - * | - * 1 - * / \ \ - * 3 2 9 - * | | - * 4 3 - * | | - * 5 7 - * | | - * 6 8 - * - * 10 - * | - * 11 - * | - * 12 - * | - * 13 - * @endcode - */ -- (void)buildAndCaptureMXEventFor:(NSArray *)rootFrames - params:(SentryMXExceptionParams *)params - diagnosticJSON:(NSData *)diagnosticJSON -{ - for (SentryMXFrame *rootFrame in rootFrames) { - NSMutableArray *stackTraceFrames = [NSMutableArray array]; - NSMutableSet *processedFrameAddresses = [NSMutableSet set]; - NSMutableDictionary *addressesToParentFrames = - [NSMutableDictionary dictionary]; - - SentryMXFrame *currentFrame = rootFrame; - [stackTraceFrames addObject:currentFrame]; - - while (stackTraceFrames.count > 0) { - currentFrame = [stackTraceFrames lastObject]; - [processedFrameAddresses addObject:@(currentFrame.address)]; - - 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 - diagnosticJSON:diagnosticJSON]; - - if (parentFrame == nil) { - // No parent frames - [stackTraceFrames removeLastObject]; - } else { - // Pop all sibling frames - 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 sibling frames - else if (firstUnprocessedSibling != nil) { - [stackTraceFrames addObject:firstUnprocessedSibling]; - } // Keep popping - else { - [stackTraceFrames removeLastObject]; - } - } - } - } -} - -- (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 - diagnosticJSON:(NSData *)diagnosticJSON { SentryEvent *event = [self createEvent:params]; + [callStackTree prepareWithEvent:event inAppLogic:self.inAppLogic handled:params.handled]; - 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]; - + // The crash event can be way from the past. We don't want to impact the current session. + // Therefore we don't call captureFatalEvent. [self captureEvent:event withDiagnosticJSON:diagnosticJSON]; } @@ -419,103 +244,6 @@ - (void)captureEvent:(SentryEvent *)event withDiagnosticJSON:(NSData *)diagnosti } } -- (NSArray *)convertToSentryThreads:(SentryMXCallStackTree *)callStackTree -{ - NSUInteger i = 0; - NSMutableArray *threads = [NSMutableArray array]; - for (SentryMXCallStack *callStack in callStackTree.callStacks) { - NSEnumerator *frameEnumerator - = callStack.flattenedRootFrames.objectEnumerator; - // The MXFrames are in reversed order when callStackPerThread is true. The Apple docs don't - // state that. This is an assumption based on observing MetricKit data. - if (callStackTree.callStackPerThread) { - frameEnumerator = [callStack.flattenedRootFrames reverseObjectEnumerator]; - } - - SentryStacktrace *stacktrace = [self convertMXFramesToSentryStacktrace:frameEnumerator]; - - SentryThread *thread = [[SentryThread alloc] initWithThreadId:@(i)]; - thread.stacktrace = stacktrace; - - [threads addObject:thread]; - - i++; - } - - return threads; -} - -- (SentryStacktrace *)convertMXFramesToSentryStacktrace:(NSEnumerator *)mxFrames -{ - NSMutableArray *frames = [NSMutableArray array]; - - for (SentryMXFrame *mxFrame in mxFrames) { - SentryFrame *frame = [[SentryFrame alloc] init]; - frame.package = mxFrame.binaryName; - frame.inApp = @([self.inAppLogic isInApp:mxFrame.binaryName]); - frame.instructionAddress = sentry_formatHexAddressUInt64(mxFrame.address); - uint64_t imageAddress = mxFrame.address - mxFrame.offsetIntoBinaryTextSegment; - frame.imageAddress = sentry_formatHexAddressUInt64(imageAddress); - - [frames addObject:frame]; - } - - SentryStacktrace *stacktrace = [[SentryStacktrace alloc] initWithFrames:frames registers:@{}]; - - return stacktrace; -} - -/** - * We must extract the debug images from the MetricKit stacktraces as the image addresses change - * when you restart the app. - */ -- (NSArray *)extractDebugMetaFromMXCallStacks: - (NSArray *)callStacks -{ - NSMutableDictionary *debugMetas = - [NSMutableDictionary dictionary]; - for (SentryMXCallStack *callStack in callStacks) { - - NSArray *callStackDebugMetas = - [self extractDebugMetaFromMXFrames:callStack.flattenedRootFrames]; - - for (SentryDebugMeta *debugMeta in callStackDebugMetas) { - if (debugMeta.debugID != nil) { - debugMetas[SENTRY_UNWRAP_NULLABLE_VALUE(id, debugMeta.debugID)] - = debugMeta; - } - } - } - - return [debugMetas allValues]; -} - -- (NSArray *)extractDebugMetaFromMXFrames:(NSArray *)mxFrames -{ - NSMutableDictionary *debugMetas = - [NSMutableDictionary dictionary]; - - for (SentryMXFrame *mxFrame in mxFrames) { - - NSString *binaryUUID = [mxFrame.binaryUUID UUIDString]; - if (debugMetas[binaryUUID]) { - continue; - } - - SentryDebugMeta *debugMeta = [[SentryDebugMeta alloc] init]; - debugMeta.type = SentryDebugImageType; - debugMeta.debugID = binaryUUID; - debugMeta.codeFile = mxFrame.binaryName; - - uint64_t imageAddress = mxFrame.address - mxFrame.offsetIntoBinaryTextSegment; - debugMeta.imageAddress = sentry_formatHexAddressUInt64(imageAddress); - - debugMetas[binaryUUID] = debugMeta; - } - - return [debugMetas allValues]; -} - @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryFormatterSwift.h b/Sources/Sentry/include/SentryFormatterSwift.h new file mode 100644 index 0000000000..2272138b2e --- /dev/null +++ b/Sources/Sentry/include/SentryFormatterSwift.h @@ -0,0 +1 @@ +extern NSString *sentry_formatHexAddressUInt64Swift(uint64_t value); diff --git a/Sources/Sentry/include/SentryPrivate.h b/Sources/Sentry/include/SentryPrivate.h index 4386c5d787..7f14e49ff2 100644 --- a/Sources/Sentry/include/SentryPrivate.h +++ b/Sources/Sentry/include/SentryPrivate.h @@ -21,7 +21,9 @@ #import "SentryDsn+Private.h" #import "SentryEnvelopeAttachmentHeader.h" #import "SentryEventSwiftHelper.h" +#import "SentryFormatterSwift.h" #import "SentryHub+Private.h" +#import "SentryInternalDefines.h" #import "SentryNSDataUtils.h" #import "SentrySDK+Private.h" #import "SentryTime.h" diff --git a/Sources/Swift/Core/MetricKit/SentryMXCallStackTree.swift b/Sources/Swift/Core/MetricKit/SentryMXCallStackTree.swift index 4a20d3ad14..2cb1b65e4e 100644 --- a/Sources/Swift/Core/MetricKit/SentryMXCallStackTree.swift +++ b/Sources/Swift/Core/MetricKit/SentryMXCallStackTree.swift @@ -1,3 +1,4 @@ +@_implementationOnly import _SentryPrivate import Foundation #if os(iOS) || os(macOS) @@ -7,39 +8,129 @@ import Foundation @objcMembers @_spi(Private) public class SentryMXCallStackTree: NSObject, Codable { - public let callStacks: [SentryMXCallStack] + let callStacks: [SentryMXCallStack] public let callStackPerThread: Bool static func from(data: Data) throws -> SentryMXCallStackTree { return try JSONDecoder().decode(SentryMXCallStackTree.self, from: data) } + + func toDebugMeta() -> [DebugMeta] { + callStacks.flatMap { frame in + frame.toDebugMeta() + }.unique { $0.debugID } + } + + public func prepare(event: Event, inAppLogic: SentryInAppLogic, handled: Bool) { + let debugMeta = toDebugMeta() + let threads = sentryMXBacktrace(inAppLogic: inAppLogic, handled: handled) + let crashedThread = threads.first { $0.crashed?.boolValue == true } + event.debugMeta = debugMeta + event.threads = threads + + if let crashedThread, let exception = event.exceptions?[0] { + exception.stacktrace = crashedThread.stacktrace + exception.threadId = crashedThread.threadId + } + } + + // A MetricKit CallStackTree is a flamegraph, but many Sentry APIs only support + // a thread backtrace. A flamegraph is just a collection of many thread backtraces + // generated by taking multiple samples. For example a hang from metric kit will + // be a group of samples of the main thread while it is hanging. To make MetricKit + // data compatible with Sentry data this function find the most commonly sampled stack. + // Some metric kit events contain multiple threads (like a crash report) and others + // contain all threads in one flamegraph. That happens when "callStackPerThread" + // is false. In these cases we can't really make a "thread" because + // it represents data that is sampled across many threads and aggregated, we do not + // know which samples came from which thread. Instead we create just one fake thread + // that just contains the most common callstack. + func sentryMXBacktrace(inAppLogic: SentryInAppLogic, handled: Bool) -> [SentryThread] { + callStacks.map { callStack in + let thread = SentryThread(threadId: 0) + let frames = callStack.toFrames() + frames.forEach { $0.inApp = NSNumber(value: inAppLogic.is(inApp: $0.package)) } + thread.stacktrace = SentryStacktrace(frames: frames, registers: [:]) + thread.crashed = NSNumber(value: (callStack.threadAttributed ?? false) && !handled) + return thread + } + } } -@objcMembers -@_spi(Private) public class SentryMXCallStack: NSObject, Codable { - public let threadAttributed: Bool? - public let callStackRootFrames: [SentryMXFrame] +struct SentryMXCallStack: Codable { + let threadAttributed: Bool? + let callStackRootFrames: [SentryMXFrame] + + func toFrames() -> [Frame] { + // The root node of a flamegraph is the first frame in a stacktrace (usually main) + callStackRootFrames.mostSampled()?.toFrames().reversed() ?? [] + } - public var flattenedRootFrames: [SentryMXFrame] { - return callStackRootFrames.flatMap { [$0] + $0.frames } + func toDebugMeta() -> [DebugMeta] { + callStackRootFrames.flatMap { frame in + frame.toDebugMeta() + } } } -@objcMembers -@_spi(Private) public class SentryMXFrame: NSObject, Codable { - public let binaryUUID: UUID - public let offsetIntoBinaryTextSegment: Int - public let binaryName: String? - public let address: UInt64 - public let subFrames: [SentryMXFrame]? - public let sampleCount: Int? +struct SentryMXFrame: Codable { + let binaryUUID: UUID + let offsetIntoBinaryTextSegment: Int + let binaryName: String? + let address: UInt64 + let subFrames: [SentryMXFrame]? + let sampleCount: Int? - var frames: [SentryMXFrame] { - return (subFrames?.flatMap { [$0] + $0.frames } ?? []) + func toSentryFrame() -> Frame { + let frame = Frame() + frame.package = binaryName + frame.instructionAddress = sentry_formatHexAddressUInt64Swift(address) + frame.imageAddress = sentry_formatHexAddressUInt64Swift(address - UInt64(offsetIntoBinaryTextSegment)) + return frame + } + + func toFrames() -> [Frame] { + return [toSentryFrame()] + (subFrames?.mostSampled()?.toFrames() ?? []) } - var framesIncludingSelf: [SentryMXFrame] { - return [self] + frames + func toDebugMeta() -> [DebugMeta] { + let result = DebugMeta() + result.type = SentryDebugImageType + result.debugID = binaryUUID.uuidString + result.codeFile = binaryName + result.imageAddress = sentry_formatHexAddressUInt64Swift(address - UInt64(offsetIntoBinaryTextSegment)) + return [result] + (subFrames?.flatMap { $0.toDebugMeta() } ?? []) + } +} + +extension Sequence { + func unique(by key: (Element) -> T) -> [Element] { + var seen = Set() + var result: [Element] = [] + for element in self { + let k = key(element) + if !seen.contains(k) { + seen.insert(k) + result.append(element) + } + } + + return result + } +} + +extension Sequence where Element == SentryMXFrame { + // A sentry frame is a list not a tree, find the most frequently sampled element at this level of the tree. + func mostSampled() -> SentryMXFrame? { + var mostSamples = -1 + var mostSampledFrame: SentryMXFrame? + for frame in self { + if frame.sampleCount ?? 0 > mostSamples { + mostSamples = frame.sampleCount ?? 0 + mostSampledFrame = frame + } + } + return mostSampledFrame } }