From 7439d7c8cf4ed0eae8e8434d2fff933118e9247b Mon Sep 17 00:00:00 2001 From: "posthog-eu[bot]" <226701856+posthog-eu[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:12:51 +0000 Subject: [PATCH 1/3] Add PostHog analytics integration Generated-By: PostHog Code Task-Id: 8222640f-ff3b-4f81-b63a-26361e552ae6 --- .env.example | 3 ++ .gitignore | 4 +++ MicStatusAI/App/MicStatusAIApp.swift | 28 +++++++++++++++++++ .../HotKeyRecorderCoordinator.swift | 5 ++++ MicStatusAI/Info.plist | 4 +++ .../Models/MicrophoneStatusModel.swift | 11 ++++++++ MicStatusAI/Views/HotKeySettingsView.swift | 2 ++ MicStatusAI/Views/InputLevelControl.swift | 10 ++++++- .../Views/StatusOverlaySettingsView.swift | 23 ++++++++++++++- project.yml | 10 +++++++ 10 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..48f3533 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +# PostHog public client configuration +POSTHOG_PROJECT_TOKEN=phc_your_project_token +POSTHOG_HOST=https://your-posthog-host diff --git a/.gitignore b/.gitignore index 9eb667f..43d22b9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# Environment +.env +.env.xcconfig + # Xcode MicStatusAI.xcodeproj/ DerivedData/ diff --git a/MicStatusAI/App/MicStatusAIApp.swift b/MicStatusAI/App/MicStatusAIApp.swift index 84ecb29..1f89825 100644 --- a/MicStatusAI/App/MicStatusAIApp.swift +++ b/MicStatusAI/App/MicStatusAIApp.swift @@ -1,7 +1,35 @@ +import PostHog import SwiftUI @main struct MicStatusAIApp: App { + init() { + let environment = ProcessInfo.processInfo.environment + let projectToken = environment["POSTHOG_PROJECT_TOKEN"] + ?? Bundle.main.object(forInfoDictionaryKey: "POSTHOG_PROJECT_TOKEN") as? String + guard let projectToken, !projectToken.isEmpty else { + #if DEBUG + fatalError("POSTHOG_PROJECT_TOKEN variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once POSTHOG_PROJECT_TOKEN is configured") + #else + return + #endif + } + + let host = environment["POSTHOG_HOST"] + ?? Bundle.main.object(forInfoDictionaryKey: "POSTHOG_HOST") as? String + guard let host, !host.isEmpty else { + #if DEBUG + fatalError("POSTHOG_HOST variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once POSTHOG_HOST is configured") + #else + return + #endif + } + + let config = PostHogConfig(apiKey: projectToken, host: host) + config.errorTrackingConfig.autoCapture = true + PostHogSDK.shared.setup(config) + } + @AppStorage("statusOverlayEnabled") private var statusOverlayEnabled = true @AppStorage("statusOverlayDuration") diff --git a/MicStatusAI/Coordinators/HotKeyRecorderCoordinator.swift b/MicStatusAI/Coordinators/HotKeyRecorderCoordinator.swift index 77b8bd2..7f08689 100644 --- a/MicStatusAI/Coordinators/HotKeyRecorderCoordinator.swift +++ b/MicStatusAI/Coordinators/HotKeyRecorderCoordinator.swift @@ -1,5 +1,6 @@ import AppKit import Carbon.HIToolbox +import PostHog @MainActor final class HotKeyRecorderCoordinator: NSObject { @@ -47,6 +48,10 @@ final class HotKeyRecorderCoordinator: NSObject { parent.onValidationError(nil) parent.onChange(configuration) + PostHogSDK.shared.capture( + "hotkey_configured", + properties: ["modifier_count": configuration.modifierCount] + ) button.finishRecording(with: configuration) button.window?.makeFirstResponder(nil) } diff --git a/MicStatusAI/Info.plist b/MicStatusAI/Info.plist index 36a532d..5b77f74 100644 --- a/MicStatusAI/Info.plist +++ b/MicStatusAI/Info.plist @@ -26,5 +26,9 @@ NSPrincipalClass NSApplication + POSTHOG_HOST + $(POSTHOG_HOST) + POSTHOG_PROJECT_TOKEN + $(POSTHOG_PROJECT_TOKEN) diff --git a/MicStatusAI/Models/MicrophoneStatusModel.swift b/MicStatusAI/Models/MicrophoneStatusModel.swift index b9c4936..1c4da8b 100644 --- a/MicStatusAI/Models/MicrophoneStatusModel.swift +++ b/MicStatusAI/Models/MicrophoneStatusModel.swift @@ -1,5 +1,6 @@ import Foundation import Observation +import PostHog @MainActor @Observable @@ -79,6 +80,11 @@ final class MicrophoneStatusModel { } else { startMonitoring() } + + PostHogSDK.shared.capture( + "microphone_monitoring_toggled", + properties: ["is_monitoring": isMonitoring] + ) } func startMonitoring() { @@ -142,6 +148,11 @@ final class MicrophoneStatusModel { if isMonitoring { refreshStatus() } + + PostHogSDK.shared.capture( + "microphone_mute_toggled", + properties: ["is_muted": targetMuteState] + ) } catch { if isMonitoring { status = .unavailable(error.localizedDescription) diff --git a/MicStatusAI/Views/HotKeySettingsView.swift b/MicStatusAI/Views/HotKeySettingsView.swift index bda55d1..68ebb4f 100644 --- a/MicStatusAI/Views/HotKeySettingsView.swift +++ b/MicStatusAI/Views/HotKeySettingsView.swift @@ -1,3 +1,4 @@ +import PostHog import SwiftUI struct HotKeySettingsView: View { @@ -69,6 +70,7 @@ struct HotKeySettingsView: View { Button { recordingError = nil model.restoreDefaultHotKey() + PostHogSDK.shared.capture("hotkey_restored") } label: { Text(L10n.actionRestoreHotkey) } diff --git a/MicStatusAI/Views/InputLevelControl.swift b/MicStatusAI/Views/InputLevelControl.swift index a82f9b2..4692941 100644 --- a/MicStatusAI/Views/InputLevelControl.swift +++ b/MicStatusAI/Views/InputLevelControl.swift @@ -1,3 +1,4 @@ +import PostHog import SwiftUI struct InputLevelControl: View { @@ -15,7 +16,14 @@ struct InputLevelControl: View { get: { model.inputLevel }, set: { model.setInputLevel($0) } ), - in: 0 ... 1 + in: 0 ... 1, + onEditingChanged: { isEditing in + guard !isEditing else { return } + PostHogSDK.shared.capture( + "input_level_adjusted", + properties: ["input_level": model.inputLevel] + ) + } ) { Text(L10n.inputAccessibility) } diff --git a/MicStatusAI/Views/StatusOverlaySettingsView.swift b/MicStatusAI/Views/StatusOverlaySettingsView.swift index 58b044b..bec1a43 100644 --- a/MicStatusAI/Views/StatusOverlaySettingsView.swift +++ b/MicStatusAI/Views/StatusOverlaySettingsView.swift @@ -1,3 +1,4 @@ +import PostHog import SwiftUI struct StatusOverlaySettingsView: View { @@ -10,6 +11,9 @@ struct StatusOverlaySettingsView: View { GroupBox { VStack(alignment: .leading, spacing: 8) { Toggle(L10n.overlayEnabled, isOn: $isEnabled) + .onChange(of: isEnabled) { _, isEnabled in + captureSettingChange("enabled", value: isEnabled) + } Picker(L10n.overlayDuration, selection: $duration) { ForEach(StatusOverlayDuration.allCases) { option in @@ -19,6 +23,9 @@ struct StatusOverlaySettingsView: View { } .pickerStyle(.menu) .disabled(!isEnabled) + .onChange(of: duration) { _, duration in + captureSettingChange("duration_seconds", value: duration.rawValue) + } Picker(L10n.overlayPlacement, selection: $placement) { ForEach(StatusOverlayPlacement.allCases) { option in @@ -28,13 +35,20 @@ struct StatusOverlaySettingsView: View { } .pickerStyle(.menu) .disabled(!isEnabled) + .onChange(of: placement) { _, placement in + captureSettingChange("placement", value: placement.rawValue) + } LabeledContent { HStack(spacing: 8) { Slider( value: $transparency, in: StatusOverlayTransparency.range, - step: StatusOverlayTransparency.step + step: StatusOverlayTransparency.step, + onEditingChanged: { isEditing in + guard !isEditing else { return } + captureSettingChange("transparency", value: transparency) + } ) { Text(L10n.overlayTransparency) } @@ -70,4 +84,11 @@ struct StatusOverlaySettingsView: View { Text(L10n.overlayTitle) } } + + private func captureSettingChange(_ setting: String, value: Any) { + PostHogSDK.shared.capture( + "status_overlay_setting_changed", + properties: ["setting": setting, "value": value] + ) + } } diff --git a/project.yml b/project.yml index 644bf08..01d1f5d 100644 --- a/project.yml +++ b/project.yml @@ -12,6 +12,15 @@ configs: Debug: debug Release: release +configFiles: + Debug: .env.xcconfig + Release: .env.xcconfig + +packages: + PostHog: + url: https://github.com/PostHog/posthog-ios.git + from: 3.59.3 + settings: base: CLANG_ENABLE_MODULES: "YES" @@ -33,6 +42,7 @@ targets: dependencies: - sdk: CoreAudio.framework - sdk: Carbon.framework + - package: PostHog settings: base: ARCHS: arm64 From 88a342207ff4eefc6d9d2cabd02040927d253ab4 Mon Sep 17 00:00:00 2001 From: "posthog-eu[bot]" <226701856+posthog-eu[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:18:04 +0000 Subject: [PATCH 2/3] Fix CI by tracking a default PostHog xcconfig xcodegen generate failed because project.yml pointed configFiles at the gitignored .env.xcconfig, which never exists in a fresh checkout. PostHog.xcconfig is tracked with empty defaults and optionally includes .env.xcconfig so local secrets still take effect. Generated-By: PostHog Code Task-Id: 8222640f-ff3b-4f81-b63a-26361e552ae6 --- PostHog.xcconfig | 8 ++++++++ project.yml | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 PostHog.xcconfig diff --git a/PostHog.xcconfig b/PostHog.xcconfig new file mode 100644 index 0000000..cdfdae6 --- /dev/null +++ b/PostHog.xcconfig @@ -0,0 +1,8 @@ +// Default PostHog configuration. +// Real values are supplied via the gitignored .env.xcconfig for local builds, +// or injected before this file is read for CI/release builds. This file must +// stay tracked so xcodegen can always find a config file to load. +POSTHOG_PROJECT_TOKEN = +POSTHOG_HOST = + +#include? ".env.xcconfig" diff --git a/project.yml b/project.yml index 01d1f5d..1c5b83d 100644 --- a/project.yml +++ b/project.yml @@ -13,8 +13,8 @@ configs: Release: release configFiles: - Debug: .env.xcconfig - Release: .env.xcconfig + Debug: PostHog.xcconfig + Release: PostHog.xcconfig packages: PostHog: From d31aa8bc606de7087d6acd883e038a0bda838ddf Mon Sep 17 00:00:00 2001 From: "posthog-eu[bot]" <226701856+posthog-eu[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:22:17 +0000 Subject: [PATCH 3/3] Fix SwiftLint violations from PostHog instrumentation Wraps two over-long fatalError messages under the 160-char line-length limit, and passes Slider's label as an explicit argument instead of a second trailing closure to satisfy multiple_closures_with_trailing_closure. No behavior change. Generated-By: PostHog Code Task-Id: 8222640f-ff3b-4f81-b63a-26361e552ae6 --- MicStatusAI/App/MicStatusAIApp.swift | 11 +++++++++-- MicStatusAI/Views/InputLevelControl.swift | 7 ++++--- MicStatusAI/Views/StatusOverlaySettingsView.swift | 7 ++++--- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/MicStatusAI/App/MicStatusAIApp.swift b/MicStatusAI/App/MicStatusAIApp.swift index 1f89825..9d1a6ea 100644 --- a/MicStatusAI/App/MicStatusAIApp.swift +++ b/MicStatusAI/App/MicStatusAIApp.swift @@ -9,7 +9,11 @@ struct MicStatusAIApp: App { ?? Bundle.main.object(forInfoDictionaryKey: "POSTHOG_PROJECT_TOKEN") as? String guard let projectToken, !projectToken.isEmpty else { #if DEBUG - fatalError("POSTHOG_PROJECT_TOKEN variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once POSTHOG_PROJECT_TOKEN is configured") + fatalError( + "POSTHOG_PROJECT_TOKEN variable required by PostHog is missing or un-configured, " + + "this causes events to be silently missed. This error stops appearing once " + + "POSTHOG_PROJECT_TOKEN is configured" + ) #else return #endif @@ -19,7 +23,10 @@ struct MicStatusAIApp: App { ?? Bundle.main.object(forInfoDictionaryKey: "POSTHOG_HOST") as? String guard let host, !host.isEmpty else { #if DEBUG - fatalError("POSTHOG_HOST variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once POSTHOG_HOST is configured") + fatalError( + "POSTHOG_HOST variable required by PostHog is missing or un-configured, this causes " + + "events to be silently missed. This error stops appearing once POSTHOG_HOST is configured" + ) #else return #endif diff --git a/MicStatusAI/Views/InputLevelControl.swift b/MicStatusAI/Views/InputLevelControl.swift index 4692941..eaa7cb7 100644 --- a/MicStatusAI/Views/InputLevelControl.swift +++ b/MicStatusAI/Views/InputLevelControl.swift @@ -23,10 +23,11 @@ struct InputLevelControl: View { "input_level_adjusted", properties: ["input_level": model.inputLevel] ) + }, + label: { + Text(L10n.inputAccessibility) } - ) { - Text(L10n.inputAccessibility) - } + ) .tint(model.inputLevel > 0 ? .green : .red) .disabled(!model.canAdjustInputLevel) .accessibilityValue( diff --git a/MicStatusAI/Views/StatusOverlaySettingsView.swift b/MicStatusAI/Views/StatusOverlaySettingsView.swift index bec1a43..477daba 100644 --- a/MicStatusAI/Views/StatusOverlaySettingsView.swift +++ b/MicStatusAI/Views/StatusOverlaySettingsView.swift @@ -48,10 +48,11 @@ struct StatusOverlaySettingsView: View { onEditingChanged: { isEditing in guard !isEditing else { return } captureSettingChange("transparency", value: transparency) + }, + label: { + Text(L10n.overlayTransparency) } - ) { - Text(L10n.overlayTransparency) - } + ) .labelsHidden() .accessibilityLabel(L10n.overlayTransparency) .accessibilityValue(