From eb2398da2a58e96da5107de649ce915b8b0e35ab Mon Sep 17 00:00:00 2001 From: NullPointerDepressiveDisorder <96403086+NullPointerDepressiveDisorder@users.noreply.github.com> Date: Wed, 3 Dec 2025 19:36:28 -0800 Subject: [PATCH 1/8] feat(logging): Add unified Log system with Sentry breadcrumbs - Add Log enum that writes to both os_log and Sentry - Support for debug/info/warning/error/fatal levels - Categories: app, device, gesture, analytics - Debug logs only appear in debug builds - Info/warning add Sentry breadcrumbs for crash context - Error/fatal also capture as Sentry events - Replace print() statements with Log calls in AnalyticsManager --- MiddleDrag/Utilities/AnalyticsManager.swift | 99 +++++++++++++++++++-- 1 file changed, 94 insertions(+), 5 deletions(-) diff --git a/MiddleDrag/Utilities/AnalyticsManager.swift b/MiddleDrag/Utilities/AnalyticsManager.swift index 8b23db0..8578b5b 100644 --- a/MiddleDrag/Utilities/AnalyticsManager.swift +++ b/MiddleDrag/Utilities/AnalyticsManager.swift @@ -1,6 +1,95 @@ import Foundation import Cocoa import Sentry +import os.log + +// MARK: - Sentry Logger +/// A unified logger that writes to both os_log and Sentry breadcrumbs +/// Usage: Log.debug("message"), Log.info("message"), Log.warning("message"), Log.error("message") +enum Log { + private static let subsystem = Bundle.main.bundleIdentifier ?? "com.middledrag" + + // OS Log categories + private static let gestureLog = OSLog(subsystem: subsystem, category: "gesture") + private static let deviceLog = OSLog(subsystem: subsystem, category: "device") + private static let analyticsLog = OSLog(subsystem: subsystem, category: "analytics") + private static let appLog = OSLog(subsystem: subsystem, category: "app") + + enum Category: String { + case gesture + case device + case analytics + case app + + var osLog: OSLog { + switch self { + case .gesture: return Log.gestureLog + case .device: return Log.deviceLog + case .analytics: return Log.analyticsLog + case .app: return Log.appLog + } + } + } + + /// Debug level - only in debug builds, not sent to Sentry + static func debug(_ message: String, category: Category = .app) { + #if DEBUG + os_log(.debug, log: category.osLog, "%{public}@", message) + #endif + } + + /// Info level - logged locally and as Sentry breadcrumb + static func info(_ message: String, category: Category = .app) { + os_log(.info, log: category.osLog, "%{public}@", message) + addBreadcrumb(message: message, category: category, level: .info) + } + + /// Warning level - logged locally and as Sentry breadcrumb + static func warning(_ message: String, category: Category = .app) { + os_log(.error, log: category.osLog, "⚠️ %{public}@", message) + addBreadcrumb(message: message, category: category, level: .warning) + } + + /// Error level - logged locally, sent as Sentry breadcrumb AND captured as event + static func error(_ message: String, category: Category = .app, error: Error? = nil) { + os_log(.fault, log: category.osLog, "❌ %{public}@", message) + addBreadcrumb(message: message, category: category, level: .error) + + // Also capture as Sentry event for errors + if let error = error { + SentrySDK.capture(error: error) { scope in + scope.setContext(value: ["message": message], key: "log_context") + } + } else { + SentrySDK.capture(message: message) { scope in + scope.setLevel(.error) + scope.setTag(value: category.rawValue, key: "log_category") + } + } + } + + /// Fatal level - for unrecoverable errors, always captured + static func fatal(_ message: String, category: Category = .app, error: Error? = nil) { + os_log(.fault, log: category.osLog, "💀 FATAL: %{public}@", message) + + SentrySDK.capture(message: "FATAL: \(message)") { scope in + scope.setLevel(.fatal) + scope.setTag(value: category.rawValue, key: "log_category") + if let error = error { + scope.setContext(value: ["error": error.localizedDescription], key: "error_info") + } + } + } + + private static func addBreadcrumb(message: String, category: Category, level: SentryLevel) { + let breadcrumb = Breadcrumb() + breadcrumb.category = category.rawValue + breadcrumb.message = message + breadcrumb.level = level + breadcrumb.timestamp = Date() + SentrySDK.addBreadcrumb(breadcrumb) + } +} // MARK: - Analytics Manager /// Centralized analytics for MiddleDrag using Sentry (crash reporting) and Simple Analytics (usage) @@ -55,7 +144,7 @@ final class AnalyticsManager { trackEvent(.appLaunched) #if DEBUG - print("[Analytics] Initialized (Sentry + Simple Analytics)") + Log.debug("Initialized (Sentry + Simple Analytics)", category: .analytics) #endif } @@ -69,7 +158,7 @@ final class AnalyticsManager { // Skip if DSN not configured guard isSentryConfigured else { #if DEBUG - print("[Analytics] Sentry DSN not configured - skipping initialization") + Log.debug("Sentry DSN not configured - skipping initialization", category: .analytics) #endif return } @@ -146,7 +235,7 @@ final class AnalyticsManager { URLSession.shared.dataTask(with: request) { _, _, error in #if DEBUG if let error = error { - print("[Analytics] Simple Analytics error: \(error.localizedDescription)") + Log.warning("Simple Analytics error: \(error.localizedDescription)", category: .analytics) } #endif }.resume() @@ -172,7 +261,7 @@ final class AnalyticsManager { sendSimpleAnalyticsEvent(path: event.category, event: event.rawValue) #if DEBUG - print("[Analytics] Event: \(event.rawValue) \(parameters)") + Log.debug("Event: \(event.rawValue) \(parameters)", category: .analytics) #endif } @@ -199,7 +288,7 @@ final class AnalyticsManager { } #if DEBUG - print("[Analytics] Error: \(error.localizedDescription)") + Log.debug("Error tracked: \(error.localizedDescription)", category: .analytics) #endif } From d35092933bbaecff685786492c896497923024f0 Mon Sep 17 00:00:00 2001 From: NullPointerDepressiveDisorder <96403086+NullPointerDepressiveDisorder@users.noreply.github.com> Date: Wed, 3 Dec 2025 19:36:49 -0800 Subject: [PATCH 2/8] refactor(device): Replace logToFile with unified Log system - Remove custom file-based logging implementation - Use Log.debug for touch callbacks (device category) - Use Log.info for device start/stop lifecycle - Use Log.warning when device list is nil - Use Log.error when no multitouch device found - Reduce log frequency (every 500 touches vs 100) --- MiddleDrag/Managers/DeviceMonitor.swift | 48 ++++++++++--------------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/MiddleDrag/Managers/DeviceMonitor.swift b/MiddleDrag/Managers/DeviceMonitor.swift index a84fcb6..b84e6c9 100644 --- a/MiddleDrag/Managers/DeviceMonitor.swift +++ b/MiddleDrag/Managers/DeviceMonitor.swift @@ -1,29 +1,10 @@ import Foundation import CoreFoundation -// MARK: - Debug Logging (Debug builds only) +// MARK: - Debug Touch Counter (Debug builds only) #if DEBUG -private let debugLogPath = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("middledrag_touch.log") private var touchCount = 0 - -private func logToFile(_ message: String) { - let timestamp = ISO8601DateFormatter().string(from: Date()) - let line = "[\(timestamp)] \(message)\n" - if let data = line.data(using: .utf8) { - if FileManager.default.fileExists(atPath: debugLogPath.path) { - if let handle = try? FileHandle(forWritingTo: debugLogPath) { - handle.seekToEndOfFile() - handle.write(data) - handle.closeFile() - } - } else { - try? data.write(to: debugLogPath) - } - } -} -#else -@inline(__always) private func logToFile(_ message: String) {} #endif // MARK: - Global Callback Storage @@ -36,8 +17,9 @@ private var gDeviceMonitor: DeviceMonitor? private let deviceContactCallback: MTContactCallbackFunction = { device, touches, numTouches, timestamp, frame in #if DEBUG touchCount += 1 - if touchCount <= 5 || touchCount % 100 == 0 { - logToFile("Touch callback #\(touchCount): \(numTouches) touches") + // Log sparingly to avoid performance impact + if touchCount <= 5 || touchCount % 500 == 0 { + Log.debug("Touch callback #\(touchCount): \(numTouches) touches", category: .device) } #endif @@ -85,20 +67,22 @@ class DeviceMonitor { func start() { guard !isRunning else { return } - logToFile("DeviceMonitor.start() called") + Log.info("DeviceMonitor starting...", category: .device) + + var deviceCount = 0 // Try to get all devices if let deviceList = MTDeviceCreateList() { let count = CFArrayGetCount(deviceList) - logToFile("Found \(count) multitouch device(s)") + Log.info("Found \(count) multitouch device(s)", category: .device) for i in 0.. Date: Wed, 3 Dec 2025 19:37:01 -0800 Subject: [PATCH 3/8] feat(app): Add lifecycle logging for crash debugging - Log app start, initialization steps, and termination - Log accessibility permission status - Log preferences loading and updates - Provides breadcrumb trail for crash reports --- MiddleDrag/AppDelegate.swift | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/MiddleDrag/AppDelegate.swift b/MiddleDrag/AppDelegate.swift index e402537..4e96bfd 100644 --- a/MiddleDrag/AppDelegate.swift +++ b/MiddleDrag/AppDelegate.swift @@ -12,9 +12,11 @@ class AppDelegate: NSObject, NSApplicationDelegate { // MARK: - Application Lifecycle func applicationDidFinishLaunching(_ notification: Notification) { - //Initialize Analytics + // Initialize Analytics first (sets up Sentry for crash reporting) AnalyticsManager.shared.initialize() + Log.info("MiddleDrag starting...", category: .app) + // Hide dock icon (menu bar app only) NSApp.setActivationPolicy(.accessory) @@ -33,16 +35,19 @@ class AppDelegate: NSObject, NSApplicationDelegate { // Load preferences preferences = PreferencesManager.shared.loadPreferences() + Log.info("Preferences loaded", category: .app) // Configure and start multitouch manager multitouchManager.updateConfiguration(preferences.gestureConfig) multitouchManager.start() + Log.info("Multitouch manager started", category: .app) // Set up menu bar UI menuBarController = MenuBarController( multitouchManager: multitouchManager, preferences: preferences ) + Log.info("Menu bar controller initialized", category: .app) // Set up notification observers setupNotifications() @@ -55,14 +60,18 @@ class AppDelegate: NSObject, NSApplicationDelegate { // Check Accessibility permission AFTER UI is set up // This way the menu bar icon appears even if permission is missing if !AXIsProcessTrusted() { + Log.warning("Accessibility permission not granted", category: .app) AnalyticsManager.shared.trackAccessibilityPermission(granted: false) showAccessibilityAlert() } else { + Log.info("Accessibility permission granted", category: .app) AnalyticsManager.shared.trackAccessibilityPermission(granted: true) } // Final cleanup of any stray windows closeAllWindows() + + Log.info("MiddleDrag initialization complete", category: .app) } private func showAccessibilityAlert() { @@ -103,7 +112,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { } func applicationWillTerminate(_ notification: Notification) { - //Track app termination + Log.info("MiddleDrag terminating", category: .app) + + // Track app termination AnalyticsManager.shared.trackTermination() multitouchManager.stop() @@ -142,6 +153,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { preferences = newPreferences PreferencesManager.shared.savePreferences(preferences) multitouchManager.updateConfiguration(preferences.gestureConfig) + Log.info("Preferences updated", category: .app) } } From d8f95a453f839e373ad52e2f2788e7412766ae64 Mon Sep 17 00:00:00 2001 From: NullPointerDepressiveDisorder <96403086+NullPointerDepressiveDisorder@users.noreply.github.com> Date: Wed, 3 Dec 2025 19:37:11 -0800 Subject: [PATCH 4/8] refactor(login): Replace print with Log in LaunchAtLoginManager - Log success/failure of launch at login configuration - Use Log.error with error context for failures - Log warning for unsupported macOS versions --- MiddleDrag/Utilities/LaunchAtLoginManager.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/MiddleDrag/Utilities/LaunchAtLoginManager.swift b/MiddleDrag/Utilities/LaunchAtLoginManager.swift index 3513413..f75c6c0 100644 --- a/MiddleDrag/Utilities/LaunchAtLoginManager.swift +++ b/MiddleDrag/Utilities/LaunchAtLoginManager.swift @@ -22,19 +22,20 @@ class LaunchAtLoginManager { do { if enabled { try SMAppService.mainApp.register() + Log.info("Launch at login enabled", category: .app) } else { try SMAppService.mainApp.unregister() + Log.info("Launch at login disabled", category: .app) } } catch { - print("Failed to configure launch at login: \(error)") + Log.error("Failed to configure launch at login: \(error.localizedDescription)", category: .app, error: error) } } private func configureLaunchAtLoginLegacy(_ enabled: Bool) { // For older macOS versions, we would use LSSharedFileList // or SMLoginItemSetEnabled, but these are deprecated - // For simplicity, we'll just log a message - print("Launch at login configuration not available on macOS < 13.0") + Log.warning("Launch at login not available on macOS < 13.0", category: .app) } /// Check if launch at login is enabled From 9fcad3dea7862ef4a2e470289a69e118d49433b0 Mon Sep 17 00:00:00 2001 From: NullPointerDepressiveDisorder <96403086+NullPointerDepressiveDisorder@users.noreply.github.com> Date: Wed, 3 Dec 2025 19:37:29 -0800 Subject: [PATCH 5/8] refactor(multitouch): Use Log.warning for event tap failures --- MiddleDrag/Managers/MultitouchManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiddleDrag/Managers/MultitouchManager.swift b/MiddleDrag/Managers/MultitouchManager.swift index 6591e62..ef139e1 100644 --- a/MiddleDrag/Managers/MultitouchManager.swift +++ b/MiddleDrag/Managers/MultitouchManager.swift @@ -123,7 +123,7 @@ class MultitouchManager { }, userInfo: refcon ) else { - print("⚠️ Could not create event tap") + Log.warning("Could not create event tap", category: .device) return } From 2b6c26783525319bfbb717379a691ba8b3a0da07 Mon Sep 17 00:00:00 2001 From: Karan Mohindroo <96403086+NullPointerDepressiveDisorder@users.noreply.github.com> Date: Wed, 3 Dec 2025 20:01:08 -0800 Subject: [PATCH 6/8] Update MiddleDrag/Utilities/AnalyticsManager.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Karan Mohindroo <96403086+NullPointerDepressiveDisorder@users.noreply.github.com> --- MiddleDrag/Utilities/AnalyticsManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiddleDrag/Utilities/AnalyticsManager.swift b/MiddleDrag/Utilities/AnalyticsManager.swift index 8578b5b..d186e26 100644 --- a/MiddleDrag/Utilities/AnalyticsManager.swift +++ b/MiddleDrag/Utilities/AnalyticsManager.swift @@ -5,7 +5,7 @@ import os.log // MARK: - Sentry Logger /// A unified logger that writes to both os_log and Sentry breadcrumbs -/// Usage: Log.debug("message"), Log.info("message"), Log.warning("message"), Log.error("message") +/// Usage: Log.debug("message"), Log.info("message"), Log.warning("message"), Log.error("message"), Log.fatal("message") enum Log { private static let subsystem = Bundle.main.bundleIdentifier ?? "com.middledrag" From 3b5257f17081cae2c0ea7d1dc112483f9465659b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 04:02:06 +0000 Subject: [PATCH 7/8] Initial plan From cce3a91434589f7b716ed3fa002af2925f521e96 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 04:05:13 +0000 Subject: [PATCH 8/8] fix(device): Prevent double registration of default multitouch device Co-authored-by: NullPointerDepressiveDisorder <96403086+NullPointerDepressiveDisorder@users.noreply.github.com> --- MiddleDrag/Managers/DeviceMonitor.swift | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/MiddleDrag/Managers/DeviceMonitor.swift b/MiddleDrag/Managers/DeviceMonitor.swift index b84e6c9..b50a5ae 100644 --- a/MiddleDrag/Managers/DeviceMonitor.swift +++ b/MiddleDrag/Managers/DeviceMonitor.swift @@ -70,6 +70,7 @@ class DeviceMonitor { Log.info("DeviceMonitor starting...", category: .device) var deviceCount = 0 + var registeredDevices: Set = [] // Try to get all devices if let deviceList = MTDeviceCreateList() { @@ -82,6 +83,7 @@ class DeviceMonitor { let deviceRef = UnsafeMutableRawPointer(mutating: dev) MTRegisterContactFrameCallback(deviceRef, deviceContactCallback) MTDeviceStart(deviceRef, 0) + registeredDevices.insert(deviceRef) deviceCount += 1 if device == nil { @@ -93,14 +95,19 @@ class DeviceMonitor { Log.warning("MTDeviceCreateList returned nil, trying default device", category: .device) } - // Also try the default device + // Also try the default device if not already registered if let defaultDevice = MultitouchFramework.shared.getDefaultDevice() { - MTRegisterContactFrameCallback(defaultDevice, deviceContactCallback) - MTDeviceStart(defaultDevice, 0) - deviceCount += 1 - - if device == nil { - device = defaultDevice + if !registeredDevices.contains(defaultDevice) { + MTRegisterContactFrameCallback(defaultDevice, deviceContactCallback) + MTDeviceStart(defaultDevice, 0) + registeredDevices.insert(defaultDevice) + deviceCount += 1 + + if device == nil { + device = defaultDevice + } + } else { + Log.debug("Default device already registered from device list", category: .device) } }