11import Foundation
22import Cocoa
33import Sentry
4+ import os. log
5+
6+ // MARK: - Sentry Logger
7+ /// A unified logger that writes to both os_log and Sentry breadcrumbs
8+ /// Usage: Log.debug("message"), Log.info("message"), Log.warning("message"), Log.error("message"), Log.fatal("message")
9+ enum Log {
10+ private static let subsystem = Bundle . main. bundleIdentifier ?? " com.middledrag "
11+
12+ // OS Log categories
13+ private static let gestureLog = OSLog ( subsystem: subsystem, category: " gesture " )
14+ private static let deviceLog = OSLog ( subsystem: subsystem, category: " device " )
15+ private static let analyticsLog = OSLog ( subsystem: subsystem, category: " analytics " )
16+ private static let appLog = OSLog ( subsystem: subsystem, category: " app " )
17+
18+ enum Category : String {
19+ case gesture
20+ case device
21+ case analytics
22+ case app
23+
24+ var osLog : OSLog {
25+ switch self {
26+ case . gesture: return Log . gestureLog
27+ case . device: return Log . deviceLog
28+ case . analytics: return Log . analyticsLog
29+ case . app: return Log . appLog
30+ }
31+ }
32+ }
33+
34+ /// Debug level - only in debug builds, not sent to Sentry
35+ static func debug( _ message: String , category: Category = . app) {
36+ #if DEBUG
37+ os_log ( . debug, log: category. osLog, " %{public}@ " , message)
38+ #endif
39+ }
40+
41+ /// Info level - logged locally and as Sentry breadcrumb
42+ static func info( _ message: String , category: Category = . app) {
43+ os_log ( . info, log: category. osLog, " %{public}@ " , message)
44+ addBreadcrumb ( message: message, category: category, level: . info)
45+ }
46+
47+ /// Warning level - logged locally and as Sentry breadcrumb
48+ static func warning( _ message: String , category: Category = . app) {
49+ os_log ( . error, log: category. osLog, " ⚠️ %{public}@ " , message)
50+ addBreadcrumb ( message: message, category: category, level: . warning)
51+ }
52+
53+ /// Error level - logged locally, sent as Sentry breadcrumb AND captured as event
54+ static func error( _ message: String , category: Category = . app, error: Error ? = nil ) {
55+ os_log ( . fault, log: category. osLog, " ❌ %{public}@ " , message)
56+ addBreadcrumb ( message: message, category: category, level: . error)
57+
58+ // Also capture as Sentry event for errors
59+ if let error = error {
60+ SentrySDK . capture ( error: error) { scope in
61+ scope. setContext ( value: [ " message " : message] , key: " log_context " )
62+ }
63+ } else {
64+ SentrySDK . capture ( message: message) { scope in
65+ scope. setLevel ( . error)
66+ scope. setTag ( value: category. rawValue, key: " log_category " )
67+ }
68+ }
69+ }
70+
71+ /// Fatal level - for unrecoverable errors, always captured
72+ static func fatal( _ message: String , category: Category = . app, error: Error ? = nil ) {
73+ os_log ( . fault, log: category. osLog, " 💀 FATAL: %{public}@ " , message)
74+
75+ SentrySDK . capture ( message: " FATAL: \( message) " ) { scope in
76+ scope. setLevel ( . fatal)
77+ scope. setTag ( value: category. rawValue, key: " log_category " )
78+ if let error = error {
79+ scope. setContext ( value: [ " error " : error. localizedDescription] , key: " error_info " )
80+ }
81+ }
82+ }
83+
84+ private static func addBreadcrumb( message: String , category: Category , level: SentryLevel ) {
85+ let breadcrumb = Breadcrumb ( )
86+ breadcrumb. category = category. rawValue
87+ breadcrumb. message = message
88+ breadcrumb. level = level
89+ breadcrumb. timestamp = Date ( )
90+ SentrySDK . addBreadcrumb ( breadcrumb)
91+ }
92+ }
493
594// MARK: - Analytics Manager
695/// Centralized analytics for MiddleDrag using Sentry (crash reporting) and Simple Analytics (usage)
@@ -55,7 +144,7 @@ final class AnalyticsManager {
55144 trackEvent ( . appLaunched)
56145
57146 #if DEBUG
58- print ( " [Analytics] Initialized (Sentry + Simple Analytics)" )
147+ Log . debug ( " Initialized (Sentry + Simple Analytics) " , category : . analytics )
59148 #endif
60149 }
61150
@@ -69,7 +158,7 @@ final class AnalyticsManager {
69158 // Skip if DSN not configured
70159 guard isSentryConfigured else {
71160 #if DEBUG
72- print ( " [Analytics] Sentry DSN not configured - skipping initialization" )
161+ Log . debug ( " Sentry DSN not configured - skipping initialization " , category : . analytics )
73162 #endif
74163 return
75164 }
@@ -146,7 +235,7 @@ final class AnalyticsManager {
146235 URLSession . shared. dataTask ( with: request) { _, _, error in
147236 #if DEBUG
148237 if let error = error {
149- print ( " [Analytics] Simple Analytics error: \( error. localizedDescription) " )
238+ Log . warning ( " Simple Analytics error: \( error. localizedDescription) " , category : . analytics )
150239 }
151240 #endif
152241 } . resume ( )
@@ -172,7 +261,7 @@ final class AnalyticsManager {
172261 sendSimpleAnalyticsEvent ( path: event. category, event: event. rawValue)
173262
174263 #if DEBUG
175- print ( " [Analytics] Event: \( event. rawValue) \( parameters) " )
264+ Log . debug ( " Event: \( event. rawValue) \( parameters) " , category : . analytics )
176265 #endif
177266 }
178267
@@ -199,7 +288,7 @@ final class AnalyticsManager {
199288 }
200289
201290 #if DEBUG
202- print ( " [Analytics] Error: \( error. localizedDescription) " )
291+ Log . debug ( " Error tracked : \( error. localizedDescription) " , category : . analytics )
203292 #endif
204293 }
205294
0 commit comments