Skip to content

Conversation

@ioannisj
Copy link
Collaborator

💡 Motivation and Context

Stacked on top of #404
#skip-changelog

Adds automatic crash reporting to the PostHog iOS SDK using PLCrashReporter.
Crashes are captured and sent as $exception events with level "fatal" on the next app launch.

Crash Types Handled:

  • Uncaught NSExceptions
  • POSIX signals (e.g., SIGSEGV, SIGABRT, SIGBUS)
  • Mach exceptions (e.g., EXC_BAD_ACCESS, EXC_CRASH)

Implementation notes

  • Context Preservation: Event context (distinct_id, groups, session_id, properties) is serialized to PLCrashReporter.customData on every context change. PLCrashReporter persists this on disk. We then read this back when processing a crash so we can reconstruct event properties at the time of crash (and not at the time of processing)
  • Crash Report Processing: On next launch, pending crash reports are processed before enabling the crash reporter for new crashes. For now, this is a best effort, in case of failure to process we simply purge the crash report. As a future improvement we could implement a retry mechanism and a check for corrupted reports
  • Debug Images: Only binary images referenced in the stack trace are included
  • Debugger Detection: Crash handler is automatically disabled when a debugger is attached
  • Platform Support: iOS, macOS, tvOS only (watchOS has a stub)

Limitations

  • Swift crash messages: Swift crashes (like fatalError,force unwraps) appear as SIGTRAP without the actual error message (PLCrashReporter doesn't expose __crash_info section). Sentry and Bugsnag seem to have figured this out already. Left this out for now since it will require modifying PLCrashReporter
  • NSException chaining: Underlying errors via NSUnderlyingErrorKey are not captured (PLCrashReporter only exposes name/reason)

💚 How did you test it?

  • Manual testing. Will need symbolication before we know if the stack traces are healthy
  • Added some unit tests on utils and processing

📝 Checklist

  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

@ioannisj ioannisj requested a review from a team as a code owner December 22, 2025 09:07
@marandaneto
Copy link
Member

Swift crash messages: Swift crashes (like fatalError,force unwraps) appear as SIGTRAP without the actual error message (PLCrashReporter doesn't expose __crash_info section). Sentry and Bugsnag seem to have figured this out already. Left this out for now since it will require modifying PLCrashReporter

can we create an issue for this and add to the limitation section in the docs once we have one?

@marandaneto
Copy link
Member

NSException chaining: Underlying errors via NSUnderlyingErrorKey are not captured (PLCrashReporter only exposes name/reason)

do we also need to patch PLCrashReporter for this? have other folks figured this out? otherwise same as #417 (comment)

s.frameworks = 'Foundation'

# PLCrashReporter dependency (not available on watchOS)
# Using ~> 1.8 for minimum compatibility with host apps
Copy link
Member

Choose a reason for hiding this comment

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

any reason to choose this version specifically?


// Add crash metadata
if let uuidRef = report.uuidRef {
properties["$crash_report_id"] = CFUUIDCreateString(nil, uuidRef) as String
Copy link
Member

Choose a reason for hiding this comment

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

what should we do with this info? should we add this to taxonomy?

"mach": [
"exception": machException.type,
"code": machException.codes.first,
"subcode": machException.codes.count > 1 ? machException.codes[1] : nil,
Copy link
Member

Choose a reason for hiding this comment

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

lets skip nil values

// MARK: - Helpers

/// Format string for zero-padded 64-bit hex addresses (e.g., "0x00007fff12345678")
static let hexAddressPaddedFormat = "0x%016llx"
Copy link
Member

Choose a reason for hiding this comment

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

make it private

]

private static func machExceptionName(_ type: UInt64) -> String {
machExceptionNames[type] ?? "EXC_UNKNOWN(\(type))"
Copy link
Member

Choose a reason for hiding this comment

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

probably better with EXC_UNKNOWN_X instead of EXC_UNKNOWN(X), more readable


import Foundation

#if os(iOS) || os(macOS) || os(tvOS)
Copy link
Member

Choose a reason for hiding this comment

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

is the manual capture also available for the 3 OSs?

func install(_ postHog: PostHogSDK) throws {
try PostHogCrashReporterIntegration.integrationInstalledLock.withLock {
if PostHogCrashReporterIntegration.integrationInstalled {
throw InternalPostHogError(description: "Crash report integration already installed to another PostHogSDK instance.")
Copy link
Member

Choose a reason for hiding this comment

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

we can just log and bail out isntead of throwing, this would be more expensive at runtime

/// as the debugger intercepts signals before the crash handler can process them.
///
/// Default: false
@objc public var enableCrashHandler: Bool = false
Copy link
Member

Choose a reason for hiding this comment

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

i think we call this just autocapture on flutter? i think we should unify naming if that makes sense

/// - Parameters:
/// - skipBuildProperties: When true, skips buildProperties call and uses properties as-is.
/// Used by crash reporting to capture events with pre-built crash-time context.
func captureInternal(
Copy link
Member

Choose a reason for hiding this comment

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

this seems to be a public API now, how can we make this internal only for us? we should not expose this to customers

Base automatically changed from feat/error-tracking-manual-capture to feat/error-tracking December 23, 2025 09:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants