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
- Crash Types
- Crash Lifecycle
- 2. The Symbolication Process
- dSYM Management
- 3. Analytics Event Design
- Event Naming Convention
- 4. Funnel and Cohort Analysis
- Funnel Tracker
- 5. Monitoring Dashboard
- Core Metrics
- 6. Alerting Strategy
- Severity Levels
- Conclusion and Recommendations
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
1// MARK: - Crash Reporter Setup2final 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: CrashStorage8 9 init(storage: CrashStorage = FileBasedCrashStorage()) {10 self.storage = storage11 }12 13 func setup() {14 // 1. Save the previous handler (chaining)15 previousExceptionHandler = NSGetUncaughtExceptionHandler()16 17 // 2. Install our own handler18 NSSetUncaughtExceptionHandler { exception in19 CrashReporter.shared.handleException(exception)20 }21 22 // 3. Signal handlers23 setupSignalHandlers()24 25 // 4. Send crashes from the previous session26 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 handler43 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 in50 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.duration83 )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.identifier93 )94 }95 96 private func sendPendingReports() {97 queue.async { [weak self] in98 guard let reports = self?.storage.readPending() else { return }99 for report in reports {100 APIClient.shared.send(report) { success in101 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
1// Build Settings check2// DEBUG_INFORMATION_FORMAT = dwarf-with-dsym3// DWARF_DSYM_FOLDER_PATH = build/4 5// Symbolication command (Terminal)6// atos -arch arm64 -o MyApp.app.dSYM/Contents/Resources/DWARF/MyApp -l 0x100000000 0x1000123457 8// MARK: - Build Phase Script (dSYM upload)9// #!/bin/bash10// DSYM_PATH="BUILT_PRODUCTS_DIR/PRODUCT_NAME.app.dSYM"11// if [ -d "PATH_TO_DSYM" ]; then12// zip -r dsym.zip "PATH_TO_DSYM"13// curl -X POST https://api.crashreporter.com/dsym14// -F "[email protected]"15// -F "version=APP_VERSION"16// fi3. Analytics Event Design
The right event design is the key to collecting meaningful data.
Event Naming Convention
1// MARK: - Analytics Event System2protocol AnalyticsEvent {3 var name: String { get }4 var properties: [String: Any] { get }5 var timestamp: Date { get }6}7 8enum AppEvent: AnalyticsEvent {9 // Screen Events10 case screenViewed(name: String)11 case screenExited(name: String, duration: TimeInterval)12 13 // User Actions14 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 Events19 case purchaseStarted(productId: String, price: Decimal)20 case purchaseCompleted(productId: String, price: Decimal, method: String)21 case purchaseFailed(productId: String, error: String)22 23 // Engagement24 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
1// MARK: - Funnel Tracker2final class FunnelTracker {3 private let analytics: AnalyticsProviding4 private var activeSteps: [String: FunnelStep] = [:]5 6 struct FunnelStep {7 let funnelName: String8 let stepName: String9 let stepIndex: Int10 let startTime: Date11 }12 13 init(analytics: AnalyticsProviding) {14 self.analytics = analytics15 }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] = funnelStep25 analytics.track("funnel_step", properties: [26 "funnel": name,27 "step": step,28 "step_index": 029 ])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] = next42 analytics.track("funnel_step", properties: [43 "funnel": name,44 "step": step,45 "step_index": next.stepIndex,46 "previous_step_duration": stepDuration47 ])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": totalDuration57 ])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": reason68 ])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
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.

