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.

"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:
- They don't know what information you need
- They can't easily access system logs
- They struggle to remember and articulate exact steps
- Complex issues may involve multiple components
- 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:
- Opens a pre-filled email composer
- Includes device and app information
- Automatically attaches recent system logs
- 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:
- Third-party frameworks that use Apple's unified logging system
- System components your app interacts with (networking, file system, etc.)
- 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:
- Identify common issues: See which errors occur most frequently
- Prioritize fixes: Focus on high-impact problems first
- Track resolution: Monitor if error rates decrease after fixes
- Detect new issues: Quickly identify new error patterns after releases
- 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:
- All errors are tracked for analytics, giving you broad patterns
- Serious errors prompt users for detailed feedback with logs
- 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:
How do you handle user feedback and error reporting? Have you found other effective techniques that actually help? Tell me on socials (links below)!
A simple & fast AI-based translator for String Catalogs & more.
Get it now and localize your app to over 100 languages in minutes!