Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 8 additions & 73 deletions desktop/Desktop/Sources/AnalyticsManager.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import AppKit
import Foundation
import Sentry

/// Unified analytics manager that sends events to PostHog.
/// Use this instead of calling PostHogManager directly
Expand Down Expand Up @@ -259,13 +260,18 @@ class AnalyticsManager {
databaseInitFailed: Bool
) {
guard !Self.isDevBuild else { return }
let properties: [String: Any] = [
// Routed to Sentry as a breadcrumb (perf telemetry, not product analytics) so the data
// is attached to any same-session crash report without creating a per-launch analytics
// event. If we ever need real perf metrics, wire up SentrySDK.startTransaction here.
let breadcrumb = Breadcrumb(level: .info, category: "app.startup")
breadcrumb.message = "App Startup Timing"
breadcrumb.data = [
"db_init_ms": round(dbInitMs),
"time_to_interactive_ms": round(timeToInteractiveMs),
"had_unclean_shutdown": hadUncleanShutdown,
"database_init_failed": databaseInitFailed,
]
PostHogManager.shared.track("App Startup Timing", properties: properties)
SentrySDK.addBreadcrumb(breadcrumb)
}

/// Track first launch with comprehensive system diagnostics
Expand Down Expand Up @@ -356,14 +362,6 @@ class AnalyticsManager {
return diagnostics
}

func appBecameActive() {
PostHogManager.shared.appBecameActive()
}

func appResignedActive() {
PostHogManager.shared.appResignedActive()
}

// MARK: - Conversation Events
// Note: The event is named "Memory Created" in analytics for historical reasons,
// but it actually tracks when a conversation/recording is created, not a "memory".
Expand Down Expand Up @@ -545,11 +543,6 @@ class AnalyticsManager {

// MARK: - Launch At Login Events

/// Track launch at login status once per app launch (not continuously)
func launchAtLoginStatusChecked(enabled: Bool) {
PostHogManager.shared.launchAtLoginStatusChecked(enabled: enabled)
}

/// Track when launch at login state changes
/// - Parameters:
/// - enabled: New state
Expand Down Expand Up @@ -636,10 +629,6 @@ class AnalyticsManager {

// MARK: - Update Events

func updateCheckStarted() {
PostHogManager.shared.updateCheckStarted()
}

func updateAvailable(version: String) {
PostHogManager.shared.updateAvailable(version: version)
}
Expand All @@ -648,20 +637,6 @@ class AnalyticsManager {
PostHogManager.shared.updateInstalled(version: version)
}

func updateNotFound() {
PostHogManager.shared.updateNotFound()
}

func updateCheckFailed(
error: String, errorDomain: String, errorCode: Int, underlyingError: String? = nil,
underlyingDomain: String? = nil, underlyingCode: Int? = nil
) {
PostHogManager.shared.updateCheckFailed(
error: error, errorDomain: errorDomain, errorCode: errorCode,
underlyingError: underlyingError, underlyingDomain: underlyingDomain,
underlyingCode: underlyingCode)
}

// MARK: - Notification Events

func notificationSent(notificationId: String, title: String, assistantId: String, surface: String) {
Expand Down Expand Up @@ -709,18 +684,6 @@ class AnalyticsManager {
PostHogManager.shared.chatBridgeModeChanged(from: oldMode, to: newMode)
}

// MARK: - Settings State

/// Track the current state of key settings (screenshots, memory extraction, notifications)
/// Called when monitoring starts and daily while monitoring is active
func trackSettingsState(
screenshotsEnabled: Bool, memoryExtractionEnabled: Bool, memoryNotificationsEnabled: Bool
) {
PostHogManager.shared.settingsStateTracked(
screenshotsEnabled: screenshotsEnabled, memoryExtractionEnabled: memoryExtractionEnabled,
memoryNotificationsEnabled: memoryNotificationsEnabled)
}

// MARK: - All Settings State (Comprehensive daily report)

private let lastAllSettingsReportKey = "lastAllSettingsReportDate"
Expand Down Expand Up @@ -927,32 +890,4 @@ class AnalyticsManager {
PostHogManager.shared.track("knowledge_graph_build_failed", properties: props)
}

// MARK: - Display Info

/// Track display characteristics (notch, screen size, etc.)
/// Called at app launch to help diagnose menu bar visibility issues
func trackDisplayInfo() {
guard let screen = NSScreen.main else { return }

let frame = screen.frame
let visibleFrame = screen.visibleFrame
let safeAreaInsets = screen.safeAreaInsets

// Detect notch: MacBooks with notch have safeAreaInsets.top > 0
let hasNotch = safeAreaInsets.top > 0

// Calculate menu bar height (difference between frame and visible frame at top)
let menuBarHeight = frame.height - visibleFrame.height - visibleFrame.origin.y

let displayInfo: [String: Any] = [
"screen_width": Int(frame.width),
"screen_height": Int(frame.height),
"has_notch": hasNotch,
"safe_area_top": Int(safeAreaInsets.top),
"menu_bar_height": Int(menuBarHeight),
"scale_factor": screen.backingScaleFactor,
]

PostHogManager.shared.displayInfoTracked(info: displayInfo)
}
}
12 changes: 0 additions & 12 deletions desktop/Desktop/Sources/OmiApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
AnalyticsManager.shared.initialize()
AnalyticsManager.shared.detectAndReportCrash()
AnalyticsManager.shared.appLaunched()
AnalyticsManager.shared.trackDisplayInfo()

// Tier gating: migrate old boolean key to new 6-tier system
TierManager.migrateExistingUsersIfNeeded()
Expand Down Expand Up @@ -433,12 +432,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
self?.updateOnboardingLifecyclePolicy(reason: "user_defaults_changed")
}

// Track launch at login status once per app launch
Task { @MainActor in
let isEnabled = LaunchAtLoginManager.shared.isEnabled
AnalyticsManager.shared.launchAtLoginStatusChecked(enabled: isEnabled)
}

// Register for Apple Events to handle URL scheme
NSAppleEventManager.shared().setEventHandler(
self,
Expand Down Expand Up @@ -1251,12 +1244,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
}

func applicationDidBecomeActive(_ notification: Notification) {
AnalyticsManager.shared.appBecameActive()
// Sync remote assistant settings so server-side changes take effect promptly
Task { await SettingsSyncManager.shared.syncFromServer() }
}

func applicationWillResignActive(_ notification: Notification) {
AnalyticsManager.shared.appResignedActive()
}
}
52 changes: 2 additions & 50 deletions desktop/Desktop/Sources/PostHogManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ class PostHogManager {

// Disable automatic lifecycle events — PostHog's observer calls setResourceValues(isExcludedFromBackupKey:)
// synchronously on the main thread (via NSApplicationDidFinishLaunchingNotification), which XPCs to the
// mds (Spotlight) daemon and can hang for 2000ms+ when the daemon is slow. We already track lifecycle
// events manually via AnalyticsManager.shared.appLaunched() / appBecameActive() etc.
// mds (Spotlight) daemon and can hang for 2000ms+ when the daemon is slow. We track the meaningful
// lifecycle event (App Launched / First Launch) manually via AnalyticsManager.shared.
config.captureApplicationLifecycleEvents = false
config.captureScreenViews = true
config.preloadFeatureFlags = true
Expand Down Expand Up @@ -331,14 +331,6 @@ extension PostHogManager {
track("First Launch", properties: diagnostics)
}

func appBecameActive() {
track("App Became Active")
}

func appResignedActive() {
track("App Resigned Active")
}

// MARK: - Page/Screen Views (PostHog specific)

func pageViewed(_ pageName: String) {
Expand Down Expand Up @@ -475,12 +467,6 @@ extension PostHogManager {

// MARK: - Launch At Login Events

func launchAtLoginStatusChecked(enabled: Bool) {
track("Launch At Login Status", properties: [
"enabled": enabled
])
}

func launchAtLoginChanged(enabled: Bool, source: String) {
track("Launch At Login Changed", properties: [
"enabled": enabled,
Expand Down Expand Up @@ -607,10 +593,6 @@ extension PostHogManager {

// MARK: - Update Events

func updateCheckStarted() {
track("Update Check Started")
}

func updateAvailable(version: String) {
track("Update Available", properties: [
"version": version
Expand All @@ -623,22 +605,6 @@ extension PostHogManager {
])
}

func updateNotFound() {
track("Update Not Found")
}

func updateCheckFailed(error: String, errorDomain: String, errorCode: Int, underlyingError: String? = nil, underlyingDomain: String? = nil, underlyingCode: Int? = nil) {
var props: [String: Any] = [
"error": error,
"error_domain": errorDomain,
"error_code": errorCode
]
if let underlyingError { props["underlying_error"] = underlyingError }
if let underlyingDomain { props["underlying_domain"] = underlyingDomain }
if let underlyingCode { props["underlying_code"] = underlyingCode }
track("Update Check Failed", properties: props)
}

// MARK: - Notification Events

func notificationSent(notificationId: String, title: String, assistantId: String, surface: String) {
Expand Down Expand Up @@ -709,22 +675,8 @@ extension PostHogManager {

// MARK: - Settings State

func settingsStateTracked(screenshotsEnabled: Bool, memoryExtractionEnabled: Bool, memoryNotificationsEnabled: Bool) {
track("Settings State", properties: [
"screenshots_enabled": screenshotsEnabled,
"memory_extraction_enabled": memoryExtractionEnabled,
"memory_notifications_enabled": memoryNotificationsEnabled
])
}

/// Comprehensive all-settings snapshot (fired on app launch, at most once per day)
func allSettingsStateTracked(properties: [String: Any]) {
track("All Settings State", properties: properties)
}

// MARK: - Display Info

func displayInfoTracked(info: [String: Any]) {
track("Display Info", properties: info)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,6 @@ public class ProactiveAssistantsPlugin: NSObject {
private var wasMonitoringBeforeLock = false
private var systemEventObservers: [NSObjectProtocol] = []

// Daily settings state tracking
private var settingsStateTimer: Timer?

// Video call throttling: reduce capture frequency when a call app is frontmost
// to avoid competing with the call app for CPU/GPU (ScreenCaptureKit, encoding, OCR).
private var videoCallFrameCounter = 0
Expand Down Expand Up @@ -456,8 +453,6 @@ public class ProactiveAssistantsPlugin: NSObject {

sendEvent(type: "monitoringStarted", data: [:])
AnalyticsManager.shared.monitoringStarted()
trackSettingsState()
startSettingsStateTimer()
NotificationCenter.default.post(
name: .assistantMonitoringStateDidChange,
object: nil,
Expand All @@ -478,8 +473,6 @@ public class ProactiveAssistantsPlugin: NSObject {
analysisDelayTimer = nil
distributionDebounceTimer?.invalidate()
distributionDebounceTimer = nil
settingsStateTimer?.invalidate()
settingsStateTimer = nil
isInDelayPeriod = false
lastDistributedApp = nil
lastDistributedWindowTitle = nil
Expand Down Expand Up @@ -956,27 +949,6 @@ public class ProactiveAssistantsPlugin: NSObject {
AssistantCoordinator.shared.distributeFrame(frame)
}

// MARK: - Settings State Tracking

/// Track current settings state to analytics
private func trackSettingsState() {
AnalyticsManager.shared.trackSettingsState(
screenshotsEnabled: isMonitoring,
memoryExtractionEnabled: MemoryAssistantSettings.shared.isEnabled,
memoryNotificationsEnabled: MemoryAssistantSettings.shared.notificationsEnabled
)
}

/// Start a daily timer to report settings state
private func startSettingsStateTimer() {
settingsStateTimer?.invalidate()
settingsStateTimer = Timer.scheduledTimer(withTimeInterval: 86400, repeats: true) { [weak self] _ in
Task { @MainActor in
self?.trackSettingsState()
}
}
}

// MARK: - Event Broadcasting

private func sendEvent(type: String, data: [String: Any]) {
Expand Down
27 changes: 0 additions & 27 deletions desktop/Desktop/Sources/UpdaterViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,6 @@ final class UpdaterDelegate: NSObject, SPUUpdaterDelegate {
/// Called when Sparkle is about to check for updates (permission gate)
func updater(_ updater: SPUUpdater, mayPerform check: SPUUpdateCheck) throws {
logSync("Sparkle: Starting update check")
Task { @MainActor in
AnalyticsManager.shared.updateCheckStarted()
}
}

/// Called when Sparkle finishes loading the appcast
Expand Down Expand Up @@ -85,7 +82,6 @@ final class UpdaterDelegate: NSObject, SPUUpdaterDelegate {
func updaterDidNotFindUpdate(_ updater: SPUUpdater) {
logSync("Sparkle: No update available")
Task { @MainActor in
AnalyticsManager.shared.updateNotFound()
self.viewModel?.updateAvailable = false
}
}
Expand All @@ -112,29 +108,6 @@ final class UpdaterDelegate: NSObject, SPUUpdaterDelegate {
for (key, value) in nsError.userInfo where key != NSUnderlyingErrorKey {
logSync("Sparkle: Error info [\(key)] = \(value)")
}
// Build diagnostic properties for analytics
let errorDomain = nsError.domain
let errorCode = nsError.code
var underlyingMessage: String? = nil
var underlyingDomain: String? = nil
var underlyingCode: Int? = nil

if let underlying = nsError.userInfo[NSUnderlyingErrorKey] as? NSError {
underlyingMessage = underlying.localizedDescription
underlyingDomain = underlying.domain
underlyingCode = underlying.code
}

Task { @MainActor in
AnalyticsManager.shared.updateCheckFailed(
error: message,
errorDomain: errorDomain,
errorCode: errorCode,
underlyingError: underlyingMessage,
underlyingDomain: underlyingDomain,
underlyingCode: underlyingCode
)
}

// SUInstallationError (4005): Sparkle's installer failed to launch.
// Don't open the browser — Sparkle will retry on next check cycle.
Expand Down
Loading