Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 7 additions & 0 deletions Sources/Sentry/Public/SentryFrame.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ NS_SWIFT_NAME(Frame)
*/
@property (nonatomic, copy) NSString *_Nullable contextLine;

/**
* Index of the parent frame used for flamegraphs.
*/
@property (nonatomic, copy) NSNumber *_Nullable parentIndex;

@property (nonatomic, copy) NSNumber *_Nullable sampleCount;

/**
* Source code lines before the error location (up to 5 lines).
* Mostly used for Godot errors.
Expand Down
2 changes: 2 additions & 0 deletions Sources/Sentry/SentryFrame.m
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ - (instancetype)init
[serializedData setValue:self.platform forKey:@"platform"];
[serializedData setValue:self.contextLine forKey:@"context_line"];
[serializedData setValue:self.preContext forKey:@"pre_context"];
[serializedData setValue:self.parentIndex forKey:@"parent_index"];
[serializedData setValue:self.sampleCount forKey:@"sample_count"];
[serializedData setValue:self.postContext forKey:@"post_context"];
[serializedData setValue:sentry_sanitize(self.vars) forKey:@"vars"];
[SentryDictionary setBoolValue:self.inApp forKey:@"in_app" intoDictionary:serializedData];
Expand Down
69 changes: 52 additions & 17 deletions Sources/Swift/Core/MetricKit/SentryMXCallStackTree+Parsing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,7 @@ extension SentryMXCallStackTree {
frame.toDebugMeta()
}.unique { $0.debugID }
}

func prepare(event: Event, inAppLogic: SentryInAppLogic?, handled: Bool) {
let debugMeta = toDebugMeta()
let threads = sentryMXBacktrace(inAppLogic: inAppLogic, handled: handled)
// First look for the crashing thread, but for events that were not a crash (like a hang) take the first thread
// since those events only report one thread
let exceptionThread = threads.first { $0.crashed?.boolValue == true } ?? threads.first
event.debugMeta = debugMeta
event.threads = threads

if let exceptionThread, let exception = event.exceptions?[0] {
exception.stacktrace = exceptionThread.stacktrace
exception.threadId = exceptionThread.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
Expand Down Expand Up @@ -54,6 +39,40 @@ extension SentryMXCallStackTree {
return thread
}
}

/// Flattens the call stack tree into a single thread with all frames.
/// Each frame includes metadata in its `vars` field to allow reconstructing the original tree:
/// - `parent_frame_index`: The index of the parent frame in the flat list (-1 for root frames)
/// - `sample_count`: The number of samples at this frame
///
/// This preserves all sample data from the flamegraph rather than just the most common stack.
func flattenedBacktrace(inAppLogic: SentryInAppLogic?, handled: Bool) -> [SentryThread] {
callStacks.enumerated().map { index, callStack in
let thread = SentryThread(threadId: NSNumber(value: index))
var frames: [Frame] = []

// Traverse the tree and flatten all frames with parent references
for rootFrame in callStack.callStackRootFrames {
flattenFrame(rootFrame, parentIndex: -1, frames: &frames, inAppLogic: inAppLogic)
}

thread.stacktrace = SentryStacktrace(frames: frames, registers: [:])
thread.crashed = NSNumber(value: (callStack.threadAttributed ?? false) && !handled)
return thread
}
}

private func flattenFrame(_ mxFrame: SentryMXFrame, parentIndex: Int, frames: inout [Frame], inAppLogic: SentryInAppLogic?) {
let currentIndex = frames.count
let frame = mxFrame.toSentryFrameWithTreeData(frameIndex: currentIndex, parentFrameIndex: parentIndex)
frame.inApp = NSNumber(value: inAppLogic?.is(inApp: frame.package) ?? false)
frames.append(frame)

// Recursively process child frames
for subFrame in mxFrame.subFrames ?? [] {
flattenFrame(subFrame, parentIndex: currentIndex, frames: &frames, inAppLogic: inAppLogic)
}
}
}

extension SentryMXCallStack {
Expand All @@ -75,7 +94,7 @@ extension SentryMXFrame {
}
return [result] + (subFrames?.flatMap { $0.toDebugMeta() } ?? [])
}

func toSamples() -> [MXSample] {
let selfFrame = MXSample.MXFrame(binaryUUID: binaryUUID, offsetIntoBinaryTextSegment: offsetIntoBinaryTextSegment, binaryName: binaryName, address: address)
let subframes = subFrames ?? []
Expand All @@ -88,6 +107,22 @@ extension SentryMXFrame {
}
return result
}

/// Converts this frame to a SentryFrame with tree metadata in the `vars` field.
/// The metadata allows reconstructing the original tree structure from a flat list.
func toSentryFrameWithTreeData(frameIndex: Int, parentFrameIndex: Int) -> Frame {
let frame = Frame()
frame.package = binaryName
frame.instructionAddress = sentry_formatHexAddressUInt64Swift(address)
if offsetIntoBinaryTextSegment >= 0 && offsetIntoBinaryTextSegment < address {
frame.imageAddress = sentry_formatHexAddressUInt64Swift(address - UInt64(offsetIntoBinaryTextSegment))
}

frame.parentIndex = parentFrameIndex as NSNumber
frame.sampleCount = sampleCount as NSNumber?

return frame
}
}

private extension MXSample.MXFrame {
Expand Down
27 changes: 22 additions & 5 deletions Sources/Swift/Core/MetricKit/SentryMXManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,12 @@ final class SentryMXManager: NSObject, MXMetricManagerSubscriber {
payload.hangDiagnostics?.forEach { diagnostic in
let hangDuration = measurementFormatter.string(from: diagnostic.hangDuration)
let exceptionValue = "MXHangDiagnostic hangDuration:\(hangDuration)"
captureEvent(handled: true, exceptionValue: exceptionValue, exceptionType: "MXHangDiagnostic", exceptionMechanism: hangDiagnosticMechanism, timeStampBegin: payload.timeStampBegin, diagnostic: diagnostic)
captureEvent(handled: true, exceptionValue: exceptionValue, exceptionType: "MXHangDiagnostic", exceptionMechanism: hangDiagnosticMechanism, timeStampBegin: payload.timeStampBegin, diagnostic: diagnostic, useFullCallStackTree: true)
}
}
}

func captureEvent(handled: Bool, exceptionValue: String, exceptionType: String, exceptionMechanism: String, timeStampBegin: Date, diagnostic: MXDiagnostic & CallStackTreeProviding) {
func captureEvent(handled: Bool, exceptionValue: String, exceptionType: String, exceptionMechanism: String, timeStampBegin: Date, diagnostic: MXDiagnostic & CallStackTreeProviding, useFullCallStackTree: Bool = false) {
if let callStackTree = try? SentryMXCallStackTree.from(data: diagnostic.callStackTree.jsonRepresentation()) {
let event = Event(level: handled ? .warning : .error)
event.timestamp = timeStampBegin
Expand All @@ -95,12 +95,29 @@ final class SentryMXManager: NSObject, MXMetricManagerSubscriber {
mechanism.synthetic = true
exception.mechanism = mechanism
event.exceptions = [exception]
capture(event: event, handled: handled, callStackTree: callStackTree, diagnosticJSON: diagnostic.jsonRepresentation())
capture(event: event, handled: handled, callStackTree: callStackTree, diagnosticJSON: diagnostic.jsonRepresentation(), useFullCallStackTree: useFullCallStackTree)
}
}

func capture(event: Event, handled: Bool, callStackTree: SentryMXCallStackTree, diagnosticJSON: Data) {
callStackTree.prepare(event: event, inAppLogic: inAppLogic, handled: handled)
func capture(event: Event, handled: Bool, callStackTree: SentryMXCallStackTree, diagnosticJSON: Data, useFullCallStackTree: Bool = false) {
let debugMeta = callStackTree.toDebugMeta()
let threads: [SentryThread]
if useFullCallStackTree {
// For hang diagnostics, use the flattened tree to preserve all samples
threads = callStackTree.flattenedBacktrace(inAppLogic: inAppLogic, handled: handled)
} else {
threads = callStackTree.sentryMXBacktrace(inAppLogic: inAppLogic, handled: handled)
}
// First look for the crashing thread, but for events that were not a crash (like a hang) take the first thread
// since those events only report one thread
let exceptionThread = threads.first { $0.crashed?.boolValue == true } ?? threads.first
event.debugMeta = debugMeta
event.threads = threads

if let exceptionThread, let exception = event.exceptions?[0] {
exception.stacktrace = exceptionThread.stacktrace
exception.threadId = exceptionThread.threadId
}
Comment on lines +111 to +120
Copy link
Member

Choose a reason for hiding this comment

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

h: I know this is a draft, but this question is crucial to ask. Do you have any clue how grouping is going to work for these types of exceptions?

// The crash event can be way from the past. We don't want to impact the current session.
// Therefore we don't call captureFatalEvent.
capture(event: event, diagnosticJSON: diagnosticJSON)
Expand Down
216 changes: 216 additions & 0 deletions sdk_api.json
Original file line number Diff line number Diff line change
Expand Up @@ -9249,6 +9249,222 @@
}
]
},
{
"kind": "Var",
"name": "parentIndex",
"printedName": "parentIndex",
"children": [
{
"kind": "TypeNominal",
"name": "Optional",
"printedName": "Foundation.NSNumber?",
"children": [
{
"kind": "TypeNominal",
"name": "NSNumber",
"printedName": "Foundation.NSNumber",
"usr": "c:objc(cs)NSNumber"
}
],
"usr": "s:Sq"
}
],
"declKind": "Var",
"usr": "c:objc(cs)SentryFrame(py)parentIndex",
"moduleName": "Sentry",
"isOpen": true,
"objc_name": "parentIndex",
"declAttributes": [
"NSCopying",
"ObjC",
"Dynamic"
],
"accessors": [
{
"kind": "Accessor",
"name": "Get",
"printedName": "Get()",
"children": [
{
"kind": "TypeNominal",
"name": "Optional",
"printedName": "Foundation.NSNumber?",
"children": [
{
"kind": "TypeNominal",
"name": "NSNumber",
"printedName": "Foundation.NSNumber",
"usr": "c:objc(cs)NSNumber"
}
],
"usr": "s:Sq"
}
],
"declKind": "Accessor",
"usr": "c:objc(cs)SentryFrame(im)parentIndex",
"moduleName": "Sentry",
"isOpen": true,
"objc_name": "parentIndex",
"declAttributes": [
"DiscardableResult",
"ObjC",
"Dynamic"
],
"accessorKind": "get"
},
{
"kind": "Accessor",
"name": "Set",
"printedName": "Set()",
"children": [
{
"kind": "TypeNameAlias",
"name": "Void",
"printedName": "Swift.Void",
"children": [
{
"kind": "TypeNominal",
"name": "Void",
"printedName": "()"
}
]
},
{
"kind": "TypeNominal",
"name": "Optional",
"printedName": "Foundation.NSNumber?",
"children": [
{
"kind": "TypeNominal",
"name": "NSNumber",
"printedName": "Foundation.NSNumber",
"usr": "c:objc(cs)NSNumber"
}
],
"usr": "s:Sq"
}
],
"declKind": "Accessor",
"usr": "c:objc(cs)SentryFrame(im)setParentIndex:",
"moduleName": "Sentry",
"isOpen": true,
"objc_name": "setParentIndex:",
"declAttributes": [
"ObjC",
"Dynamic"
],
"accessorKind": "set"
}
]
},
{
"kind": "Var",
"name": "sampleCount",
"printedName": "sampleCount",
"children": [
{
"kind": "TypeNominal",
"name": "Optional",
"printedName": "Foundation.NSNumber?",
"children": [
{
"kind": "TypeNominal",
"name": "NSNumber",
"printedName": "Foundation.NSNumber",
"usr": "c:objc(cs)NSNumber"
}
],
"usr": "s:Sq"
}
],
"declKind": "Var",
"usr": "c:objc(cs)SentryFrame(py)sampleCount",
"moduleName": "Sentry",
"isOpen": true,
"objc_name": "sampleCount",
"declAttributes": [
"NSCopying",
"ObjC",
"Dynamic"
],
"accessors": [
{
"kind": "Accessor",
"name": "Get",
"printedName": "Get()",
"children": [
{
"kind": "TypeNominal",
"name": "Optional",
"printedName": "Foundation.NSNumber?",
"children": [
{
"kind": "TypeNominal",
"name": "NSNumber",
"printedName": "Foundation.NSNumber",
"usr": "c:objc(cs)NSNumber"
}
],
"usr": "s:Sq"
}
],
"declKind": "Accessor",
"usr": "c:objc(cs)SentryFrame(im)sampleCount",
"moduleName": "Sentry",
"isOpen": true,
"objc_name": "sampleCount",
"declAttributes": [
"DiscardableResult",
"ObjC",
"Dynamic"
],
"accessorKind": "get"
},
{
"kind": "Accessor",
"name": "Set",
"printedName": "Set()",
"children": [
{
"kind": "TypeNameAlias",
"name": "Void",
"printedName": "Swift.Void",
"children": [
{
"kind": "TypeNominal",
"name": "Void",
"printedName": "()"
}
]
},
{
"kind": "TypeNominal",
"name": "Optional",
"printedName": "Foundation.NSNumber?",
"children": [
{
"kind": "TypeNominal",
"name": "NSNumber",
"printedName": "Foundation.NSNumber",
"usr": "c:objc(cs)NSNumber"
}
],
"usr": "s:Sq"
}
],
"declKind": "Accessor",
"usr": "c:objc(cs)SentryFrame(im)setSampleCount:",
"moduleName": "Sentry",
"isOpen": true,
"objc_name": "setSampleCount:",
"declAttributes": [
"ObjC",
"Dynamic"
],
"accessorKind": "set"
}
]
},
{
"kind": "Var",
"name": "preContext",
Expand Down
Loading