Skip to content

Commit 917decc

Browse files
authored
Merge pull request #25 from FlineDev/wip/reporting
Add convenient way to get log file & attach to email or other places
2 parents 3ae4362 + 680ff48 commit 917decc

24 files changed

+2115
-648
lines changed

Package.resolved

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,20 @@ let package = Package(
66
defaultLocalization: "en",
77
platforms: [.macOS(.v13), .iOS(.v16), .tvOS(.v16), .watchOS(.v9), .macCatalyst(.v16)],
88
products: [.library(name: "ErrorKit", targets: ["ErrorKit"])],
9+
dependencies: [
10+
// CryptoKit is not available on Linux, so we need Swift Crypto
11+
.package(url: "https://github.com/apple/swift-crypto.git", from: "3.11.0"),
12+
],
913
targets: [
1014
.target(
1115
name: "ErrorKit",
16+
dependencies: [
17+
.product(
18+
name: "Crypto",
19+
package: "swift-crypto",
20+
condition: .when(platforms: [.android, .linux, .openbsd, .wasi, .windows])
21+
),
22+
],
1223
resources: [.process("Resources/Localizable.xcstrings")]
1324
),
1425
.testTarget(name: "ErrorKitTests", dependencies: ["ErrorKit"]),

README.md

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ ErrorKit makes error handling in Swift more intuitive. It reduces boilerplate co
1010
- [Typed Throws for System Functions](#typed-throws-for-system-functions)
1111
- [Error Nesting with Catching](#error-nesting-with-catching)
1212
- [Error Chain Debugging](#error-chain-debugging)
13+
- [User Feedback with Error Logs](#user-feedback-with-error-logs)
1314

1415
## The Problem with Swift's Error Protocol
1516

@@ -544,3 +545,122 @@ This precise grouping allows you to:
544545
### Summary
545546
546547
ErrorKit's debugging tools transform error handling from a black box into a transparent system. By combining `errorChainDescription` for debugging with `groupingID` for analytics, you get deep insight into error flows while maintaining the ability to track and prioritize issues effectively. This is particularly powerful when combined with ErrorKit's `Catching` protocol, creating a comprehensive system for error handling, debugging, and monitoring.
548+
549+
550+
## User Feedback with Error Logs
551+
552+
When users encounter issues in your app, getting enough context to diagnose the problem can be challenging. Users rarely know what information you need, and reproducing issues without logs is often impossible. 😕
553+
554+
ErrorKit makes it simple to add diagnostic log collection to your app, providing crucial context for bug reports and support requests.
555+
556+
### The Power of System Logs
557+
558+
ErrorKit leverages Apple's unified logging system (`OSLog`/`Logger`) to collect valuable diagnostic information. If you're not already using structured logging, here's a quick primer:
559+
560+
```swift
561+
import OSLog
562+
563+
// Log at appropriate levels
564+
Logger().debug("Detailed connection info: \(details)") // Development debugging
565+
Logger().info("User tapped on \(button)") // General information
566+
Logger().notice("Successfully loaded user profile") // Important events
567+
Logger().error("Failed to parse server response") // Errors that should be fixed
568+
Logger().fault("Database corruption detected") // Critical system failures
569+
```
570+
571+
ErrorKit can collect these logs based on level, giving you control over how much detail to include in reports. 3rd-party frameworks that also use Apple's unified logging system will be included so you get a full picture of what happened in your app, not just what you logged yourself.
572+
573+
### Creating a Feedback Button with Automatic Log Collection
574+
575+
The easiest way to implement a support system is using the `.mailComposer` SwiftUI modifier combined with `logAttachment`:
576+
577+
```swift
578+
struct ContentView: View {
579+
@State private var showMailComposer = false
580+
581+
var body: some View {
582+
Form {
583+
// Your app content here
584+
585+
Button("Report a Problem") {
586+
showMailComposer = true
587+
}
588+
.mailComposer(
589+
isPresented: $showMailComposer,
590+
recipient: "support@yourapp.com",
591+
subject: "<AppName> Bug Report",
592+
messageBody: """
593+
Please describe what happened:
594+
595+
596+
597+
----------------------------------
598+
[Please do not remove the information below]
599+
600+
App version: \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown")
601+
Build: \(Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown")
602+
Device: \(UIDevice.current.model)
603+
iOS: \(UIDevice.current.systemVersion)
604+
""",
605+
attachments: [
606+
try? ErrorKit.logAttachment(ofLast: .minutes(30), minLevel: .notice)
607+
]
608+
)
609+
}
610+
}
611+
}
612+
```
613+
614+
This creates a simple "Report a Problem" button that:
615+
1. Opens a pre-filled email composer
616+
2. Includes useful device and app information
617+
3. Automatically attaches recent system logs
618+
4. Provides space for the user to describe the issue
619+
620+
The above is just an example, feel free to adjust it to your needs and include any additional info needed.
621+
622+
### Alternative Methods for More Control
623+
624+
If you need more control over log handling, ErrorKit offers two additional approaches:
625+
626+
#### 1. Getting Log Data Directly
627+
628+
For sending logs to your own backend or processing them in-app:
629+
630+
```swift
631+
let logData = try ErrorKit.loggedData(
632+
ofLast: .minutes(10),
633+
minLevel: .notice
634+
)
635+
636+
// Use the data with your custom reporting system
637+
analyticsService.sendLogs(data: logData)
638+
```
639+
640+
#### 2. Exporting to a Temporary File
641+
642+
For sharing logs via other mechanisms:
643+
644+
```swift
645+
let logFileURL = try ErrorKit.exportLogFile(
646+
ofLast: .hours(1),
647+
minLevel: .error
648+
)
649+
650+
// Share the log file
651+
let activityVC = UIActivityViewController(
652+
activityItems: [logFileURL],
653+
applicationActivities: nil
654+
)
655+
present(activityVC, animated: true)
656+
```
657+
658+
### Benefits of Automatic Log Collection
659+
660+
- **Better bug reports**: Get the context you need without asking users for technical details
661+
- **Faster issue resolution**: See exactly what happened leading up to the problem
662+
- **Lower support burden**: Reduce back-and-forth communications with users
663+
- **User satisfaction**: Demonstrate that you take their problems seriously
664+
- **Developer sanity**: Stop trying to reproduce issues with insufficient information
665+
666+
By implementing a feedback button with automatic log collection, you transform the error reporting experience for both users and developers. Users can report issues with a single tap, and you get the diagnostic information you need to fix problems quickly.

Sources/ErrorKit/BuiltInErrors/DatabaseError.swift

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -136,29 +136,25 @@ public enum DatabaseError: Throwable, Catching {
136136
public var userFriendlyMessage: String {
137137
switch self {
138138
case .connectionFailed:
139-
return String(
140-
localized: "BuiltInErrors.DatabaseError.connectionFailed",
141-
defaultValue: "Unable to establish a connection to the database. Check your network settings and try again.",
142-
bundle: .module
139+
return String.localized(
140+
key: "BuiltInErrors.DatabaseError.connectionFailed",
141+
defaultValue: "Unable to establish a connection to the database. Check your network settings and try again."
143142
)
144143
case .operationFailed(let context):
145-
return String(
146-
localized: "BuiltInErrors.DatabaseError.operationFailed",
147-
defaultValue: "The database operation for \(context) could not be completed. Please retry the action.",
148-
bundle: .module
144+
return String.localized(
145+
key: "BuiltInErrors.DatabaseError.operationFailed",
146+
defaultValue: "The database operation for \(context) could not be completed. Please retry the action."
149147
)
150148
case .recordNotFound(let entity, let identifier):
151149
if let identifier {
152-
return String(
153-
localized: "BuiltInErrors.DatabaseError.recordNotFoundWithID",
154-
defaultValue: "The \(entity) record with ID \(identifier) was not found in the database. Verify the details and try again.",
155-
bundle: .module
150+
return String.localized(
151+
key: "BuiltInErrors.DatabaseError.recordNotFoundWithID",
152+
defaultValue: "The \(entity) record with ID \(identifier) was not found in the database. Verify the details and try again."
156153
)
157154
} else {
158-
return String(
159-
localized: "BuiltInErrors.DatabaseError.recordNotFound",
160-
defaultValue: "The \(entity) record was not found in the database. Verify the details and try again.",
161-
bundle: .module
155+
return String.localized(
156+
key: "BuiltInErrors.DatabaseError.recordNotFound",
157+
defaultValue: "The \(entity) record was not found in the database. Verify the details and try again."
162158
)
163159
}
164160
case .generic(let userFriendlyMessage):

Sources/ErrorKit/BuiltInErrors/FileError.swift

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -128,22 +128,19 @@ public enum FileError: Throwable, Catching {
128128
public var userFriendlyMessage: String {
129129
switch self {
130130
case .fileNotFound(let fileName):
131-
return String(
132-
localized: "BuiltInErrors.FileError.fileNotFound",
133-
defaultValue: "The file \(fileName) could not be located. Please verify the file path and try again.",
134-
bundle: .module
131+
return String.localized(
132+
key: "BuiltInErrors.FileError.fileNotFound",
133+
defaultValue: "The file \(fileName) could not be located. Please verify the file path and try again."
135134
)
136135
case .readFailed(let fileName):
137-
return String(
138-
localized: "BuiltInErrors.FileError.readError",
139-
defaultValue: "An error occurred while attempting to read the file \(fileName). Please check file permissions and try again.",
140-
bundle: .module
136+
return String.localized(
137+
key: "BuiltInErrors.FileError.readError",
138+
defaultValue: "An error occurred while attempting to read the file \(fileName). Please check file permissions and try again."
141139
)
142140
case .writeFailed(let fileName):
143-
return String(
144-
localized: "BuiltInErrors.FileError.writeError",
145-
defaultValue: "Unable to write to the file \(fileName). Ensure you have the necessary permissions and try again.",
146-
bundle: .module
141+
return String.localized(
142+
key: "BuiltInErrors.FileError.writeError",
143+
defaultValue: "Unable to write to the file \(fileName). Ensure you have the necessary permissions and try again."
147144
)
148145
case .generic(let userFriendlyMessage):
149146
return userFriendlyMessage

Sources/ErrorKit/BuiltInErrors/NetworkError.swift

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -178,39 +178,38 @@ public enum NetworkError: Throwable, Catching {
178178
public var userFriendlyMessage: String {
179179
switch self {
180180
case .noInternet:
181-
return String(
182-
localized: "BuiltInErrors.NetworkError.noInternet",
183-
defaultValue: "Unable to connect to the internet. Please check your network settings and try again.",
184-
bundle: .module
181+
return String.localized(
182+
key: "BuiltInErrors.NetworkError.noInternet",
183+
defaultValue: "Unable to connect to the internet. Please check your network settings and try again."
185184
)
186185
case .timeout:
187-
return String(
188-
localized: "BuiltInErrors.NetworkError.timeout",
189-
defaultValue: "The network request took too long to complete. Please check your connection and try again.",
190-
bundle: .module
186+
return String.localized(
187+
key: "BuiltInErrors.NetworkError.timeout",
188+
defaultValue: "The network request took too long to complete. Please check your connection and try again."
191189
)
192190
case .badRequest(let code, let message):
193-
return String(
194-
localized: "BuiltInErrors.NetworkError.badRequest",
195-
defaultValue: "There was an issue with the request (Code: \(code)). \(message). Please review and retry.",
196-
bundle: .module
191+
return String.localized(
192+
key: "BuiltInErrors.NetworkError.badRequest",
193+
defaultValue: "There was an issue with the request (Code: \(code)). \(message). Please review and retry."
197194
)
198195
case .serverError(let code, let message):
199-
let defaultMessage = String(
200-
localized: "BuiltInErrors.NetworkError.serverError",
201-
defaultValue: "The server encountered an error (Code: \(code)). ",
202-
bundle: .module
196+
let defaultMessage = String.localized(
197+
key: "BuiltInErrors.NetworkError.serverError",
198+
defaultValue: "The server encountered an error (Code: \(code)). "
203199
)
200+
204201
if let message = message {
205202
return defaultMessage + message
206203
} else {
207-
return defaultMessage + "Please try again later."
204+
return defaultMessage + String.localized(
205+
key: "Common.Message.tryAgainLater",
206+
defaultValue: "Please try again later."
207+
)
208208
}
209209
case .decodingFailure:
210-
return String(
211-
localized: "BuiltInErrors.NetworkError.decodingFailure",
212-
defaultValue: "Unable to process the server's response. Please try again or contact support if the issue persists.",
213-
bundle: .module
210+
return String.localized(
211+
key: "BuiltInErrors.NetworkError.decodingFailure",
212+
defaultValue: "Unable to process the server's response. Please try again or contact support if the issue persists."
214213
)
215214
case .generic(let userFriendlyMessage):
216215
return userFriendlyMessage

Sources/ErrorKit/BuiltInErrors/OperationError.swift

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,16 +108,14 @@ public enum OperationError: Throwable, Catching {
108108
public var userFriendlyMessage: String {
109109
switch self {
110110
case .dependencyFailed(let dependency):
111-
return String(
112-
localized: "BuiltInErrors.OperationError.dependencyFailed",
113-
defaultValue: "The operation could not be started because a required component failed to initialize: \(dependency). Please restart the application or contact support.",
114-
bundle: .module
111+
return String.localized(
112+
key: "BuiltInErrors.OperationError.dependencyFailed",
113+
defaultValue: "The operation could not be started because a required component failed to initialize: \(dependency). Please restart the application or contact support."
115114
)
116115
case .canceled:
117-
return String(
118-
localized: "BuiltInErrors.OperationError.canceled",
119-
defaultValue: "The operation was canceled at your request. You can retry the action if needed.",
120-
bundle: .module
116+
return String.localized(
117+
key: "BuiltInErrors.OperationError.canceled",
118+
defaultValue: "The operation was canceled at your request. You can retry the action if needed."
121119
)
122120
case .generic(let userFriendlyMessage):
123121
return userFriendlyMessage

Sources/ErrorKit/BuiltInErrors/ParsingError.swift

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,16 +108,14 @@ public enum ParsingError: Throwable, Catching {
108108
public var userFriendlyMessage: String {
109109
switch self {
110110
case .invalidInput(let input):
111-
return String(
112-
localized: "BuiltInErrors.ParsingError.invalidInput",
113-
defaultValue: "The provided input could not be processed correctly: \(input). Please review the input and ensure it matches the expected format.",
114-
bundle: .module
111+
return String.localized(
112+
key: "BuiltInErrors.ParsingError.invalidInput",
113+
defaultValue: "The provided input could not be processed correctly: \(input). Please review the input and ensure it matches the expected format."
115114
)
116115
case .missingField(let field):
117-
return String(
118-
localized: "BuiltInErrors.ParsingError.missingField",
119-
defaultValue: "The required information is incomplete. The \(field) field is missing and must be provided to continue.",
120-
bundle: .module
116+
return String.localized(
117+
key: "BuiltInErrors.ParsingError.missingField",
118+
defaultValue: "The required information is incomplete. The \(field) field is missing and must be provided to continue."
121119
)
122120
case .generic(let userFriendlyMessage):
123121
return userFriendlyMessage

Sources/ErrorKit/BuiltInErrors/PermissionError.swift

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -129,22 +129,19 @@ public enum PermissionError: Throwable, Catching {
129129
public var userFriendlyMessage: String {
130130
switch self {
131131
case .denied(let permission):
132-
return String(
133-
localized: "BuiltInErrors.PermissionError.denied",
134-
defaultValue: "Access to \(permission) was declined. To use this feature, please enable the permission in your device Settings.",
135-
bundle: .module
132+
return String.localized(
133+
key: "BuiltInErrors.PermissionError.denied",
134+
defaultValue: "Access to \(permission) was declined. To use this feature, please enable the permission in your device Settings."
136135
)
137136
case .restricted(let permission):
138-
return String(
139-
localized: "BuiltInErrors.PermissionError.restricted",
140-
defaultValue: "Access to \(permission) is currently restricted. This may be due to system settings or parental controls.",
141-
bundle: .module
137+
return String.localized(
138+
key: "BuiltInErrors.PermissionError.restricted",
139+
defaultValue: "Access to \(permission) is currently restricted. This may be due to system settings or parental controls."
142140
)
143141
case .notDetermined(let permission):
144-
return String(
145-
localized: "BuiltInErrors.PermissionError.notDetermined",
146-
defaultValue: "Permission for \(permission) has not been confirmed. Please review and grant access in your device Settings.",
147-
bundle: .module
142+
return String.localized(
143+
key: "BuiltInErrors.PermissionError.notDetermined",
144+
defaultValue: "Permission for \(permission) has not been confirmed. Please review and grant access in your device Settings."
148145
)
149146
case .generic(let userFriendlyMessage):
150147
return userFriendlyMessage

0 commit comments

Comments
 (0)