From 79100e0d1ad170c6cd3c814d7642ab58d1f6c549 Mon Sep 17 00:00:00 2001 From: Taylor Lineman Date: Wed, 26 Aug 2026 18:18:05 -0400 Subject: [PATCH 1/8] Switch to telemetry deck Signed-off-by: Taylor Lineman --- Config.local.example.xcconfig | 7 +-- Config.xcconfig | 9 ++-- Minna.xcodeproj/project.pbxproj | 36 ++++++++-------- .../xcshareddata/swiftpm/Package.resolved | 20 ++++----- Minna/Database/FrontendDatabase.swift | 2 +- Minna/Info.plist | 6 +-- Minna/IrisApp.swift | 11 ++--- Minna/TelemetryWrapper.swift | 43 +++++++++++++++---- Minna/Utility/BuildConfiguration.swift | 19 ++------ Minna/Views/Preview/FileChat.swift | 4 +- Minna/Views/Search/AskMinnaView.swift | 4 +- 11 files changed, 81 insertions(+), 80 deletions(-) diff --git a/Config.local.example.xcconfig b/Config.local.example.xcconfig index 2e8188f..1be22ad 100644 --- a/Config.local.example.xcconfig +++ b/Config.local.example.xcconfig @@ -21,8 +21,5 @@ DEVELOPMENT_TEAM = // e.g. abc123def456@o1234567.ingest.us.sentry.io/7654321 SENTRY_DSN = -// e.g. phc_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -POSTHOG_PROJECT_TOKEN = - -// e.g. us.i.posthog.com -POSTHOG_HOST = +// e.g. XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX +TELEMETRY_DECK_ID = diff --git a/Config.xcconfig b/Config.xcconfig index 488786f..b67fe0a 100644 --- a/Config.xcconfig +++ b/Config.xcconfig @@ -20,7 +20,7 @@ DEVELOPMENT_TEAM = // MARK: - Telemetry // -// Minna ships with crash reporting (Sentry) and privacy-focused product analytics (PostHog). Both are DISABLED when these values are blank, which is the default for all contributor builds — a source build reports nothing, anywhere, unless you deliberately opt in below. +// Minna ships with crash reporting (Sentry) and privacy-focused product analytics (TelemetryDeck). Both are DISABLED when these values are blank, which is the default for all contributor builds — a source build reports nothing, anywhere, unless you deliberately opt in below. // // NOTE: xcconfig treats "//" as the start of a comment, so URLs here are stored WITHOUT their "https://" scheme. Minna re-adds the scheme at runtime. Do not paste a full URL into these values. @@ -29,11 +29,8 @@ DEVELOPMENT_TEAM = // Store as: @.ingest.us.sentry.io/ SENTRY_DSN = -// PostHog project token (starts with "phc_"). -POSTHOG_PROJECT_TOKEN = - -// PostHog host with the leading "https://" removed, e.g. us.i.posthog.com -POSTHOG_HOST = +// TelemetryDeck project token (starts with "XXXX-"). +TELEMETRY_DECK_ID = // MARK: - Local overrides // diff --git a/Minna.xcodeproj/project.pbxproj b/Minna.xcodeproj/project.pbxproj index 0e3cf84..f23ea3e 100644 --- a/Minna.xcodeproj/project.pbxproj +++ b/Minna.xcodeproj/project.pbxproj @@ -27,9 +27,9 @@ DB9696AA2FF492BF00CB037A /* DatabaseSchema in Frameworks */ = {isa = PBXBuildFile; productRef = DB9696A92FF492BF00CB037A /* DatabaseSchema */; }; DB9696AC2FF492BF00CB037A /* MinnaChat in Frameworks */ = {isa = PBXBuildFile; productRef = DB9696AB2FF492BF00CB037A /* MinnaChat */; }; DB9696AE2FF492C000CB037A /* ModelManager in Frameworks */ = {isa = PBXBuildFile; productRef = DB9696AD2FF492C000CB037A /* ModelManager */; }; - DBA6E66130254545002A94F2 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = DBA6E66030254545002A94F2 /* PostHog */; }; DBD7CF502FF5689400A2CA0A /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = DBD7CF4F2FF5689400A2CA0A /* KeychainSwift */; }; DBD7CFBB2FF59FEC00A2CA0A /* ModernSettingsWindow in Frameworks */ = {isa = PBXBuildFile; productRef = DBD7CFBA2FF59FEC00A2CA0A /* ModernSettingsWindow */; }; + DBFCD2C2303F922000A99DC6 /* TelemetryDeck in Frameworks */ = {isa = PBXBuildFile; productRef = DBFCD2C1303F922000A99DC6 /* TelemetryDeck */; }; DBFD0AA8300EA44F0060744B /* LookAtMe in Frameworks */ = {isa = PBXBuildFile; productRef = DBFD0AA7300EA44F0060744B /* LookAtMe */; }; /* End PBXBuildFile section */ @@ -93,6 +93,7 @@ DB9696A62FF4929600CB037A /* DatabaseSchema */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = DatabaseSchema; sourceTree = ""; }; DB9696A72FF4929600CB037A /* MinnaChat */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = MinnaChat; sourceTree = ""; }; DBC0F1A02FF0000100000001 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; + DBFCD235303F465100A99DC6 /* AudioEngine */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = AudioEngine; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -168,7 +169,6 @@ DB0CF59F2FEDCD1D0036306B /* SentrySPM in Frameworks */, DB0CF5892FEDCB9E0036306B /* ViewStorageClient in Frameworks */, DB2BB96530052C7D00900E62 /* IrisSearch in Frameworks */, - DBA6E66130254545002A94F2 /* PostHog in Frameworks */, DBFD0AA8300EA44F0060744B /* LookAtMe in Frameworks */, DBD7CF502FF5689400A2CA0A /* KeychainSwift in Frameworks */, DB9695562FF440F200CB037A /* Textual in Frameworks */, @@ -180,6 +180,7 @@ DB6E4A823013CB6F00487D86 /* AppleIntelligenceEmbedder in Frameworks */, DB9696AA2FF492BF00CB037A /* DatabaseSchema in Frameworks */, DB9696AC2FF492BF00CB037A /* MinnaChat in Frameworks */, + DBFCD2C2303F922000A99DC6 /* TelemetryDeck in Frameworks */, DB2BB96130052C7D00900E62 /* Digester in Frameworks */, DB2BB96330052C7D00900E62 /* IrisCommon in Frameworks */, DB6E4FAB301415E300487D86 /* ModelCDN in Frameworks */, @@ -258,6 +259,7 @@ DB9696A82FF4929600CB037A /* Packages */ = { isa = PBXGroup; children = ( + DBFCD235303F465100A99DC6 /* AudioEngine */, DB4AB8863017A062007E4CF7 /* ModelCDN */, DB4BC3B7300AA8CC009C20A3 /* IrisSearch */, DB4BC3B6300AA8C3009C20A3 /* LookAtMe */, @@ -312,8 +314,8 @@ DB6E4A813013CB6F00487D86 /* AppleIntelligenceEmbedder */, DB6E4A833013CB6F00487D86 /* CoreMLEmbedder */, DB6E4FAA301415E300487D86 /* ModelCDN */, - DBA6E66030254545002A94F2 /* PostHog */, DB00BAE43026861500B94A32 /* Logging */, + DBFCD2C1303F922000A99DC6 /* TelemetryDeck */, ); productName = Minna; productReference = DB0CF4D02FEDCA6B0036306B /* Minna.app */; @@ -435,8 +437,8 @@ DB9695542FF440F200CB037A /* XCRemoteSwiftPackageReference "textual" */, DBD7CF4E2FF5689400A2CA0A /* XCRemoteSwiftPackageReference "keychain-swift" */, DBD7CFB92FF59FEC00A2CA0A /* XCRemoteSwiftPackageReference "ModernSettings" */, - DBA6E65F30254545002A94F2 /* XCRemoteSwiftPackageReference "posthog-ios" */, DB00BAE33026861500B94A32 /* XCRemoteSwiftPackageReference "swift-log" */, + DBFCD2C0303F922000A99DC6 /* XCRemoteSwiftPackageReference "SwiftSDK" */, ); preferredProjectObjectVersion = 77; productRefGroup = DB0CF4D12FEDCA6B0036306B /* Products */; @@ -1505,14 +1507,6 @@ minimumVersion = 0.5.0; }; }; - DBA6E65F30254545002A94F2 /* XCRemoteSwiftPackageReference "posthog-ios" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/PostHog/posthog-ios.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 3.69.2; - }; - }; DBD7CF4E2FF5689400A2CA0A /* XCRemoteSwiftPackageReference "keychain-swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/evgenyneu/keychain-swift"; @@ -1529,6 +1523,14 @@ minimumVersion = 1.0.0; }; }; + DBFCD2C0303F922000A99DC6 /* XCRemoteSwiftPackageReference "SwiftSDK" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/TelemetryDeck/SwiftSDK"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.14.2; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1617,11 +1619,6 @@ isa = XCSwiftPackageProductDependency; productName = ModelManager; }; - DBA6E66030254545002A94F2 /* PostHog */ = { - isa = XCSwiftPackageProductDependency; - package = DBA6E65F30254545002A94F2 /* XCRemoteSwiftPackageReference "posthog-ios" */; - productName = PostHog; - }; DBD7CF4F2FF5689400A2CA0A /* KeychainSwift */ = { isa = XCSwiftPackageProductDependency; package = DBD7CF4E2FF5689400A2CA0A /* XCRemoteSwiftPackageReference "keychain-swift" */; @@ -1637,6 +1634,11 @@ package = DB0CF5A12FEDD0970036306B /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */; productName = "plugin:SwiftLintBuildToolPlugin"; }; + DBFCD2C1303F922000A99DC6 /* TelemetryDeck */ = { + isa = XCSwiftPackageProductDependency; + package = DBFCD2C0303F922000A99DC6 /* XCRemoteSwiftPackageReference "SwiftSDK" */; + productName = TelemetryDeck; + }; DBFD0AA7300EA44F0060744B /* LookAtMe */ = { isa = XCSwiftPackageProductDependency; productName = LookAtMe; diff --git a/Minna.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Minna.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index bf453a3..d286ccb 100644 --- a/Minna.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Minna.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "1d84db8793aeb2a1f256271089c7a2a869cb093234d0af61a18dfc6ac5ce8f8a", + "originHash" : "17ea74d33e418df11f2b6289a62b8f53f9f85ee6e63836c80eb5894c94d53c9b", "pins" : [ { "identity" : "anylanguagemodel", @@ -91,15 +91,6 @@ "version" : "1.0.0" } }, - { - "identity" : "posthog-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/PostHog/posthog-ios.git", - "state" : { - "revision" : "21f0162f47b4f4d775cdde5f6a74bfe665333e65", - "version" : "3.69.2" - } - }, { "identity" : "sentry-cocoa", "kind" : "remoteSourceControl", @@ -280,6 +271,15 @@ "version" : "0.65.0" } }, + { + "identity" : "swiftsdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/TelemetryDeck/SwiftSDK", + "state" : { + "revision" : "58f436299d3f6710bcedc18aff26480aaf1879fc", + "version" : "2.14.2" + } + }, { "identity" : "swiftsoup", "kind" : "remoteSourceControl", diff --git a/Minna/Database/FrontendDatabase.swift b/Minna/Database/FrontendDatabase.swift index d9c070d..bad14e2 100644 --- a/Minna/Database/FrontendDatabase.swift +++ b/Minna/Database/FrontendDatabase.swift @@ -82,7 +82,7 @@ class FrontendDatabase: Database { private func sendAnalytics() throws { let fileCount = (try? context.fetchCount(FetchDescriptor())) ?? 0 let askMinnaCount = (try? context.fetchCount(FetchDescriptor())) ?? 0 - TelemetryWrapper.startup(fileCount: fileCount, askMinnaCount: askMinnaCount) + TelemetryWrapper.shared.startup(fileCount: fileCount, askMinnaCount: askMinnaCount) } private func populateStartupData() throws { diff --git a/Minna/Info.plist b/Minna/Info.plist index 22c6f20..a38b252 100644 --- a/Minna/Info.plist +++ b/Minna/Info.plist @@ -36,10 +36,8 @@ - POSTHOG_HOST - $(POSTHOG_HOST) - POSTHOG_PROJECT_TOKEN - $(POSTHOG_PROJECT_TOKEN) + TELEMETRY_DECK_ID + $(TELEMETRY_DECK_ID) SENTRY_DSN $(SENTRY_DSN) SUEnableInstallerLauncherService diff --git a/Minna/IrisApp.swift b/Minna/IrisApp.swift index 50bd82f..54828d4 100644 --- a/Minna/IrisApp.swift +++ b/Minna/IrisApp.swift @@ -13,7 +13,6 @@ import ModelManager import ModernSettingsWindow import Logging import SFSafeSymbols -import PostHog #if SPARKLE import Sparkle @@ -77,13 +76,9 @@ struct MinnaApp: App { } } } - - // Product analytics are opt-in via Config.xcconfig on the same terms. - if let projectToken = BuildConfiguration.postHogProjectToken, - let host = BuildConfiguration.postHogHost { - let config = PostHogConfig(projectToken: projectToken, host: host) - PostHogSDK.shared.setup(config) - } + + // Initialize the telemetry wrapper + let _ = TelemetryWrapper.shared #if SPARKLE // Don't start the sparkle updater under XCTest. Unit tests on CI will fail since sparkle opens a popup asking when to update which hangs the process. diff --git a/Minna/TelemetryWrapper.swift b/Minna/TelemetryWrapper.swift index 5284ed4..1d73cd3 100644 --- a/Minna/TelemetryWrapper.swift +++ b/Minna/TelemetryWrapper.swift @@ -5,35 +5,60 @@ // Created by Taylor Lineman on 8/6/26. // -import PostHog +import TelemetryDeck +import Logging fileprivate extension String { static let startup = "startup" static let chat = "chat" + static let open = "didLaunch" } -struct TelemetryWrapper { +final class TelemetryWrapper { enum ChatLocation: String { case askMinna case askDoc } - static func startup(fileCount: Int, askMinnaCount: Int) { + static let shared: TelemetryWrapper = TelemetryWrapper() + + var initialized: Bool = false + + private init() { + // Product analytics are opt-in via Config.xcconfig on the same terms. + guard let appID = BuildConfiguration.telemetryDeckID else { + Log.logger.warning("Not initializing telemetry: Could not find App ID") + return + } + + let config = TelemetryDeck.Config(appID: appID) + TelemetryDeck.initialize(config: config) + initialized = true + } + + /// Used to track daily active users + func didLaunch() { + guard initialized else { Log.logger.warning("Telemetry not initialized!"); return } + TelemetryDeck.signal(.open) + } + + func startup(fileCount: Int, askMinnaCount: Int) { + guard initialized else { Log.logger.warning("Telemetry not initialized!"); return } let payload = [ - "files": fileCount, - "askMinna": askMinnaCount + "files": "\(fileCount)", + "askMinna": "\(askMinnaCount)" ] - PostHogSDK.shared.capture(.startup, properties: payload) + TelemetryDeck.signal(.startup, parameters: payload) } - static func chat(model: String, location: ChatLocation) { + func chat(model: String, location: ChatLocation) { + guard initialized else { Log.logger.warning("Telemetry not initialized!"); return } let payload = [ "model": model, "location": location.rawValue ] - PostHogSDK.shared.capture(.chat, properties: payload) + TelemetryDeck.signal(.chat, parameters: payload) } } - diff --git a/Minna/Utility/BuildConfiguration.swift b/Minna/Utility/BuildConfiguration.swift index dd1573f..81ca88d 100644 --- a/Minna/Utility/BuildConfiguration.swift +++ b/Minna/Utility/BuildConfiguration.swift @@ -20,22 +20,9 @@ enum BuildConfiguration { value(for: "SENTRY_DSN").map { "https://\($0)" } } - /// The PostHog project token, or `nil` when analytics should stay disabled. - /// - Authored by: Claude Opus 5 (Anthropic) - static var postHogProjectToken: String? { - value(for: "POSTHOG_PROJECT_TOKEN") - } - - /// The PostHog host, or `nil` when analytics should stay disabled. - /// - Authored by: Claude Opus 5 (Anthropic) - static var postHogHost: String? { - value(for: "POSTHOG_HOST").map { "https://\($0)" } - } - - /// Whether product analytics are configured for this build. - /// - Authored by: Claude Opus 5 (Anthropic) - static var isAnalyticsEnabled: Bool { - postHogProjectToken != nil && postHogHost != nil + /// The TelemetryDeck unique app id, or `nil` when analytics should stay disabled. + static var telemetryDeckID: String? { + value(for: "TELEMETRY_DECK_ID") } /// Reads a string from `Info.plist`, treating unconfigured values as absent. diff --git a/Minna/Views/Preview/FileChat.swift b/Minna/Views/Preview/FileChat.swift index 0d85656..4653635 100644 --- a/Minna/Views/Preview/FileChat.swift +++ b/Minna/Views/Preview/FileChat.swift @@ -161,9 +161,9 @@ struct FileChat: View { Task { do { if let model = chatter?.selectedModel { - TelemetryWrapper.chat(model: model.id, location: .askDoc) + TelemetryWrapper.shared.chat(model: model.id, location: .askDoc) } else { - TelemetryWrapper.chat(model: "unknown", location: .askDoc) + TelemetryWrapper.shared.chat(model: "unknown", location: .askDoc) } try await chatter?.submit() diff --git a/Minna/Views/Search/AskMinnaView.swift b/Minna/Views/Search/AskMinnaView.swift index 0ed2431..4e65600 100644 --- a/Minna/Views/Search/AskMinnaView.swift +++ b/Minna/Views/Search/AskMinnaView.swift @@ -252,9 +252,9 @@ struct AskMinnaView: View { Task { do { if let model = chatter.selectedModel { - TelemetryWrapper.chat(model: model.id, location: .askMinna) + TelemetryWrapper.shared.chat(model: model.id, location: .askMinna) } else { - TelemetryWrapper.chat(model: "unknown", location: .askMinna) + TelemetryWrapper.shared.chat(model: "unknown", location: .askMinna) } try await chatter.submit() From 5063342389514fc56d05a4f9700bd9e56a8ee1df Mon Sep 17 00:00:00 2001 From: Taylor Lineman Date: Wed, 26 Aug 2026 18:20:40 -0400 Subject: [PATCH 2/8] Update readme Signed-off-by: Taylor Lineman --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7c0b10f..ba3b8ff 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ https://github.com/user-attachments/assets/d038c2bb-67a2-4760-bc72-8a670a0915a8 Minna takes privacy **very** seriously. We believe that your files are your own, and shouldn't be seen by anyone else. Any conversations with an AI go directly to the provide *you* chose, with *your* key. On-device models (MLX & Apple Foundation Models) make no network calls at all. All LLM provider configurations are encrpyted in the macOS keychain. For more information see [SECURITY.md](SECURITY.md). ## A note on Telemetry -Minna uses [PostHog](https://posthog.com/) for app telemetry. The goal of this telemetry is to get a *broad* understanding of Minna's userbase, and is never used for tracking. Personally Identifiable Information is stripped before being sent to PostHog, and the remote configurations are set to drop IP addresses. We do not want your data, so we do not store it. +Minna uses [TelemetryDeck](https://telemetrydeck.com/) for app telemetry. The goal of this telemetry is to get a *broad* understanding of Minna's userbase, and is never used for tracking. Personally Identifiable Information is not sent to TelemetryDeck, and the remote configurations are set to drop IP addresses. We do not want your data, so we do not store it. # Installation From b4d09cb477a7d34ac99e50fb35c877e26c2215c5 Mon Sep 17 00:00:00 2001 From: Taylor Lineman Date: Wed, 26 Aug 2026 18:23:30 -0400 Subject: [PATCH 3/8] Updated changelog Signed-off-by: Taylor Lineman --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b57e3a5..17a9e9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format has been adjusted to include a release Title. This has been inserted ### Fixed - Moved files are now properly trackeed. +- Moved to TelemetryDeck for private telemetry. ## [0.14.2] - File chat & Import fixex - 2026-08-24 From a59d26caba1d86ae2e74684803dcb5487f9b94bc Mon Sep 17 00:00:00 2001 From: Taylor Lineman Date: Wed, 26 Aug 2026 18:26:11 -0400 Subject: [PATCH 4/8] added didLaunch Signed-off-by: Taylor Lineman --- Minna/IrisApp.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Minna/IrisApp.swift b/Minna/IrisApp.swift index 54828d4..633265e 100644 --- a/Minna/IrisApp.swift +++ b/Minna/IrisApp.swift @@ -78,7 +78,8 @@ struct MinnaApp: App { } // Initialize the telemetry wrapper - let _ = TelemetryWrapper.shared + _ = TelemetryWrapper.shared + TelemetryWrapper.shared.didLaunch() #if SPARKLE // Don't start the sparkle updater under XCTest. Unit tests on CI will fail since sparkle opens a popup asking when to update which hangs the process. From d1e1cd868c6a770a9b90ab947653ea3f45c84650 Mon Sep 17 00:00:00 2001 From: Taylor Lineman Date: Wed, 26 Aug 2026 18:34:43 -0400 Subject: [PATCH 5/8] Updated changelog & removed audio engine from xc proj Signed-off-by: Taylor Lineman --- CHANGELOG.md | 4 +++- Minna.xcodeproj/project.pbxproj | 2 -- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17a9e9d..e62f1ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,11 @@ The format has been adjusted to include a release Title. This has been inserted ## [Unreleased] +### Changed +- Moved to TelemetryDeck for private telemetry. + ### Fixed - Moved files are now properly trackeed. -- Moved to TelemetryDeck for private telemetry. ## [0.14.2] - File chat & Import fixex - 2026-08-24 diff --git a/Minna.xcodeproj/project.pbxproj b/Minna.xcodeproj/project.pbxproj index f23ea3e..c703f45 100644 --- a/Minna.xcodeproj/project.pbxproj +++ b/Minna.xcodeproj/project.pbxproj @@ -93,7 +93,6 @@ DB9696A62FF4929600CB037A /* DatabaseSchema */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = DatabaseSchema; sourceTree = ""; }; DB9696A72FF4929600CB037A /* MinnaChat */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = MinnaChat; sourceTree = ""; }; DBC0F1A02FF0000100000001 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; - DBFCD235303F465100A99DC6 /* AudioEngine */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = AudioEngine; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -259,7 +258,6 @@ DB9696A82FF4929600CB037A /* Packages */ = { isa = PBXGroup; children = ( - DBFCD235303F465100A99DC6 /* AudioEngine */, DB4AB8863017A062007E4CF7 /* ModelCDN */, DB4BC3B7300AA8CC009C20A3 /* IrisSearch */, DB4BC3B6300AA8C3009C20A3 /* LookAtMe */, From d868c86204939ca9cca2581f7624dbafff1d2dd2 Mon Sep 17 00:00:00 2001 From: Taylor Lineman Date: Wed, 26 Aug 2026 18:35:53 -0400 Subject: [PATCH 6/8] Drop redundent initialization of shared Signed-off-by: Taylor Lineman --- Minna/IrisApp.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Minna/IrisApp.swift b/Minna/IrisApp.swift index 633265e..d2600a1 100644 --- a/Minna/IrisApp.swift +++ b/Minna/IrisApp.swift @@ -78,7 +78,6 @@ struct MinnaApp: App { } // Initialize the telemetry wrapper - _ = TelemetryWrapper.shared TelemetryWrapper.shared.didLaunch() #if SPARKLE From b0efffed464782f3155a88818b07541ffb3f7f82 Mon Sep 17 00:00:00 2001 From: Taylor Lineman Date: Wed, 26 Aug 2026 18:36:29 -0400 Subject: [PATCH 7/8] Update didLaunch enum name Signed-off-by: Taylor Lineman --- Minna/TelemetryWrapper.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Minna/TelemetryWrapper.swift b/Minna/TelemetryWrapper.swift index 1d73cd3..fd5457f 100644 --- a/Minna/TelemetryWrapper.swift +++ b/Minna/TelemetryWrapper.swift @@ -11,7 +11,7 @@ import Logging fileprivate extension String { static let startup = "startup" static let chat = "chat" - static let open = "didLaunch" + static let didLaunch = "didLaunch" } final class TelemetryWrapper { @@ -39,7 +39,7 @@ final class TelemetryWrapper { /// Used to track daily active users func didLaunch() { guard initialized else { Log.logger.warning("Telemetry not initialized!"); return } - TelemetryDeck.signal(.open) + TelemetryDeck.signal(.didLaunch) } func startup(fileCount: Int, askMinnaCount: Int) { From 7375b8444cd7908f7a09d7cea730524c7a6749f0 Mon Sep 17 00:00:00 2001 From: Taylor Lineman Date: Thu, 27 Aug 2026 10:41:38 -0400 Subject: [PATCH 8/8] Added onboarding funnel Signed-off-by: Taylor Lineman --- Minna/TelemetryWrapper.swift | 18 ++++++++++++++++++ .../Onboarding/DownloadingModelView.swift | 4 ++++ Minna/Views/Onboarding/FeatureOverview.swift | 2 ++ Minna/Views/Onboarding/IntroVideoView.swift | 3 +++ Minna/Views/Onboarding/ModelSetupView.swift | 2 ++ Minna/Views/Onboarding/ProviderSetupView.swift | 1 + 6 files changed, 30 insertions(+) diff --git a/Minna/TelemetryWrapper.swift b/Minna/TelemetryWrapper.swift index fd5457f..df55b7c 100644 --- a/Minna/TelemetryWrapper.swift +++ b/Minna/TelemetryWrapper.swift @@ -12,6 +12,7 @@ fileprivate extension String { static let startup = "startup" static let chat = "chat" static let didLaunch = "didLaunch" + static let onboardingStage = "onboardingStage" } final class TelemetryWrapper { @@ -20,6 +21,15 @@ final class TelemetryWrapper { case askDoc } + enum OnboardingLocation: String { + case intro + case overview + case modelQuestionnaire + case inferenceDownload + case inferenceDownloadSkipped + case providerSetup + } + static let shared: TelemetryWrapper = TelemetryWrapper() var initialized: Bool = false @@ -61,4 +71,12 @@ final class TelemetryWrapper { TelemetryDeck.signal(.chat, parameters: payload) } + + func onboardingStage(stage: TelemetryWrapper.OnboardingLocation) { + guard initialized else { Log.logger.warning("Telemetry not initialized!"); return } + + let payload = ["stage": stage.rawValue] + + TelemetryDeck.signal(.onboardingStage, parameters: payload) + } } diff --git a/Minna/Views/Onboarding/DownloadingModelView.swift b/Minna/Views/Onboarding/DownloadingModelView.swift index cb627db..296cef7 100644 --- a/Minna/Views/Onboarding/DownloadingModelView.swift +++ b/Minna/Views/Onboarding/DownloadingModelView.swift @@ -61,6 +61,7 @@ struct DownloadingModelView: View { let stream = NotificationCenter.default.messages(of: modelManager, for: DownloadDidFinish.self) for await download in stream where download.identifier == modelIdentifier { + TelemetryWrapper.shared.onboardingStage(stage: .inferenceDownload) onboardingRouter.inferenceFinished(modelManager: modelManager) } } @@ -74,6 +75,7 @@ struct DownloadingModelView: View { } .onAppear { if modelManager.doesModelExistOnDisk(identifier: modelIdentifier) { + TelemetryWrapper.shared.onboardingStage(stage: .inferenceDownload) onboardingRouter.inferenceFinished(modelManager: modelManager) } } @@ -84,6 +86,7 @@ struct DownloadingModelView: View { Group { if canSkipDownload { Button("Skip Downloading") { + TelemetryWrapper.shared.onboardingStage(stage: .inferenceDownloadSkipped) onboardingRouter.inferenceFinished(modelManager: modelManager) } } else { @@ -127,6 +130,7 @@ struct DownloadingModelView: View { if canSkipDownload { Button("Next") { + TelemetryWrapper.shared.onboardingStage(stage: .inferenceDownloadSkipped) onboardingRouter.inferenceFinished(modelManager: modelManager) } .controlSize(.extraLarge) diff --git a/Minna/Views/Onboarding/FeatureOverview.swift b/Minna/Views/Onboarding/FeatureOverview.swift index 1c6b35a..78eae40 100644 --- a/Minna/Views/Onboarding/FeatureOverview.swift +++ b/Minna/Views/Onboarding/FeatureOverview.swift @@ -33,6 +33,7 @@ struct FeatureOverview: View { ForEach(FeatureOverview.pages) { page in HStack { OnboardingCard(page: page) { + TelemetryWrapper.shared.onboardingStage(stage: .overview) onboardingRouter.overviewFinished() } next: { withAnimation { @@ -41,6 +42,7 @@ struct FeatureOverview: View { if nextIndex < FeatureOverview.pages.count { self.currentPage = FeatureOverview.pages[nextIndex] } else { + TelemetryWrapper.shared.onboardingStage(stage: .overview) onboardingRouter.overviewFinished() } } diff --git a/Minna/Views/Onboarding/IntroVideoView.swift b/Minna/Views/Onboarding/IntroVideoView.swift index 5cd8495..04476b5 100644 --- a/Minna/Views/Onboarding/IntroVideoView.swift +++ b/Minna/Views/Onboarding/IntroVideoView.swift @@ -19,10 +19,12 @@ struct IntroVideoView: View { HStack { if let url { EndAnnouncingVideoPlayer(url: url) { + TelemetryWrapper.shared.onboardingStage(stage: .intro) onboardingRouter.introFinished() } doneLoading: { showVideo = true } failedToLoad: { + TelemetryWrapper.shared.onboardingStage(stage: .intro) onboardingRouter.introFinished() } .opacity(showVideo ? 1 : 0) @@ -30,6 +32,7 @@ struct IntroVideoView: View { } else { ContentUnavailableView("Could not load intro video", systemSymbol: .videoSlash) .task { + TelemetryWrapper.shared.onboardingStage(stage: .intro) onboardingRouter.introFinished() } } diff --git a/Minna/Views/Onboarding/ModelSetupView.swift b/Minna/Views/Onboarding/ModelSetupView.swift index 5420b21..98cf9c9 100644 --- a/Minna/Views/Onboarding/ModelSetupView.swift +++ b/Minna/Views/Onboarding/ModelSetupView.swift @@ -29,11 +29,13 @@ struct ModelSetupView: View { .frame(width: 350) HStack { Button("With my own AI Providers") { + TelemetryWrapper.shared.onboardingStage(stage: .modelQuestionnaire) onboardingRouter.inferenceQuestionnaire(result: .providers, modelManager: modelManager) } .controlSize(.extraLarge) Button("Completely On Device") { + TelemetryWrapper.shared.onboardingStage(stage: .modelQuestionnaire) onboardingRouter.inferenceQuestionnaire(result: .onDevice, modelManager: modelManager) } .controlSize(.extraLarge) diff --git a/Minna/Views/Onboarding/ProviderSetupView.swift b/Minna/Views/Onboarding/ProviderSetupView.swift index 2a24997..e64c219 100644 --- a/Minna/Views/Onboarding/ProviderSetupView.swift +++ b/Minna/Views/Onboarding/ProviderSetupView.swift @@ -49,6 +49,7 @@ struct ProviderSetupView: View { .padding(.vertical) Button("Next") { + TelemetryWrapper.shared.onboardingStage(stage: .providerSetup) onboardingRouter.providersFinished(modelManager: modelManager) } .controlSize(.extraLarge)