Better Error Reporting in Swift Apps: Automatic Logs + Analytics

Tired of vague bug reports like "it doesn't work"? In this post, you'll learn how to collect automatic logs and track real-world errors in your Swift apps—with just a few lines of code.

Better Error Reporting in Swift Apps: Automatic Logs + Analytics

"It doesn't work."

If you've ever supported an iOS app, you've received this frustratingly vague user feedback. No steps to reproduce, no error message, no context—just the dreaded "doesn't work" report that leaves you with more questions than answers.

Even the most detail-oriented users rarely know what information you need to diagnose issues. And when they do try to help, they might not have the technical knowledge to provide the right details. This disconnect creates a frustrating experience for everyone involved.

In this post, I'll share two practical approaches I've implemented in ErrorKit to bridge this gap: a simple feedback button that automatically collects diagnostic logs, and a structured approach to error analytics that helps you identify patterns even without direct user reports.

The Missing Context Problem

When users encounter issues, several challenges make diagnosis difficult:

  1. They don't know what information you need
  2. They can't easily access system logs
  3. They struggle to remember and articulate exact steps
  4. Complex issues may involve multiple components
  5. Intermittent issues are hard to reproduce on demand

Without proper context, debugging becomes a guessing game. You might spend hours trying to reproduce an issue that could be solved in minutes with the right information.

Solution 1: Feedback Button with Logs Attached

The first solution is to make it ridiculously easy for users to send you complete information. ErrorKit provides a SwiftUI modifier that adds a mail composer with automatic log collection:

struct ContentView: View {
    @State private var showMailComposer = false
    
    var body: some View {
        VStack {
            // Your app content
            
            Button("Report a Problem") {
                showMailComposer = true
            }
            .mailComposer(
                isPresented: $showMailComposer,
                recipient: "support@yourapp.com",
                subject: "YourApp Bug Report",
                messageBody: """
                   Please describe what happened:
                   
                   
                   
                   ----------------------------------
                   Device: \(UIDevice.current.model)
                   iOS: \(UIDevice.current.systemVersion)
                   App version: \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown")
                   """,
                attachments: [
                    try? ErrorKit.logAttachment(ofLast: .minutes(30))
                ]
            )
        }
    }
}

This creates a simple "Report a Problem" button that:

  1. Opens a pre-filled email composer
  2. Includes device and app information
  3. Automatically attaches recent system logs
  4. Provides space for the user to describe the issue

The log attachment is the secret sauce here. When the user taps this button after encountering an issue, you get a comprehensive picture of what was happening in and around your app when the problem occurred.

Leveraging Apple's Unified Logging System

ErrorKit uses Apple's unified logging system (OSLog/Logger) to collect diagnostic information. If you're not already using structured logging, here's a quick intro:

import OSLog

// Create loggers
let logger = Logger()
// or with subsystem and category
let networkLogger = Logger(subsystem: "com.yourapp", category: "networking")

// Log at appropriate levels
logger.debug("Detailed connection info")      // Development debugging
logger.info("User tapped submit button")      // General information
logger.notice("Profile successfully loaded")   // Important events
logger.error("Failed to load user data")      // Errors that should be fixed
logger.fault("Database corruption detected")   // System failures

// Format values and control privacy
logger.info("User \(userId, privacy: .private) logged in from \(ipAddress, privacy: .public)")

The unified logging system provides several advantages over print() statements:

  • Log levels for filtering information
  • Privacy controls for sensitive data
  • Efficient performance with minimal overhead
  • Persistence across app launches

Comprehensive Log Collection

A key advantage of ErrorKit's approach is that it captures not just your app's logs, but also relevant logs from:

  1. Third-party frameworks that use Apple's unified logging system
  2. System components your app interacts with (networking, file system, etc.)
  3. Background processes related to your app's functionality

This gives you a complete picture of what was happening in and around your app when the issue occurred—not just the logs you explicitly added.

Controlling Log Collection

You can customize log collection to balance detail and privacy:

// Collect logs from last 30 minutes with notice level or higher (default)
try ErrorKit.logAttachment(ofLast: .minutes(30), minLevel: .notice)

// Collect logs from last hour with error level or higher (less verbose)
try ErrorKit.logAttachment(ofLast: .hours(1), minLevel: .error)

// Collect logs from last 5 minutes with debug level (very detailed)
try ErrorKit.logAttachment(ofLast: .minutes(5), minLevel: .debug)

The minLevel parameter filters logs by importance:

  • .debug: All logs (very verbose)
  • .info: Informational logs and above
  • .notice: Notable events (default)
  • .error: Only errors and faults
  • .fault: Only critical errors

This gives you control over how much information you collect while still providing the context you need for diagnosis.

Alternative Methods for More Control

If you need more control over log handling, ErrorKit offers additional approaches:

Getting Log Data Directly

For sending logs to your own backend or processing them in-app, use loggedData:

let logData = try ErrorKit.loggedData(
    ofLast: .minutes(10),
    minLevel: .notice
)

// Use the data with your custom reporting system
analyticsService.sendLogs(data: logData)

Exporting to a Temporary File

For sharing logs via other mechanisms, use exportLogFile:

let logFileURL = try ErrorKit.exportLogFile(
    ofLast: .hours(1),
    minLevel: .error
)

// Share the log file
let activityVC = UIActivityViewController(
    activityItems: [logFileURL],
    applicationActivities: nil
)
present(activityVC, animated: true)

Solution 2: Smart Error Analytics with Grouping IDs

While the feedback button helps users report issues they notice, many problems go unreported. Users might encounter an error, shrug, and try again—never telling you about it. That's where error analytics comes in.

ErrorKit provides tools to automatically track errors and group them intelligently:

func handleError(_ error: Error) {
    // Get a stable ID that ignores dynamic parameters
    let groupID = ErrorKit.groupingID(for: error)
    
    // Get the full error chain description
    let errorDetails = ErrorKit.errorChainDescription(for: error)
    
    // Send to your analytics system
    Analytics.track(
        event: "error_occurred",
        properties: [
            "error_group": groupID,
            "error_details": errorDetails,
            "user_id": currentUser.id
        ]
    )
    
    // Show appropriate UI to the user
    showErrorAlert(message: ErrorKit.userFriendlyMessage(for: error))
}

Sample global error handling function to add to your app.

The magic here is in the groupingID(for:) function. It generates a stable identifier based on the error's type structure and enum cases, ignoring dynamic parameters and localized messages.

This means that errors with the same underlying cause will have the same grouping ID, even if specific details (like file paths or user IDs) differ:

// Both generate the same groupID: "3f9d2a"
ProfileError
└─ DatabaseError
   └─ FileError.notFound(path: "/Users/john/data.db")

ProfileError
└─ DatabaseError
   └─ FileError.notFound(path: "/Users/jane/backup.db")

This approach provides several benefits:

  1. Identify common issues: See which errors occur most frequently
  2. Prioritize fixes: Focus on high-impact problems first
  3. Track resolution: Monitor if error rates decrease after fixes
  4. Detect new issues: Quickly identify new error patterns after releases
  5. Correlate with user segments: See if some errors affect specific users

Combine Both Approaches for Max Insight

A powerful approach is to combine automatic analytics with user-initiated feedback, so you might want to do something like this:

func handleError(_ error: Error) {
    // Always track for analytics
    trackErrorAnalytics(error)
    
    // For serious or unexpected errors, prompt for feedback
    if isSerious(error) {
        showErrorAlert(
            message: ErrorKit.userFriendlyMessage(for: error),
            feedbackOption: true
        )
    } else {
        // For minor issues, just show a message
        showErrorAlert(message: ErrorKit.userFriendlyMessage(for: error))
    }
}

func showErrorAlert(message: String, feedbackOption: Bool = false) {
    // Implementation of an alert that optionally includes a
    // "Send Feedback" button that opens the mail composer with logs
}

This creates a comprehensive system where:

  1. All errors are tracked for analytics, giving you broad patterns
  2. Serious errors prompt users for detailed feedback with logs
  3. Users can always initiate feedback for issues you might not track

Best Practices for Logging

To maximize the value of log collection, consider these best practices:

1. Structure Logs for Context

Provide enough context in your logs to understand what was happening:

// Instead of:
Logger().error("Failed to load")

// Use:
Logger().error("Failed to load document \(documentId): \(ErrorKit.errorChainDescription(for: error))")

2. Choose Appropriate Log Levels

Use log levels strategically to control verbosity:

  • .debug for developer details only needed during development
  • .info for tracking normal app flow
  • .notice for important events users would care about
  • .error for problems that need fixing but don't prevent core functionality
  • .fault for critical issues that break core functionality

3. Protect Sensitive Information

Use privacy modifiers to protect user data:

Logger().info("Processing payment for user \(userId, privacy: .private)")

4. Log Key User Actions

Create breadcrumbs of user activity to understand the path to errors:

Logger().notice("User navigated to profile screen")
Logger().info("User tapped edit button")
Logger().notice("User saved profile changes")

5. Log Start and Completion of Important Operations

Bracket significant operations to identify incomplete tasks:

Logger().notice("Starting data sync")
// ... sync implementation
Logger().notice("Completed data sync")

The Impact on Support and Development

Implementing these tools can transform both user experience and development workflows:

For Users:

  • Simplified Reporting: Submit feedback with a single tap
  • No Technical Questions: Avoid frustrating back-and-forth communications
  • Faster Resolution: Issues can be diagnosed and fixed more quickly
  • Better Experience: Shows users you take their problems seriously

For Developers:

  • Complete Context: See exactly what was happening when issues occurred
  • Reduced Support Time: Less time spent asking for additional information
  • Better Reproduction: More reliable reproduction steps based on log data
  • Efficient Debugging: Quickly identify patterns in error reports
  • Data-Driven Priorities: Focus on fixing the most common issues first

Conclusion

ErrorKit's approach bridges that frustrating gap between a user saying "it doesn't work" and actually knowing what happened. I've found that automatic log collection combined with smart error analytics creates a feedback loop that actually works. 💪

What's really powerful is getting detailed logs when users choose to report problems while also catching the issues they never mention. This dual approach has transformed how I understand and fix problems in my apps. If you're tired of debugging issues blindfolded, ErrorKit includes all these logging tools and error handling improvements—tools I built because I needed them myself:

GitHub - FlineDev/ErrorKit: Simplified error handling with built-in user-friendly messages for common errors. Fully localized. Community-driven.
Simplified error handling with built-in user-friendly messages for common errors. Fully localized. Community-driven. - FlineDev/ErrorKit

How do you handle user feedback and error reporting? Have you found other effective techniques that actually help? Tell me on socials (links below)!

🌐
Wanna grow your app? Check out TranslateKit!
A simple & fast AI-based translator for String Catalogs & more.
Get it now and localize your app to over 100 languages in minutes!
👨‍💻
Want to Connect?
Follow me on 🦋 Bluesky, 🐦 Twitter (X), and 🦣 Mastodon.