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
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,8 @@ public struct SentrySDKWrapper {
options.enableUncaughtNSExceptionReporting =
!SentrySDKOverrides.Crash.disableUncaughtNSExceptionReporting.boolValue
#endif

options.experimental.appHangs.enableV3 = true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

V3 ignores sample hang gates

Medium Severity

experimental.appHangs.enableV3 is hard-coded to true, so App Hang V3 stays on when legacy app hang tracking is turned off for benchmarking or via --io.sentry.app-hangs.disable-tracking, unlike enableAppHangTracking on the line above.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6ec1b95. Configure here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sample enables dual hang trackers

Medium Severity

Turning on App Hang V3 here leaves enableAppHangTracking enabled in the normal sample path, so both SentryHangTrackerIntegrationObjC and SentryHangTrackingV3Integration install and can each emit fatal hang events for the same stall.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6ec1b95. Configure here.

}

private func configurePerformanceTracing(_ options: Options) {
Expand Down
96 changes: 95 additions & 1 deletion Samples/iOS-Swift/App/Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Sentry
import SentrySampleShared
import UIKit

Expand Down Expand Up @@ -38,7 +39,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
SampleAppDebugMenu.shared.display()

metricKit.receiveReports()


captureExampleFlamegraph()

return true
}

Expand All @@ -52,6 +55,97 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// Workaround for 'Stored properties cannot be marked potentially unavailable with '@available''
private var metricKit = MetricKitManager()

// swiftlint:disable function_body_length
private func captureExampleFlamegraph() {
// Build a fake flamegraph tree flattened into frames with parent_index + sample_count.
//
// main (samples: 10)
// ├── UIApplicationMain (samples: 10)
// │ ├── -[AppDelegate application:didFinishLaunching:] (samples: 10)
// │ │ ├── loadConfiguration (samples: 4)
// │ │ │ ├── parseJSON (samples: 3)
// │ │ │ └── validateConfig (samples: 1)
// │ │ ├── setupDatabase (samples: 3)
// │ │ │ └── runMigrations (samples: 2)
// │ │ └── buildUI (samples: 3)
// │ │ ├── loadStoryboard (samples: 1)
// │ │ └── layoutSubviews (samples: 2)

struct FrameData {
let function: String
let package: String
let parentIndex: Int
let sampleCount: Int
}
let frameData: [FrameData] = [
// 0
FrameData(function: "main", package: "iOS-Swift", parentIndex: -1, sampleCount: 10),
// 1
FrameData(function: "UIApplicationMain", package: "UIKitCore", parentIndex: 0, sampleCount: 10),
// 2
FrameData(function: "-[AppDelegate application:didFinishLaunchingWithOptions:]", package: "iOS-Swift", parentIndex: 1, sampleCount: 10),
// 3
FrameData(function: "loadConfiguration", package: "iOS-Swift", parentIndex: 2, sampleCount: 4),
// 4
FrameData(function: "parseJSON", package: "iOS-Swift", parentIndex: 3, sampleCount: 3),
// 5
FrameData(function: "validateConfig", package: "iOS-Swift", parentIndex: 3, sampleCount: 1),
// 6
FrameData(function: "setupDatabase", package: "iOS-Swift", parentIndex: 2, sampleCount: 3),
// 7
FrameData(function: "runMigrations", package: "iOS-Swift", parentIndex: 6, sampleCount: 2),
// 8
FrameData(function: "buildUI", package: "iOS-Swift", parentIndex: 2, sampleCount: 3),
// 9
FrameData(function: "loadStoryboard", package: "UIKitCore", parentIndex: 8, sampleCount: 1),
// 10
FrameData(function: "layoutSubviews", package: "UIKitCore", parentIndex: 8, sampleCount: 2)
]

let frames: [Frame] = frameData.enumerated().map { index, data in
let frame = Frame()
frame.function = data.function
frame.package = data.package
frame.parentIndex = NSNumber(value: data.parentIndex)
frame.sampleCount = NSNumber(value: data.sampleCount)
frame.inApp = NSNumber(value: data.package == "iOS-Swift")
frame.instructionAddress = String(format: "0x%016x", 0x1000_0000 + index * 0x100)
frame.imageAddress = "0x0000000010000000"
return frame
}

let stacktrace = SentryStacktrace(frames: frames, registers: [:])
stacktrace.snapshot = NSNumber(value: true)

let thread = SentryThread(threadId: NSNumber(value: 0))
thread.name = "main"
thread.crashed = NSNumber(value: false)
thread.current = NSNumber(value: true)
thread.isMain = NSNumber(value: true)
thread.stacktrace = stacktrace

let mechanism = Mechanism(type: "mx_hang_diagnostic")
mechanism.handled = NSNumber(value: true)
mechanism.synthetic = NSNumber(value: true)

let exception = Exception(
value: "Example flamegraph hang: 6.6 sec",
type: "MXHangDiagnostic"
)
exception.mechanism = mechanism
exception.stacktrace = stacktrace
exception.threadId = NSNumber(value: 0)

let event = Event(level: .warning)
event.threads = [thread]
event.exceptions = [exception]
event.tags = ["example": "flamegraph"]

SentrySDK.capture(event: event)
print("[iOS-Swift] [debug] captured example flamegraph event")
}
// swiftlint:enable function_body_length

/**
* previously tried putting this in an AppDelegate.load override in ObjC, but it wouldn't run until
* after a launch profiler would have an opportunity to run, since SentryProfiler.load would always run
Expand Down
Loading
Loading