Skip to content

Commit 27c24df

Browse files
Merge pull request #4 from NullPointerDepressiveDisorder/sentry
Sentry
2 parents 8f642ef + 6e30896 commit 27c24df

5 files changed

Lines changed: 145 additions & 46 deletions

File tree

MiddleDrag/AppDelegate.swift

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
1212
// MARK: - Application Lifecycle
1313

1414
func applicationDidFinishLaunching(_ notification: Notification) {
15-
//Initialize Analytics
15+
// Initialize Analytics first (sets up Sentry for crash reporting)
1616
AnalyticsManager.shared.initialize()
1717

18+
Log.info("MiddleDrag starting...", category: .app)
19+
1820
// Hide dock icon (menu bar app only)
1921
NSApp.setActivationPolicy(.accessory)
2022

@@ -33,16 +35,19 @@ class AppDelegate: NSObject, NSApplicationDelegate {
3335

3436
// Load preferences
3537
preferences = PreferencesManager.shared.loadPreferences()
38+
Log.info("Preferences loaded", category: .app)
3639

3740
// Configure and start multitouch manager
3841
multitouchManager.updateConfiguration(preferences.gestureConfig)
3942
multitouchManager.start()
43+
Log.info("Multitouch manager started", category: .app)
4044

4145
// Set up menu bar UI
4246
menuBarController = MenuBarController(
4347
multitouchManager: multitouchManager,
4448
preferences: preferences
4549
)
50+
Log.info("Menu bar controller initialized", category: .app)
4651

4752
// Set up notification observers
4853
setupNotifications()
@@ -55,14 +60,18 @@ class AppDelegate: NSObject, NSApplicationDelegate {
5560
// Check Accessibility permission AFTER UI is set up
5661
// This way the menu bar icon appears even if permission is missing
5762
if !AXIsProcessTrusted() {
63+
Log.warning("Accessibility permission not granted", category: .app)
5864
AnalyticsManager.shared.trackAccessibilityPermission(granted: false)
5965
showAccessibilityAlert()
6066
} else {
67+
Log.info("Accessibility permission granted", category: .app)
6168
AnalyticsManager.shared.trackAccessibilityPermission(granted: true)
6269
}
6370

6471
// Final cleanup of any stray windows
6572
closeAllWindows()
73+
74+
Log.info("MiddleDrag initialization complete", category: .app)
6675
}
6776

6877
private func showAccessibilityAlert() {
@@ -103,7 +112,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
103112
}
104113

105114
func applicationWillTerminate(_ notification: Notification) {
106-
//Track app termination
115+
Log.info("MiddleDrag terminating", category: .app)
116+
117+
// Track app termination
107118
AnalyticsManager.shared.trackTermination()
108119

109120
multitouchManager.stop()
@@ -142,6 +153,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
142153
preferences = newPreferences
143154
PreferencesManager.shared.savePreferences(preferences)
144155
multitouchManager.updateConfiguration(preferences.gestureConfig)
156+
Log.info("Preferences updated", category: .app)
145157
}
146158
}
147159

MiddleDrag/Managers/DeviceMonitor.swift

Lines changed: 32 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,10 @@
11
import Foundation
22
import CoreFoundation
33

4-
// MARK: - Debug Logging (Debug builds only)
4+
// MARK: - Debug Touch Counter (Debug builds only)
55

66
#if DEBUG
7-
private let debugLogPath = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("middledrag_touch.log")
87
private var touchCount = 0
9-
10-
private func logToFile(_ message: String) {
11-
let timestamp = ISO8601DateFormatter().string(from: Date())
12-
let line = "[\(timestamp)] \(message)\n"
13-
if let data = line.data(using: .utf8) {
14-
if FileManager.default.fileExists(atPath: debugLogPath.path) {
15-
if let handle = try? FileHandle(forWritingTo: debugLogPath) {
16-
handle.seekToEndOfFile()
17-
handle.write(data)
18-
handle.closeFile()
19-
}
20-
} else {
21-
try? data.write(to: debugLogPath)
22-
}
23-
}
24-
}
25-
#else
26-
@inline(__always) private func logToFile(_ message: String) {}
278
#endif
289

2910
// MARK: - Global Callback Storage
@@ -36,8 +17,9 @@ private var gDeviceMonitor: DeviceMonitor?
3617
private let deviceContactCallback: MTContactCallbackFunction = { device, touches, numTouches, timestamp, frame in
3718
#if DEBUG
3819
touchCount += 1
39-
if touchCount <= 5 || touchCount % 100 == 0 {
40-
logToFile("Touch callback #\(touchCount): \(numTouches) touches")
20+
// Log sparingly to avoid performance impact
21+
if touchCount <= 5 || touchCount % 500 == 0 {
22+
Log.debug("Touch callback #\(touchCount): \(numTouches) touches", category: .device)
4123
}
4224
#endif
4325

@@ -85,44 +67,57 @@ class DeviceMonitor {
8567
func start() {
8668
guard !isRunning else { return }
8769

88-
logToFile("DeviceMonitor.start() called")
70+
Log.info("DeviceMonitor starting...", category: .device)
71+
72+
var deviceCount = 0
73+
var registeredDevices: Set<UnsafeMutableRawPointer> = []
8974

9075
// Try to get all devices
9176
if let deviceList = MTDeviceCreateList() {
9277
let count = CFArrayGetCount(deviceList)
93-
logToFile("Found \(count) multitouch device(s)")
78+
Log.info("Found \(count) multitouch device(s)", category: .device)
9479

9580
for i in 0..<count {
9681
let devicePtr = CFArrayGetValueAtIndex(deviceList, i)
9782
if let dev = devicePtr {
9883
let deviceRef = UnsafeMutableRawPointer(mutating: dev)
99-
logToFile("Registering callback for device \(i): \(deviceRef)")
10084
MTRegisterContactFrameCallback(deviceRef, deviceContactCallback)
10185
MTDeviceStart(deviceRef, 0)
86+
registeredDevices.insert(deviceRef)
87+
deviceCount += 1
10288

10389
if device == nil {
10490
device = deviceRef
10591
}
10692
}
10793
}
10894
} else {
109-
logToFile("MTDeviceCreateList returned nil, trying default")
95+
Log.warning("MTDeviceCreateList returned nil, trying default device", category: .device)
11096
}
11197

112-
// Also try the default device
98+
// Also try the default device if not already registered
11399
if let defaultDevice = MultitouchFramework.shared.getDefaultDevice() {
114-
logToFile("Also registering default device: \(defaultDevice)")
115-
MTRegisterContactFrameCallback(defaultDevice, deviceContactCallback)
116-
MTDeviceStart(defaultDevice, 0)
117-
118-
if device == nil {
119-
device = defaultDevice
100+
if !registeredDevices.contains(defaultDevice) {
101+
MTRegisterContactFrameCallback(defaultDevice, deviceContactCallback)
102+
MTDeviceStart(defaultDevice, 0)
103+
registeredDevices.insert(defaultDevice)
104+
deviceCount += 1
105+
106+
if device == nil {
107+
device = defaultDevice
108+
}
109+
} else {
110+
Log.debug("Default device already registered from device list", category: .device)
120111
}
121112
}
122113

123-
logToFile("Device registration complete")
114+
if device == nil {
115+
Log.error("No multitouch device found!", category: .device)
116+
} else {
117+
Log.info("DeviceMonitor started with \(deviceCount) device(s)", category: .device)
118+
}
119+
124120
isRunning = true
125-
logToFile("DeviceMonitor started successfully")
126121
}
127122

128123
/// Stop monitoring
@@ -134,6 +129,8 @@ class DeviceMonitor {
134129

135130
self.device = nil
136131
isRunning = false
132+
133+
Log.info("DeviceMonitor stopped", category: .device)
137134
}
138135

139136
// MARK: - Internal

MiddleDrag/Managers/MultitouchManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ class MultitouchManager {
123123
},
124124
userInfo: refcon
125125
) else {
126-
print("⚠️ Could not create event tap")
126+
Log.warning("Could not create event tap", category: .device)
127127
return
128128
}
129129

MiddleDrag/Utilities/AnalyticsManager.swift

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,95 @@
11
import Foundation
22
import Cocoa
33
import 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

MiddleDrag/Utilities/LaunchAtLoginManager.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,20 @@ class LaunchAtLoginManager {
2222
do {
2323
if enabled {
2424
try SMAppService.mainApp.register()
25+
Log.info("Launch at login enabled", category: .app)
2526
} else {
2627
try SMAppService.mainApp.unregister()
28+
Log.info("Launch at login disabled", category: .app)
2729
}
2830
} catch {
29-
print("Failed to configure launch at login: \(error)")
31+
Log.error("Failed to configure launch at login: \(error.localizedDescription)", category: .app, error: error)
3032
}
3133
}
3234

3335
private func configureLaunchAtLoginLegacy(_ enabled: Bool) {
3436
// For older macOS versions, we would use LSSharedFileList
3537
// or SMLoginItemSetEnabled, but these are deprecated
36-
// For simplicity, we'll just log a message
37-
print("Launch at login configuration not available on macOS < 13.0")
38+
Log.warning("Launch at login not available on macOS < 13.0", category: .app)
3839
}
3940

4041
/// Check if launch at login is enabled

0 commit comments

Comments
 (0)