All Articles
CategoryDevOps
Reading Time
21 min read
Published
2026-01-25
Word Count
1,791words

Grab a coffee — this one is a deep dive!

iOS Crash Reporting and Analytics: The Production Quality Guide

Summary

Take production quality to the top with crash reporting infrastructure, symbolication, analytics event design, and monitoring dashboard setup.

  • Crash types: Mach Exception, Unix Signal, NSException, Swift Error, Watchdog, OOM, Hang.
  • dSYM files are required for symbolication; atos translates memory addresses into function names.
  • Crash-free rate should be measured per user, not per session; the target is above 99.5% crash-free users.
  • MetricKit gives direct access to Apple's watchdog kill, hang, and disk write diagnostics.
iOS Crash Reporting and Analytics: The Production Quality Guide

The real work begins after your app ships on the App Store. What errors are your users hitting? Which screens are they getting stuck on? Which features get used the most? The answer to these questions is hidden in a solid crash reporting and analytics infrastructure. In this guide, we'll cover every dimension of production quality, from crashes to analytics, from monitoring to alerting.


Table of Contents


1. Crash Reporting Fundamentals

On iOS, crashes fall into two main categories:

Crash Types

Type
Cause
Catchable?
Example
Mach Exception
Kernel-level error
Via signal handler
EXC_BAD_ACCESS, EXC_CRASH
Unix Signal
POSIX signal
Via signal handler
SIGABRT, SIGSEGV, SIGBUS
NSException
ObjC exception
Via try-catch
NSInvalidArgumentException
Swift Error
Swift runtime
Via do-catch
fatalError, precondition
Watchdog
Timeout (main thread)
No
App launch > 20s
OOM
Out of memory
Indirectly
Jetsam (system kill)
Hang
Main thread block
Via MetricKit
250ms+ block

Crash Lifecycle

swift
1// MARK: - Crash Reporter Setup
2final class CrashReporter {
3 static let shared = CrashReporter()
4 
5 private var previousExceptionHandler: NSUncaughtExceptionHandler?
6 private let queue = DispatchQueue(label: "com.app.crash-reporter", qos: .utility)
7 private let storage: CrashStorage
8 
9 init(storage: CrashStorage = FileBasedCrashStorage()) {
10 self.storage = storage
11 }
12 
13 func setup() {
14 // 1. Save the previous handler (chaining)
15 previousExceptionHandler = NSGetUncaughtExceptionHandler()
16 
17 // 2. Install our own handler
18 NSSetUncaughtExceptionHandler { exception in
19 CrashReporter.shared.handleException(exception)
20 }
21 
22 // 3. Signal handlers
23 setupSignalHandlers()
24 
25 // 4. Send crashes from the previous session
26 sendPendingReports()
27 }
28 
29 private func handleException(_ exception: NSException) {
30 let report = CrashReport(
31 name: exception.name.rawValue,
32 reason: exception.reason ?? "Unknown",
33 stackTrace: exception.callStackSymbols,
34 timestamp: Date(),
35 appState: captureAppState(),
36 deviceInfo: captureDeviceInfo()
37 )
38 
39 // Write synchronously (async isn't reliable at the moment of a crash)
40 storage.writeSynchronously(report)
41 
42 // Forward to the previous handler
43 previousExceptionHandler?(exception)
44 }
45 
46 private func setupSignalHandlers() {
47 let signals: [Int32] = [SIGABRT, SIGSEGV, SIGBUS, SIGFPE, SIGILL, SIGTRAP]
48 for sig in signals {
49 signal(sig) { signalNumber in
50 let report = CrashReport(
51 name: "Signal \(signalNumber)",
52 reason: CrashReporter.signalName(signalNumber),
53 stackTrace: Thread.callStackSymbols,
54 timestamp: Date(),
55 appState: CrashReporter.shared.captureAppState(),
56 deviceInfo: CrashReporter.shared.captureDeviceInfo()
57 )
58 CrashReporter.shared.storage.writeSynchronously(report)
59 }
60 }
61 }
62 
63 private static func signalName(_ signal: Int32) -> String {
64 switch signal {
65 case SIGABRT: return "SIGABRT"
66 case SIGSEGV: return "SIGSEGV"
67 case SIGBUS: return "SIGBUS"
68 case SIGFPE: return "SIGFPE"
69 case SIGILL: return "SIGILL"
70 case SIGTRAP: return "SIGTRAP"
71 default: return "UNKNOWN"
72 }
73 }
74 
75 private func captureAppState() -> AppState {
76 AppState(
77 memoryUsage: ProcessInfo.processInfo.physicalMemory,
78 diskFree: FileManager.default.freeDiskSpace,
79 batteryLevel: UIDevice.current.batteryLevel,
80 thermalState: ProcessInfo.processInfo.thermalState.rawValue,
81 activeScreen: NavigationTracker.shared.currentScreen,
82 sessionDuration: SessionManager.shared.duration
83 )
84 }
85 
86 private func captureDeviceInfo() -> DeviceInfo {
87 DeviceInfo(
88 model: UIDevice.current.model,
89 osVersion: UIDevice.current.systemVersion,
90 appVersion: Bundle.main.appVersion,
91 buildNumber: Bundle.main.buildNumber,
92 locale: Locale.current.identifier
93 )
94 }
95 
96 private func sendPendingReports() {
97 queue.async { [weak self] in
98 guard let reports = self?.storage.readPending() else { return }
99 for report in reports {
100 APIClient.shared.send(report) { success in
101 if success {
102 self?.storage.markSent(report.id)
103 }
104 }
105 }
106 }
107 }
108}

Easter Egg

You found a hidden gem!

There's a hidden detail in this section. Want to uncover it?


2. The Symbolication Process

A raw crash report only contains memory addresses. Symbolication translates these addresses into readable function names.

dSYM Management

swift
1// Build Settings check
2// DEBUG_INFORMATION_FORMAT = dwarf-with-dsym
3// DWARF_DSYM_FOLDER_PATH = build/
4 
5// Symbolication command (Terminal)
6// atos -arch arm64 -o MyApp.app.dSYM/Contents/Resources/DWARF/MyApp -l 0x100000000 0x100012345
7 
8// MARK: - Build Phase Script (dSYM upload)
9// #!/bin/bash
10// DSYM_PATH="BUILT_PRODUCTS_DIR/PRODUCT_NAME.app.dSYM"
11// if [ -d "PATH_TO_DSYM" ]; then
12// zip -r dsym.zip "PATH_TO_DSYM"
13// curl -X POST https://api.crashreporter.com/dsym
15// -F "version=APP_VERSION"
16// fi

3. Analytics Event Design

The right event design is the key to collecting meaningful data.

Event Naming Convention

swift
1// MARK: - Analytics Event System
2protocol AnalyticsEvent {
3 var name: String { get }
4 var properties: [String: Any] { get }
5 var timestamp: Date { get }
6}
7 
8enum AppEvent: AnalyticsEvent {
9 // Screen Events
10 case screenViewed(name: String)
11 case screenExited(name: String, duration: TimeInterval)
12 
13 // User Actions
14 case buttonTapped(name: String, screen: String)
15 case searchPerformed(query: String, resultCount: Int)
16 case itemSelected(id: String, category: String, position: Int)
17 
18 // Business Events
19 case purchaseStarted(productId: String, price: Decimal)
20 case purchaseCompleted(productId: String, price: Decimal, method: String)
21 case purchaseFailed(productId: String, error: String)
22 
23 // Engagement
24 case contentShared(type: String, platform: String)
25 case notificationReceived(type: String)
26 case notificationTapped(type: String, delay: TimeInterval)
27 
28 var name: String {
29 switch self {
30 case .screenViewed: return "screen_viewed"
31 case .screenExited: return "screen_exited"
32 case .buttonTapped: return "button_tapped"
33 case .searchPerformed: return "search_performed"
34 case .itemSelected: return "item_selected"
35 case .purchaseStarted: return "purchase_started"
36 case .purchaseCompleted: return "purchase_completed"
37 case .purchaseFailed: return "purchase_failed"
38 case .contentShared: return "content_shared"
39 case .notificationReceived: return "notification_received"
40 case .notificationTapped: return "notification_tapped"
41 }
42 }
43 
44 var properties: [String: Any] {
45 switch self {
46 case .screenViewed(let name):
47 return ["screen_name": name]
48 case .screenExited(let name, let duration):
49 return ["screen_name": name, "duration_seconds": duration]
50 case .buttonTapped(let name, let screen):
51 return ["button_name": name, "screen_name": screen]
52 case .purchaseCompleted(let id, let price, let method):
53 return ["product_id": id, "price": price, "payment_method": method]
54 default:
55 return [:]
56 }
57 }
58 
59 var timestamp: Date { Date() }
60}

4. Funnel and Cohort Analysis

Funnel analysis shows how far users progress through the steps of a given flow.

Funnel Tracker

swift
1// MARK: - Funnel Tracker
2final class FunnelTracker {
3 private let analytics: AnalyticsProviding
4 private var activeSteps: [String: FunnelStep] = [:]
5 
6 struct FunnelStep {
7 let funnelName: String
8 let stepName: String
9 let stepIndex: Int
10 let startTime: Date
11 }
12 
13 init(analytics: AnalyticsProviding) {
14 self.analytics = analytics
15 }
16 
17 func startFunnel(_ name: String, step: String) {
18 let funnelStep = FunnelStep(
19 funnelName: name,
20 stepName: step,
21 stepIndex: 0,
22 startTime: Date()
23 )
24 activeSteps[name] = funnelStep
25 analytics.track("funnel_step", properties: [
26 "funnel": name,
27 "step": step,
28 "step_index": 0
29 ])
30 }
31 
32 func advanceFunnel(_ name: String, to step: String) {
33 guard let current = activeSteps[name] else { return }
34 let next = FunnelStep(
35 funnelName: name,
36 stepName: step,
37 stepIndex: current.stepIndex + 1,
38 startTime: Date()
39 )
40 let stepDuration = Date().timeIntervalSince(current.startTime)
41 activeSteps[name] = next
42 analytics.track("funnel_step", properties: [
43 "funnel": name,
44 "step": step,
45 "step_index": next.stepIndex,
46 "previous_step_duration": stepDuration
47 ])
48 }
49 
50 func completeFunnel(_ name: String) {
51 guard let current = activeSteps[name] else { return }
52 let totalDuration = Date().timeIntervalSince(current.startTime)
53 analytics.track("funnel_completed", properties: [
54 "funnel": name,
55 "total_steps": current.stepIndex + 1,
56 "total_duration": totalDuration
57 ])
58 activeSteps.removeValue(forKey: name)
59 }
60 
61 func dropFunnel(_ name: String, reason: String) {
62 guard let current = activeSteps[name] else { return }
63 analytics.track("funnel_dropped", properties: [
64 "funnel": name,
65 "dropped_at_step": current.stepName,
66 "step_index": current.stepIndex,
67 "reason": reason
68 ])
69 activeSteps.removeValue(forKey: name)
70 }
71}

5. Monitoring Dashboard

Core Metrics

Metric
Target
Alert Threshold
Measurement Method
Crash-Free Rate
>99.5%
<99%
Crash count / DAU
ANR Rate
<0.5%
>1%
Hang count / session
Launch Time
<2s
>4s
Time to interactive
API Latency (p95)
<500ms
>1000ms
Network monitoring
Memory Peak
<200MB
>350MB
Memory gauge
Disk Usage
<100MB
>500MB
FileManager

6. Alerting Strategy

The right alerting has to be sensitive enough to wake you up at 3 AM, yet smart enough not to overwhelm you with false positives.

Severity Levels

Level
Response Time
Notification
Example
P0 - Critical
15 minutes
SMS + phone
Crash rate >5%
P1 - High
1 hour
Push + email
API completely down
P2 - Medium
4 hours
Email
Crash rate >1%
P3 - Low
Next business day
Dashboard
New crash type

GOLDEN TIP

The most valuable insight in this article

This tip holds the article's most important takeaway.

Reader Reward

Congratulations! You made it to the end of this article, so I have a special gift for you:

Conclusion and Recommendations

Crash reporting and analytics are the two foundational pillars of monitoring your app's health. Set your crash-free rate target, build the right event taxonomy, and put your alerting mechanism in place. Remember: you can't improve what you don't measure.

Tags

#crash reporting#analytics#monitoring#DevOps#iOS#production
Muhittin Çamdalı

Muhittin Çamdalı

Lead Mobile Engineer

Lead Mobile Engineer with 12+ years of experience. Expert in iOS, Android and cross-platform architectures with Swift, SwiftUI, Kotlin and Flutter. I build performant, user-friendly mobile apps.

iOS Development News

Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.

We respect your privacy. You can unsubscribe at any time.

Share