“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: "[email protected]",
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:
.debugfor developer details only needed during development.infofor tracking normal app flow.noticefor important events users would care about.errorfor problems that need fixing but don’t prevent core functionality.faultfor 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)!

