-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPlayolaErrorReporter.swift
More file actions
277 lines (241 loc) · 8.47 KB
/
Copy pathPlayolaErrorReporter.swift
File metadata and controls
277 lines (241 loc) · 8.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
//
// PlayolaErrorReporter.swift
// PlayolaPlayer
//
// Created by Brian D Keane on 3/21/25.
//
//
// PlayolaErrorReporter.swift
// PlayolaPlayer
//
// Created on 3/21/25.
//
import Foundation
import os.log
/// Protocol defining the methods a delegate must implement to receive error reports
public protocol PlayolaErrorReporterDelegate: AnyObject {
/// Called when an error occurs within the PlayolaPlayer library
/// - Parameters:
/// - error: The error that occurred
/// - sourceFile: The file where the error originated
/// - sourceLine: The line number where the error originated
/// - function: The function where the error originated
/// - stackTrace: A string representation of the stack trace
func playolaDidEncounterError(
_ error: Error,
sourceFile: String,
sourceLine: Int,
function: String,
stackTrace: String)
}
/// Error reporting levels to control the verbosity of logging
public enum PlayolaErrorReportingLevel: Int, Sendable {
case none = 0 // No error reporting
case critical = 1 // Only critical errors that prevent functionality
case error = 2 // All errors
case warning = 3 // Errors and warnings
case debug = 4 // Everything including debug information
}
/// Contains information about a PlayolaPlayer error
public struct PlayolaErrorReport: Sendable {
public let error: Error
public let sourceFile: String
public let sourceLine: Int
public let function: String
public let stackTrace: String
public let timestamp: Date
public let threadName: String
public let reportingLevel: PlayolaErrorReportingLevel
init(
error: Error,
sourceFile: String,
sourceLine: Int,
function: String,
stackTrace: String = Thread.callStackSymbols.joined(separator: "\n"),
timestamp: Date = Date(),
threadName: String = Thread.current.description,
reportingLevel: PlayolaErrorReportingLevel = .error
) {
self.error = error
self.sourceFile = sourceFile
self.sourceLine = sourceLine
self.function = function
self.stackTrace = stackTrace
self.timestamp = timestamp
self.threadName = threadName
self.reportingLevel = reportingLevel
}
}
/// Main error reporting class for PlayolaPlayer
public actor PlayolaErrorReporter {
// MARK: - Singleton
public static let shared = PlayolaErrorReporter()
// MARK: - Properties
private static let logger = OSLog(
subsystem: "fm.playola.PlayolaPlayer", category: "ErrorReporter")
/// Delegate that will receive error reports
public weak var delegate: PlayolaErrorReporterDelegate?
/// Controls which errors get reported to the delegate
public var reportingLevel: PlayolaErrorReportingLevel = .error
/// Whether to log errors to system console even if no delegate is set
public var logToConsole: Bool = true
/// Maximum stack frame count to include in reports
public var maxStackFrames: Int = 20
/// Keeps track of recently reported errors to avoid duplicate reporting
private var recentErrorHashes: [Int: Date] = [:]
private let duplicateThresholdSeconds: TimeInterval = 5
// MARK: - Initialization
private init() {}
// MARK: - Public Methods
/// Report an error with source information
/// - Parameters:
/// - error: The error to report
/// - file: Source file (automatically provided by default)
/// - line: Source line (automatically provided by default)
/// - function: Function name (automatically provided by default)
/// - level: The severity level of this error
public func reportError(
_ error: Error,
file: String = #file,
line: Int = #line,
function: String = #function,
level: PlayolaErrorReportingLevel = .error
) async {
// Don't process if reporting level is not high enough
guard level.rawValue <= reportingLevel.rawValue else { return }
// Get file name without path
let fileName = URL(fileURLWithPath: file).lastPathComponent
// Capture stack trace
let stackTrace = formatStackTrace(frames: Thread.callStackSymbols, maxFrames: maxStackFrames)
// Create error report
let report = PlayolaErrorReport(
error: error,
sourceFile: fileName,
sourceLine: line,
function: function,
stackTrace: stackTrace,
reportingLevel: level
)
// Deduplicate recent identical errors
let errorHash = hashForError(error, file: fileName, line: line, function: function)
if await isDuplicateError(hash: errorHash) {
return
}
// Log to system console if enabled
if logToConsole {
logErrorToConsole(report)
}
// Call delegate on main thread
if let delegate = delegate {
await MainActor.run {
delegate.playolaDidEncounterError(
report.error,
sourceFile: report.sourceFile,
sourceLine: report.sourceLine,
function: report.function,
stackTrace: report.stackTrace
)
}
}
}
/// Report an error with additional context
/// - Parameters:
/// - error: The error to report
/// - context: Additional context to include in the error report
/// - file: Source file (automatically provided by default)
/// - line: Source line (automatically provided by default)
/// - function: Function name (automatically provided by default)
/// - level: The severity level of this error
public func reportError(
_ error: Error,
context: String,
file: String = #file,
line: Int = #line,
function: String = #function,
level: PlayolaErrorReportingLevel = .error
) async {
// Wrap the error with context
let contextualError = NSError(
domain: "fm.playola.PlayolaPlayer",
code: (error as NSError).code,
userInfo: [
NSLocalizedDescriptionKey: "\(context): \(error.localizedDescription)",
NSUnderlyingErrorKey: error,
]
)
await reportError(contextualError, file: file, line: line, function: function, level: level)
}
// MARK: - Private Methods
private func logErrorToConsole(_ report: PlayolaErrorReport) {
let errorMessage = """
PLAYOLA ERROR: \(report.error.localizedDescription)
File: \(report.sourceFile):\(report.sourceLine)
Function: \(report.function)
Thread: \(report.threadName)
Stack Trace:
\(report.stackTrace)
"""
switch report.reportingLevel {
case .critical:
os_log(.fault, log: PlayolaErrorReporter.logger, "%{public}@", errorMessage)
case .error:
os_log(.error, log: PlayolaErrorReporter.logger, "%{public}@", errorMessage)
case .warning:
os_log(.info, log: PlayolaErrorReporter.logger, "%{public}@", errorMessage)
case .debug:
os_log(.debug, log: PlayolaErrorReporter.logger, "%{public}@", errorMessage)
case .none:
break
}
}
private func formatStackTrace(frames: [String], maxFrames: Int) -> String {
// Take only the specified number of frames and join them with newlines
return frames.prefix(maxFrames).joined(separator: "\n")
}
private func hashForError(_ error: Error, file: String, line: Int, function: String) -> Int {
// Create a hash that uniquely identifies this error instance
var hasher = Hasher()
hasher.combine(error.localizedDescription)
hasher.combine(file)
hasher.combine(line)
hasher.combine(function)
return hasher.finalize()
}
private func isDuplicateError(hash: Int) async -> Bool {
let now = Date()
// Clean up old entries
recentErrorHashes = recentErrorHashes.filter { _, date in
now.timeIntervalSince(date) < duplicateThresholdSeconds
}
// Check if this is a recent duplicate
if let lastReported = recentErrorHashes[hash],
now.timeIntervalSince(lastReported) < duplicateThresholdSeconds
{
return true
}
// Not a duplicate, add to recent errors
recentErrorHashes[hash] = now
return false
}
}
// MARK: - Convenience Extensions
/// Extension to provide a simpler way to report errors
extension Error {
/// Report this error through the PlayolaErrorReporter
/// - Parameters:
/// - file: Source file (automatically provided by default)
/// - line: Source line (automatically provided by default)
/// - function: Function name (automatically provided by default)
/// - level: The severity level of this error
public func playolaReport(
file: String = #file,
line: Int = #line,
function: String = #function,
level: PlayolaErrorReportingLevel = .error
) {
Task {
await PlayolaErrorReporter.shared.reportError(
self, file: file, line: line, function: function, level: level)
}
}
}