Replace EventLogger with typed AnalyticsManager using PostHog

Complete analytics overhaul: delete EventLogger.swift, create Analytics.swift
with typed event enum (~45 events), screen tracking, super properties
(theme, icon pack, voting layout, etc.), session replay with kill switch,
autocapture, and network telemetry. Replace all 99 call sites across 38 files
with compiler-enforced typed events in object_action naming convention.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-10 15:12:33 -06:00
parent a08d0d33c0
commit e0330dbc8d
38 changed files with 1048 additions and 202 deletions

View File

@@ -38,7 +38,7 @@ struct PaywallPreviewSettingsView: View {
}
}
.sheet(isPresented: $showFullPreview) {
FeelsSubscriptionStoreView(style: selectedStyle)
FeelsSubscriptionStoreView(source: "paywall_preview", style: selectedStyle)
.environmentObject(iapManager)
}
}

View File

@@ -73,7 +73,7 @@ struct SettingsTabView: View {
WhyUpgradeView()
}
.sheet(isPresented: $showSubscriptionStore) {
FeelsSubscriptionStoreView()
FeelsSubscriptionStoreView(source: "settings_tab")
.environmentObject(iapManager)
}
}

View File

@@ -60,6 +60,7 @@ struct SettingsContentView: View {
legalSectionHeader
eulaButton
privacyButton
analyticsToggle
addTestDataButton
@@ -108,7 +109,7 @@ struct SettingsContentView: View {
ReminderTimePickerView()
}
.onAppear(perform: {
EventLogger.log(event: "show_settings_view")
AnalyticsManager.shared.trackScreen(.settings)
FeelsTipsManager.shared.onSettingsViewed()
})
}
@@ -119,7 +120,7 @@ struct SettingsContentView: View {
ZStack {
theme.currentTheme.secondaryBGColor
Button(action: {
EventLogger.log(event: "tap_reminder_time")
AnalyticsManager.shared.track(.reminderTimeTapped)
showReminderTimePicker = true
}, label: {
HStack(spacing: 12) {
@@ -832,7 +833,7 @@ struct SettingsContentView: View {
if newValue {
let success = await authManager.enableLock()
if !success {
EventLogger.log(event: "privacy_lock_enable_failed")
AnalyticsManager.shared.track(.privacyLockEnableFailed)
}
} else {
authManager.disableLock()
@@ -927,16 +928,16 @@ struct SettingsContentView: View {
// Sync all existing moods to HealthKit
await HealthKitManager.shared.syncAllMoods()
} else {
EventLogger.log(event: "healthkit_state_of_mind_not_authorized")
AnalyticsManager.shared.track(.healthKitNotAuthorized)
}
} catch {
print("HealthKit authorization failed: \(error)")
EventLogger.log(event: "healthkit_enable_failed")
AnalyticsManager.shared.track(.healthKitEnableFailed)
}
}
} else {
healthService.isEnabled = false
EventLogger.log(event: "healthkit_disabled")
AnalyticsManager.shared.track(.healthKitDisabled)
}
}
))
@@ -988,7 +989,7 @@ struct SettingsContentView: View {
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
.healthKitSyncTip()
.sheet(isPresented: $showSubscriptionStore) {
FeelsSubscriptionStoreView()
FeelsSubscriptionStoreView(source: "settings")
}
}
@@ -998,7 +999,7 @@ struct SettingsContentView: View {
ZStack {
theme.currentTheme.secondaryBGColor
Button(action: {
EventLogger.log(event: "tap_export_data")
AnalyticsManager.shared.track(.exportTapped)
showExportView = true
}, label: {
HStack(spacing: 12) {
@@ -1035,7 +1036,7 @@ struct SettingsContentView: View {
ZStack {
theme.currentTheme.secondaryBGColor
Button(action: {
EventLogger.log(event: "tap_show_onboarding")
AnalyticsManager.shared.track(.onboardingReshown)
showOnboarding.toggle()
}, label: {
Text(String(localized: "settings_view_show_onboarding"))
@@ -1055,7 +1056,7 @@ struct SettingsContentView: View {
Toggle(String(localized: "settings_use_delete_enable"),
isOn: $deleteEnabled)
.onChange(of: deleteEnabled) { _, newValue in
EventLogger.log(event: "toggle_can_delete", withData: ["value": newValue])
AnalyticsManager.shared.track(.deleteToggleChanged(enabled: newValue))
}
.foregroundColor(textColor)
.accessibilityHint(String(localized: "Allow deleting mood entries by swiping"))
@@ -1070,7 +1071,7 @@ struct SettingsContentView: View {
ZStack {
theme.currentTheme.secondaryBGColor
Button(action: {
EventLogger.log(event: "show_eula")
AnalyticsManager.shared.track(.eulaViewed)
if let url = URL(string: "https://feels.app/eula.html") {
UIApplication.shared.open(url)
}
@@ -1089,7 +1090,7 @@ struct SettingsContentView: View {
ZStack {
theme.currentTheme.secondaryBGColor
Button(action: {
EventLogger.log(event: "show_privacy")
AnalyticsManager.shared.track(.privacyPolicyViewed)
if let url = URL(string: "https://feels.app/privacy.html") {
UIApplication.shared.open(url)
}
@@ -1104,6 +1105,48 @@ struct SettingsContentView: View {
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
}
// MARK: - Analytics Toggle
private var analyticsToggle: some View {
ZStack {
theme.currentTheme.secondaryBGColor
HStack(spacing: 12) {
Image(systemName: "chart.bar.xaxis")
.font(.title2)
.foregroundColor(.accentColor)
.frame(width: 32)
VStack(alignment: .leading, spacing: 2) {
Text("Share Analytics")
.foregroundColor(textColor)
Text("Help improve Feels by sharing anonymous usage data")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Toggle("", isOn: Binding(
get: { !AnalyticsManager.shared.isOptedOut },
set: { enabled in
if enabled {
AnalyticsManager.shared.optIn()
} else {
AnalyticsManager.shared.optOut()
}
}
))
.labelsHidden()
.accessibilityLabel("Share Analytics")
.accessibilityHint("Toggle anonymous usage analytics")
}
.padding()
}
.fixedSize(horizontal: false, vertical: true)
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
}
// MARK: - Helper Functions
private func openInFilesApp(_ url: URL) {
@@ -1175,7 +1218,7 @@ struct ReminderTimePickerView: View {
// This handles notification scheduling and Live Activity rescheduling
OnboardingDataDataManager.shared.updateOnboardingData(onboardingData: onboardingData)
EventLogger.log(event: "reminder_time_updated")
AnalyticsManager.shared.track(.reminderTimeUpdated)
}
}
@@ -1231,6 +1274,7 @@ struct SettingsView: View {
legalSectionHeader
eulaButton
privacyButton
analyticsToggle
// specialThanksCell
}
@@ -1268,7 +1312,7 @@ struct SettingsView: View {
))
}
.onAppear(perform: {
EventLogger.log(event: "show_settings_view")
AnalyticsManager.shared.trackScreen(.settings)
})
.background(
theme.currentTheme.bg
@@ -1282,7 +1326,7 @@ struct SettingsView: View {
onCompletion: { result in
switch result {
case .success(let url):
EventLogger.log(event: "exported_file")
AnalyticsManager.shared.track(.dataExported(format: "file", count: 0))
print("Saved to \(url)")
case .failure(let error):
print(error.localizedDescription)
@@ -1319,13 +1363,13 @@ struct SettingsView: View {
DataController.shared.add(mood: mood, forDate: forDate, entryType: entryType)
}
DataController.shared.saveAndRunDataListeners()
EventLogger.log(event: "import_file")
AnalyticsManager.shared.track(.importSucceeded)
} else {
EventLogger.log(event: "error_import_file")
AnalyticsManager.shared.track(.importFailed(error: nil))
}
} catch {
// Handle failure.
EventLogger.log(event: "error_import_file", withData: ["error": error.localizedDescription])
AnalyticsManager.shared.track(.importFailed(error: error.localizedDescription))
print("Unable to read file contents")
print(error.localizedDescription)
}
@@ -1402,7 +1446,7 @@ struct SettingsView: View {
if newValue {
let success = await authManager.enableLock()
if !success {
EventLogger.log(event: "privacy_lock_enable_failed")
AnalyticsManager.shared.track(.privacyLockEnableFailed)
}
} else {
authManager.disableLock()
@@ -1465,16 +1509,16 @@ struct SettingsView: View {
// Sync all existing moods to HealthKit
await HealthKitManager.shared.syncAllMoods()
} else {
EventLogger.log(event: "healthkit_state_of_mind_not_authorized")
AnalyticsManager.shared.track(.healthKitNotAuthorized)
}
} catch {
print("HealthKit authorization failed: \(error)")
EventLogger.log(event: "healthkit_enable_failed")
AnalyticsManager.shared.track(.healthKitEnableFailed)
}
}
} else {
healthService.isEnabled = false
EventLogger.log(event: "healthkit_disabled")
AnalyticsManager.shared.track(.healthKitDisabled)
}
}
))
@@ -1518,7 +1562,7 @@ struct SettingsView: View {
.background(theme.currentTheme.secondaryBGColor)
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
.sheet(isPresented: $showSubscriptionStore) {
FeelsSubscriptionStoreView()
FeelsSubscriptionStoreView(source: "settings")
}
}
@@ -1528,7 +1572,7 @@ struct SettingsView: View {
ZStack {
theme.currentTheme.secondaryBGColor
Button(action: {
EventLogger.log(event: "tap_export_data")
AnalyticsManager.shared.track(.exportTapped)
showExportView = true
}, label: {
HStack(spacing: 12) {
@@ -1558,12 +1602,12 @@ struct SettingsView: View {
.fixedSize(horizontal: false, vertical: true)
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
}
private var closeButtonView: some View {
HStack{
Spacer()
Button(action: {
EventLogger.log(event: "tap_settings_close")
AnalyticsManager.shared.track(.settingsClosed)
dismiss()
}, label: {
Text(String(localized: "settings_view_exit"))
@@ -1578,7 +1622,7 @@ struct SettingsView: View {
theme.currentTheme.secondaryBGColor
VStack {
Button(action: {
EventLogger.log(event: "tap_show_special_thanks")
AnalyticsManager.shared.track(.specialThanksViewed)
withAnimation{
showSpecialThanks.toggle()
}
@@ -1753,7 +1797,7 @@ struct SettingsView: View {
ZStack {
theme.currentTheme.secondaryBGColor
Button(action: {
EventLogger.log(event: "tap_show_onboarding")
AnalyticsManager.shared.track(.onboardingReshown)
showOnboarding.toggle()
}, label: {
Text(String(localized: "settings_view_show_onboarding"))
@@ -1769,7 +1813,7 @@ struct SettingsView: View {
ZStack {
theme.currentTheme.secondaryBGColor
Button(action: {
EventLogger.log(event: "show_eula")
AnalyticsManager.shared.track(.eulaViewed)
openURL(URL(string: "https://feels.app/eula.html")!)
}, label: {
Text(String(localized: "settings_view_show_eula"))
@@ -1785,7 +1829,7 @@ struct SettingsView: View {
ZStack {
theme.currentTheme.secondaryBGColor
Button(action: {
EventLogger.log(event: "show_privacy")
AnalyticsManager.shared.track(.privacyPolicyViewed)
openURL(URL(string: "https://feels.app/privacy.html")!)
}, label: {
Text(String(localized: "settings_view_show_privacy"))
@@ -1796,7 +1840,47 @@ struct SettingsView: View {
.fixedSize(horizontal: false, vertical: true)
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
}
private var analyticsToggle: some View {
ZStack {
theme.currentTheme.secondaryBGColor
HStack(spacing: 12) {
Image(systemName: "chart.bar.xaxis")
.font(.title2)
.foregroundColor(.accentColor)
.frame(width: 32)
VStack(alignment: .leading, spacing: 2) {
Text("Share Analytics")
.foregroundColor(textColor)
Text("Help improve Feels by sharing anonymous usage data")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Toggle("", isOn: Binding(
get: { !AnalyticsManager.shared.isOptedOut },
set: { enabled in
if enabled {
AnalyticsManager.shared.optIn()
} else {
AnalyticsManager.shared.optOut()
}
}
))
.labelsHidden()
.accessibilityLabel("Share Analytics")
.accessibilityHint("Toggle anonymous usage analytics")
}
.padding()
}
.fixedSize(horizontal: false, vertical: true)
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
}
private var canDelete: some View {
ZStack {
theme.currentTheme.secondaryBGColor
@@ -1804,7 +1888,7 @@ struct SettingsView: View {
Toggle(String(localized: "settings_use_delete_enable"),
isOn: $deleteEnabled)
.onChange(of: deleteEnabled) { _, newValue in
EventLogger.log(event: "toggle_can_delete", withData: ["value": newValue])
AnalyticsManager.shared.track(.deleteToggleChanged(enabled: newValue))
}
.foregroundColor(textColor)
.padding()
@@ -1819,7 +1903,7 @@ struct SettingsView: View {
theme.currentTheme.secondaryBGColor
Button(action: {
showingExporter.toggle()
EventLogger.log(event: "export_data", withData: ["title": "default"])
AnalyticsManager.shared.track(.exportTapped)
}, label: {
Text("Export")
.foregroundColor(textColor)
@@ -1835,7 +1919,7 @@ struct SettingsView: View {
theme.currentTheme.secondaryBGColor
Button(action: {
showingImporter.toggle()
EventLogger.log(event: "import_data", withData: ["title": "default"])
AnalyticsManager.shared.track(.importTapped)
}, label: {
Text("Import")
.foregroundColor(textColor)