Skip to content
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ 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.

Expand Down
7 changes: 2 additions & 5 deletions Config.local.example.xcconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
9 changes: 3 additions & 6 deletions Config.xcconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -29,11 +29,8 @@ DEVELOPMENT_TEAM =
// Store as: <key>@<org>.ingest.us.sentry.io/<project>
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
//
Expand Down
34 changes: 17 additions & 17 deletions Minna.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */

Expand Down Expand Up @@ -168,7 +168,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 */,
Expand All @@ -180,6 +179,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 */,
Expand Down Expand Up @@ -312,8 +312,8 @@
DB6E4A813013CB6F00487D86 /* AppleIntelligenceEmbedder */,
DB6E4A833013CB6F00487D86 /* CoreMLEmbedder */,
DB6E4FAA301415E300487D86 /* ModelCDN */,
DBA6E66030254545002A94F2 /* PostHog */,
DB00BAE43026861500B94A32 /* Logging */,
DBFCD2C1303F922000A99DC6 /* TelemetryDeck */,
);
productName = Minna;
productReference = DB0CF4D02FEDCA6B0036306B /* Minna.app */;
Expand Down Expand Up @@ -435,8 +435,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 */;
Expand Down Expand Up @@ -1505,14 +1505,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";
Expand All @@ -1529,6 +1521,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 */
Expand Down Expand Up @@ -1617,11 +1617,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" */;
Expand All @@ -1637,6 +1632,11 @@
package = DB0CF5A12FEDD0970036306B /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */;
productName = "plugin:SwiftLintBuildToolPlugin";
};
DBFCD2C1303F922000A99DC6 /* TelemetryDeck */ = {
isa = XCSwiftPackageProductDependency;
package = DBFCD2C0303F922000A99DC6 /* XCRemoteSwiftPackageReference "SwiftSDK" */;
productName = TelemetryDeck;
};
DBFD0AA7300EA44F0060744B /* LookAtMe */ = {
isa = XCSwiftPackageProductDependency;
productName = LookAtMe;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Minna/Database/FrontendDatabase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ class FrontendDatabase: Database {
private func sendAnalytics() throws {
let fileCount = (try? context.fetchCount(FetchDescriptor<File>())) ?? 0
let askMinnaCount = (try? context.fetchCount(FetchDescriptor<Chat>())) ?? 0
TelemetryWrapper.startup(fileCount: fileCount, askMinnaCount: askMinnaCount)
TelemetryWrapper.shared.startup(fileCount: fileCount, askMinnaCount: askMinnaCount)
}

private func populateStartupData() throws {
Expand Down
6 changes: 2 additions & 4 deletions Minna/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,8 @@
</array>
</dict>
</array>
<key>POSTHOG_HOST</key>
<string>$(POSTHOG_HOST)</string>
<key>POSTHOG_PROJECT_TOKEN</key>
<string>$(POSTHOG_PROJECT_TOKEN)</string>
<key>TELEMETRY_DECK_ID</key>
<string>$(TELEMETRY_DECK_ID)</string>
<key>SENTRY_DSN</key>
<string>$(SENTRY_DSN)</string>
<key>SUEnableInstallerLauncherService</key>
Expand Down
11 changes: 3 additions & 8 deletions Minna/IrisApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import ModelManager
import ModernSettingsWindow
import Logging
import SFSafeSymbols
import PostHog

#if SPARKLE
import Sparkle
Expand Down Expand Up @@ -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
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.
Expand Down
61 changes: 52 additions & 9 deletions Minna/TelemetryWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,78 @@
// 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 didLaunch = "didLaunch"
static let onboardingStage = "onboardingStage"
}

struct TelemetryWrapper {
final class TelemetryWrapper {
enum ChatLocation: String {
case askMinna
case askDoc
}

static func startup(fileCount: Int, askMinnaCount: Int) {
enum OnboardingLocation: String {
case intro
case overview
case modelQuestionnaire
case inferenceDownload
case inferenceDownloadSkipped
case providerSetup
}

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(.didLaunch)
}

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)
}
}

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)
}
}
19 changes: 3 additions & 16 deletions Minna/Utility/BuildConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions Minna/Views/Onboarding/DownloadingModelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand All @@ -74,6 +75,7 @@ struct DownloadingModelView: View {
}
.onAppear {
if modelManager.doesModelExistOnDisk(identifier: modelIdentifier) {
TelemetryWrapper.shared.onboardingStage(stage: .inferenceDownload)
onboardingRouter.inferenceFinished(modelManager: modelManager)
}
}
Expand All @@ -84,6 +86,7 @@ struct DownloadingModelView: View {
Group {
if canSkipDownload {
Button("Skip Downloading") {
TelemetryWrapper.shared.onboardingStage(stage: .inferenceDownloadSkipped)
onboardingRouter.inferenceFinished(modelManager: modelManager)
}
} else {
Expand Down Expand Up @@ -127,6 +130,7 @@ struct DownloadingModelView: View {

if canSkipDownload {
Button("Next") {
TelemetryWrapper.shared.onboardingStage(stage: .inferenceDownloadSkipped)
onboardingRouter.inferenceFinished(modelManager: modelManager)
}
.controlSize(.extraLarge)
Expand Down
2 changes: 2 additions & 0 deletions Minna/Views/Onboarding/FeatureOverview.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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()
}
}
Expand Down
Loading
Loading