Skip to content
ExponaBLOG
SWIFT, SWIFTUI & IOS APPS · DAY 070 / 100

Crash reporting and analytics on iOS: a practical guide

Know how your app performs.

Expona Admin7 min read

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:

swift
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:

swift
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:

swift
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:

swift
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:

swift
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:

swift
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.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles