Introduction
You've shipped your iOS app to production, but now the real work begins: understanding how it's performing and why users are hitting crashes. Without proper observability, you're flying blind—your app might be crashing for thousands of users while you remain unaware. This practical guide on crash reporting and analytics on iOS walks you through the tools and strategies we use across production applications, from Xcode's built-in capabilities to third-party services that give you the depth you need to act quickly.
Xcode Organizer: your first line of defense
Before reaching for external tools, get familiar with Xcode Organizer. Every app on the App Store automatically sends crash reports to Apple, and Xcode Organizer aggregates them for you. This is your baseline—it's free, built-in, and available to every developer.
Open Xcode, go to Window > Organizer, select your app, and navigate to the Crashes tab. You'll see a list of crashes grouped by type, with stack traces that you can symbolicate automatically. For each crash, Xcode shows you:
- Crash frequency and affected users
- The OS versions and devices involved
- Full stack traces with line numbers
- Logs from the moments before the crash
The catch: Xcode Organizer works well for high-volume crashes, but it's not ideal for deep investigation. You get limited context, and the data refreshes slowly. Also, you can't easily set up alerts or integrate crash data into your incident response workflow.
Start here to catch obvious issues. If your app crashes on launch or has a widespread problem, Xcode Organizer will show it. But for production apps with thousands of users, you'll want richer tooling.
Crashlytics and Sentry: comprehensive crash analysis
Once you need to track down specific issues, move to a dedicated crash reporting service. We typically use Crashlytics (now part of Firebase) or Sentry, depending on the project. Both give you detailed crash symbolication, user session context, and actionable insights.
Setting up Crashlytics
If you're already using Firebase, Crashlytics is the natural choice. It integrates seamlessly and requires minimal setup:
import FirebaseCore
import FirebaseCrashlytics
// In your app delegate or main app struct
FirebaseApp.configure()
// Enable native crash reporting
Crashlytics.crashlytics().enableCrashCollection()That's it. Crashlytics automatically captures uncaught exceptions and sends symbolicated stack traces to your Firebase console. For custom events or debugging, log additional context:
import FirebaseCrashlytics
Crashlytics.crashlytics().record(error: error)
// Add custom keys for context
Crashlytics.crashlytics().setCustomValue("user_123", forKey: "userId")
Crashlytics.crashlytics().setCustomValue(cartItems.count, forKey: "cartItemCount")In production, this approach has saved us hours of investigation. When a user reports a payment flow crash, we can instantly see the full session—what screens they visited, what API calls failed, what state the app was in. Crashlytics also surfaces trends: if 10% of users on iOS 16.1 crash on launch, you'll see it immediately.
Using Sentry for more control
Sentry is a strong alternative if you want more flexibility or already use it for backend monitoring. Setup is straightforward:
import Sentry
// In app initialization
SentrySDK.start { options in
options.dsn = "your-sentry-dsn"
options.tracesSampleRate = 1.0
options.environment = "production"
}
// Crashes are captured automatically
// For custom errors:
do {
try risky()
} catch {
SentrySDK.capture(error: error)
}Sentry excels when you need to correlate iOS crashes with backend errors or track performance metrics across your entire stack. It also offers better control over sampling and filtering, useful if you're managing high-volume traffic.
What to track in both
Whichever you choose, attach contextual data that helps you reproduce issues:
- User identifiers: Session ID, user ID, account type
- Feature flags: Which experiments or rollouts was the user in?
- API calls: Recent network requests and responses
- App state: What screen, navigation state, pending operations
- Device info: Memory, storage, network type
Add breadcrumbs for key events in your app flow:
Crashlytics.crashlytics().addLog("User tapped checkout button")
Crashlytics.crashlytics().addLog("Payment API returned 500")
Crashlytics.crashlytics().addLog("App entered background")This turns a bare stack trace into a narrative. You see exactly what the user was doing when things went wrong.
MetricKit: performance data built into iOS
Crashlytics and Sentry capture crashes and errors, but MetricKit gives you visibility into app hang, memory issues, and battery drain—the silent killers that don't always show up as crashes but frustrate users and tank your ratings.
MetricKit is an Apple framework available on iOS 13 and later. It collects diagnostic data (CPU time, memory, disk reads, power consumption) and bundled metrics like app hang time. You request reports from the OS daily:
import MetricKit
class MetricsReceiver: NSObject, MXMetricManagerSubscriber {
override init() {
super.init()
MXMetricManager.shared.add(self)
}
func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
// Process memory issues
if let memoryMetrics = payload.memoryMetrics {
let peakMemory = memoryMetrics.peakMemoryUsage
print("Peak memory: \(peakMemory) bytes")
}
// Process app hang time
if let hangMetrics = payload.applicationTimeMetrics {
let hangTime = hangMetrics.cumulativeMainThreadHangTime
print("Main thread hang time: \(hangTime.duration)")
}
// Send to your backend or Sentry
sendMetricsToBackend(payload)
}
}
}MetricKit data arrives once daily, so it's not real-time. But it's invaluable for understanding patterns. You might discover that every morning at 9am, your app hangs for 2 seconds due to a background sync operation. With that data, you can optimize and verify the fix before users complain.
Integrate MetricKit with your crash reporting service. Sentry has built-in MetricKit support; with Crashlytics, you'll forward the data manually.
Respect App Tracking Transparency
iOS requires you to ask permission before tracking users across apps. This affects how you approach analytics. Your crash reporting and performance tools must work within ATT constraints.
When users deny tracking
If a user denies app tracking transparency, you can still:
- Collect first-party analytics (crashes, errors, performance)
- Send data to your own backend
- Use vendor SDKs like Crashlytics and MetricKit (Apple's own tools are exempt)
You cannot use the IDFA (Identifier for Advertisers) or cross-app tracking. For crash reporting, this rarely matters—you're collecting data about your own app. But for analytics, it means you can't easily build cohorts based on user behavior across multiple apps.
Requesting ATT permission correctly
If you need granular user tracking for analytics, request permission properly:
import AppTrackingTransparency
func requestTrackingPermission() {
if #available(iOS 14, *) {
ATTrackingManager.requestTrackingAuthorization { status in
DispatchQueue.main.async {
switch status {
case .authorized:
print("User authorized tracking")
case .denied:
print("User denied tracking")
case .notDetermined:
print("User hasn't decided yet")
@unknown default:
break
}
}
}
}
}Be transparent about why you need it. Many users decline tracking, so don't rely on IDFA for core functionality. Instead, use first-party identifiers—user IDs from your backend—which work regardless of ATT status.
Common mistakes
- Ignoring Xcode Organizer completely. You're missing free signal. Check it weekly at minimum.
- Over-logging in production. Verbose logging can slow your app and burn through disk space. Be selective about breadcrumbs and custom events.
- Not symbolicating stack traces. If crashes show hex addresses instead of function names, your build symbols aren't being uploaded to your crash reporting service. Verify this in CI/CD.
- Forgetting to capture user context. A crash report with no user ID or session is nearly worthless. Set these early in your app lifecycle.
- Treating all crashes the same. A crash affecting 1% of users on an old device is different from one affecting 50% on the latest OS. Prioritize by impact and scope.
- Assuming high crash rates are just user error. If 5% of users crash during onboarding, it's a bug, not a feature. Track this metric and treat it as a KPI.
Key takeaways
- Start with Xcode Organizer to catch broad patterns; graduate to Crashlytics or Sentry for production apps needing deep context.
- Always attach user ID, session ID, and relevant app state to crash reports so you can reproduce and investigate issues quickly.
- Use MetricKit to surface performance problems—hangs, memory spikes, battery drain—that don't always crash but degrade user experience.
- Respect App Tracking Transparency by using first-party identifiers and understanding that crash reporting works independently of ATT status.
- Treat crash rate as a core product metric. Monitor it in your CI/CD pipeline and set up alerts for regressions.
- Regularly review crash data with your team during standups or retrospectives. It's often the early signal that a feature needs optimization or a fix needs rollout.






Comments
Loading comments…