Add PostHog analytics integration for Android and iOS
Implement comprehensive analytics tracking across both platforms: Android (Kotlin): - Add PostHog SDK dependency and initialization in MainActivity - Create expect/actual pattern for cross-platform analytics (commonMain/androidMain/iosMain/jvmMain/jsMain/wasmJsMain) - Track screen views: registration, login, residences, tasks, contractors, documents, notifications, profile - Track key events: user_registered, user_signed_in, residence_created, task_created, contractor_created, document_created - Track paywall events: contractor_paywall_shown, documents_paywall_shown - Track sharing events: residence_shared, contractor_shared - Track theme_changed event iOS (Swift): - Add PostHog iOS SDK via SPM - Create PostHogAnalytics wrapper and AnalyticsEvents constants - Initialize SDK in iOSApp with session replay support - Track same screen views and events as Android - Track user identification after login/registration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,8 @@ import com.example.casera.models.ContractorUpdateRequest
|
||||
import com.example.casera.models.Residence
|
||||
import com.example.casera.network.ApiResult
|
||||
import com.example.casera.repository.LookupsRepository
|
||||
import com.example.casera.analytics.PostHogAnalytics
|
||||
import com.example.casera.analytics.AnalyticsEvents
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -53,8 +55,11 @@ fun AddContractorDialog(
|
||||
var expandedResidenceMenu by remember { mutableStateOf(false) }
|
||||
val contractorSpecialties by LookupsRepository.contractorSpecialties.collectAsState()
|
||||
|
||||
// Load residences for picker
|
||||
// Track screen view and load residences for picker
|
||||
LaunchedEffect(Unit) {
|
||||
if (contractorId == null) {
|
||||
PostHogAnalytics.screen(AnalyticsEvents.NEW_CONTRACTOR_SCREEN_SHOWN)
|
||||
}
|
||||
residenceViewModel.loadResidences()
|
||||
}
|
||||
|
||||
@@ -91,6 +96,7 @@ fun AddContractorDialog(
|
||||
|
||||
LaunchedEffect(createState) {
|
||||
if (createState is ApiResult.Success) {
|
||||
PostHogAnalytics.capture(AnalyticsEvents.CONTRACTOR_CREATED)
|
||||
onContractorSaved()
|
||||
viewModel.resetCreateState()
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ import com.example.casera.models.TaskCategory
|
||||
import com.example.casera.models.TaskCreateRequest
|
||||
import com.example.casera.models.TaskFrequency
|
||||
import com.example.casera.models.TaskPriority
|
||||
import com.example.casera.analytics.PostHogAnalytics
|
||||
import com.example.casera.analytics.AnalyticsEvents
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -110,6 +112,11 @@ fun AddTaskDialog(
|
||||
showSuggestions = false
|
||||
}
|
||||
|
||||
// Track screen view
|
||||
LaunchedEffect(Unit) {
|
||||
PostHogAnalytics.screen(AnalyticsEvents.NEW_TASK_SCREEN_SHOWN)
|
||||
}
|
||||
|
||||
// Set defaults when data loads
|
||||
LaunchedEffect(frequencies) {
|
||||
if (frequencies.isNotEmpty()) {
|
||||
|
||||
@@ -29,6 +29,8 @@ import com.example.casera.network.ApiResult
|
||||
import com.example.casera.repository.LookupsRepository
|
||||
import com.example.casera.ui.subscription.UpgradeFeatureScreen
|
||||
import com.example.casera.utils.SubscriptionHelper
|
||||
import com.example.casera.analytics.PostHogAnalytics
|
||||
import com.example.casera.analytics.AnalyticsEvents
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@@ -58,6 +60,7 @@ fun ContractorsScreen(
|
||||
var isRefreshing by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
PostHogAnalytics.screen(AnalyticsEvents.CONTRACTOR_SCREEN_SHOWN)
|
||||
viewModel.loadContractors()
|
||||
}
|
||||
|
||||
@@ -182,6 +185,10 @@ fun ContractorsScreen(
|
||||
if (canAdd.allowed) {
|
||||
showAddDialog = true
|
||||
} else {
|
||||
PostHogAnalytics.capture(
|
||||
AnalyticsEvents.CONTRACTOR_PAYWALL_SHOWN,
|
||||
mapOf("current_count" to currentCount)
|
||||
)
|
||||
showUpgradeDialog = true
|
||||
}
|
||||
},
|
||||
|
||||
@@ -26,6 +26,8 @@ import com.example.casera.network.ApiResult
|
||||
import com.example.casera.platform.ImageData
|
||||
import com.example.casera.platform.rememberImagePicker
|
||||
import com.example.casera.platform.rememberCameraPicker
|
||||
import com.example.casera.analytics.PostHogAnalytics
|
||||
import com.example.casera.analytics.AnalyticsEvents
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -103,6 +105,17 @@ fun DocumentFormScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// Track screen view
|
||||
LaunchedEffect(Unit) {
|
||||
if (!isEditMode) {
|
||||
val type = if (initialDocumentType == "warranty") "warranty" else "document"
|
||||
PostHogAnalytics.screen(
|
||||
AnalyticsEvents.NEW_DOCUMENT_SCREEN_SHOWN,
|
||||
mapOf("type" to type)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Load residences if needed
|
||||
LaunchedEffect(needsResidenceSelection) {
|
||||
if (needsResidenceSelection) {
|
||||
@@ -148,6 +161,14 @@ fun DocumentFormScreen(
|
||||
// Handle success
|
||||
LaunchedEffect(operationState) {
|
||||
if (operationState is ApiResult.Success) {
|
||||
if (!isEditMode) {
|
||||
// Track document creation
|
||||
val type = if (selectedDocumentType == "warranty") "warranty" else "document"
|
||||
PostHogAnalytics.capture(
|
||||
AnalyticsEvents.DOCUMENT_CREATED,
|
||||
mapOf("type" to type)
|
||||
)
|
||||
}
|
||||
if (isEditMode) {
|
||||
documentViewModel.resetUpdateState()
|
||||
} else {
|
||||
|
||||
@@ -16,6 +16,8 @@ import com.example.casera.ui.subscription.UpgradeFeatureScreen
|
||||
import com.example.casera.utils.SubscriptionHelper
|
||||
import com.example.casera.viewmodel.DocumentViewModel
|
||||
import com.example.casera.models.*
|
||||
import com.example.casera.analytics.PostHogAnalytics
|
||||
import com.example.casera.analytics.AnalyticsEvents
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@@ -47,6 +49,8 @@ fun DocumentsScreen(
|
||||
var showUpgradeDialog by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
// Track screen view
|
||||
PostHogAnalytics.screen(AnalyticsEvents.DOCUMENTS_SCREEN_SHOWN)
|
||||
// Load warranties by default (documentType="warranty")
|
||||
documentViewModel.loadDocuments(
|
||||
residenceId = residenceId,
|
||||
@@ -180,6 +184,10 @@ fun DocumentsScreen(
|
||||
// Pass residenceId even if null - AddDocumentScreen will handle it
|
||||
onNavigateToAddDocument(residenceId ?: -1, documentType)
|
||||
} else {
|
||||
PostHogAnalytics.capture(
|
||||
AnalyticsEvents.DOCUMENTS_PAYWALL_SHOWN,
|
||||
mapOf("current_count" to currentCount)
|
||||
)
|
||||
showUpgradeDialog = true
|
||||
}
|
||||
},
|
||||
|
||||
@@ -26,6 +26,8 @@ import com.example.casera.ui.components.auth.AuthHeader
|
||||
import com.example.casera.ui.components.common.ErrorCard
|
||||
import com.example.casera.viewmodel.AuthViewModel
|
||||
import com.example.casera.network.ApiResult
|
||||
import com.example.casera.analytics.PostHogAnalytics
|
||||
import com.example.casera.analytics.AnalyticsEvents
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@@ -52,6 +54,9 @@ fun LoginScreen(
|
||||
when (loginState) {
|
||||
is ApiResult.Success -> {
|
||||
val user = (loginState as ApiResult.Success).data.user
|
||||
// Track successful login
|
||||
PostHogAnalytics.capture(AnalyticsEvents.USER_SIGNED_IN, mapOf("method" to "email"))
|
||||
PostHogAnalytics.identify(user.id.toString(), mapOf("email" to (user.email ?: ""), "username" to (user.username ?: "")))
|
||||
onLoginSuccess(user)
|
||||
}
|
||||
else -> {}
|
||||
|
||||
@@ -19,6 +19,8 @@ import com.example.casera.ui.theme.AppRadius
|
||||
import com.example.casera.ui.theme.AppSpacing
|
||||
import com.example.casera.util.DateUtils
|
||||
import com.example.casera.viewmodel.NotificationPreferencesViewModel
|
||||
import com.example.casera.analytics.PostHogAnalytics
|
||||
import com.example.casera.analytics.AnalyticsEvents
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@@ -56,8 +58,9 @@ fun NotificationPreferencesScreen(
|
||||
val defaultTaskOverdueLocalHour = 9 // 9 AM local
|
||||
val defaultDailyDigestLocalHour = 8 // 8 AM local
|
||||
|
||||
// Load preferences on first render
|
||||
// Track screen view and load preferences on first render
|
||||
LaunchedEffect(Unit) {
|
||||
PostHogAnalytics.screen(AnalyticsEvents.NOTIFICATION_SETTINGS_SCREEN_SHOWN)
|
||||
viewModel.loadPreferences()
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ import com.example.casera.storage.TokenStorage
|
||||
import com.example.casera.cache.SubscriptionCache
|
||||
import com.example.casera.ui.subscription.UpgradePromptDialog
|
||||
import androidx.compose.runtime.getValue
|
||||
import com.example.casera.analytics.PostHogAnalytics
|
||||
import com.example.casera.analytics.AnalyticsEvents
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@@ -66,8 +68,9 @@ fun ProfileScreen(
|
||||
errorTitle = stringResource(Res.string.profile_update_failed)
|
||||
)
|
||||
|
||||
// Load current user data
|
||||
// Track screen view and load current user data
|
||||
LaunchedEffect(Unit) {
|
||||
PostHogAnalytics.screen(AnalyticsEvents.SETTINGS_SCREEN_SHOWN)
|
||||
val token = TokenStorage.getToken()
|
||||
if (token != null) {
|
||||
val authApi = com.example.casera.network.AuthApi()
|
||||
@@ -528,6 +531,11 @@ fun ProfileScreen(
|
||||
currentTheme = currentTheme,
|
||||
onThemeSelected = { theme ->
|
||||
ThemeManager.setTheme(theme)
|
||||
// Track theme change
|
||||
PostHogAnalytics.capture(
|
||||
AnalyticsEvents.THEME_CHANGED,
|
||||
mapOf("theme" to theme.id)
|
||||
)
|
||||
showThemePicker = false
|
||||
},
|
||||
onDismiss = { showThemePicker = false }
|
||||
|
||||
@@ -19,6 +19,8 @@ import com.example.casera.ui.components.auth.AuthHeader
|
||||
import com.example.casera.ui.components.common.ErrorCard
|
||||
import com.example.casera.viewmodel.AuthViewModel
|
||||
import com.example.casera.network.ApiResult
|
||||
import com.example.casera.analytics.PostHogAnalytics
|
||||
import com.example.casera.analytics.AnalyticsEvents
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@@ -44,9 +46,18 @@ fun RegisterScreen(
|
||||
errorTitle = stringResource(Res.string.error_generic)
|
||||
)
|
||||
|
||||
// Track screen view
|
||||
LaunchedEffect(Unit) {
|
||||
PostHogAnalytics.screen(AnalyticsEvents.REGISTRATION_SCREEN_SHOWN)
|
||||
}
|
||||
|
||||
LaunchedEffect(createState) {
|
||||
when (createState) {
|
||||
is ApiResult.Success -> {
|
||||
// Track successful registration
|
||||
val user = (createState as ApiResult.Success).data.user
|
||||
PostHogAnalytics.capture(AnalyticsEvents.USER_REGISTERED, mapOf("method" to "email"))
|
||||
PostHogAnalytics.identify(user.id.toString(), mapOf("email" to (user.email ?: ""), "username" to (user.username ?: "")))
|
||||
viewModel.resetRegisterState()
|
||||
onRegisterSuccess()
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ import com.example.casera.cache.SubscriptionCache
|
||||
import com.example.casera.data.DataManager
|
||||
import com.example.casera.util.DateUtils
|
||||
import com.example.casera.platform.rememberShareResidence
|
||||
import com.example.casera.analytics.PostHogAnalytics
|
||||
import com.example.casera.analytics.AnalyticsEvents
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@@ -126,6 +128,11 @@ fun ResidenceDetailScreen(
|
||||
LaunchedEffect(taskAddNewTaskState) {
|
||||
when (taskAddNewTaskState) {
|
||||
is ApiResult.Success -> {
|
||||
// Track task creation
|
||||
PostHogAnalytics.capture(
|
||||
AnalyticsEvents.TASK_CREATED,
|
||||
mapOf("residence_id" to residenceId)
|
||||
)
|
||||
showNewTaskDialog = false
|
||||
taskViewModel.resetAddTaskState()
|
||||
residenceViewModel.loadResidenceTasks(residenceId)
|
||||
|
||||
@@ -24,6 +24,8 @@ import com.example.casera.models.ResidenceUser
|
||||
import com.example.casera.network.ApiResult
|
||||
import com.example.casera.network.ResidenceApi
|
||||
import com.example.casera.storage.TokenStorage
|
||||
import com.example.casera.analytics.PostHogAnalytics
|
||||
import com.example.casera.analytics.AnalyticsEvents
|
||||
import casera.composeapp.generated.resources.*
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
@@ -102,6 +104,13 @@ fun ResidenceFormScreen(
|
||||
// Validation errors
|
||||
var nameError by remember { mutableStateOf("") }
|
||||
|
||||
// Track screen view
|
||||
LaunchedEffect(Unit) {
|
||||
if (!isEditMode) {
|
||||
PostHogAnalytics.screen(AnalyticsEvents.NEW_RESIDENCE_SCREEN_SHOWN)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle operation state changes
|
||||
LaunchedEffect(operationState) {
|
||||
when (operationState) {
|
||||
@@ -109,6 +118,10 @@ fun ResidenceFormScreen(
|
||||
if (isEditMode) {
|
||||
viewModel.resetUpdateState()
|
||||
} else {
|
||||
// Track residence created
|
||||
PostHogAnalytics.capture(AnalyticsEvents.RESIDENCE_CREATED, mapOf(
|
||||
"residence_type" to (propertyType?.name ?: "unknown")
|
||||
))
|
||||
viewModel.resetCreateState()
|
||||
}
|
||||
onSuccess()
|
||||
|
||||
@@ -29,6 +29,8 @@ import com.example.casera.network.ApiResult
|
||||
import com.example.casera.utils.SubscriptionHelper
|
||||
import com.example.casera.ui.subscription.UpgradePromptDialog
|
||||
import com.example.casera.cache.SubscriptionCache
|
||||
import com.example.casera.analytics.PostHogAnalytics
|
||||
import com.example.casera.analytics.AnalyticsEvents
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@@ -59,7 +61,9 @@ fun ResidencesScreen(
|
||||
return Pair(check.allowed, check.triggerKey)
|
||||
}
|
||||
|
||||
// Track screen view
|
||||
LaunchedEffect(Unit) {
|
||||
PostHogAnalytics.screen(AnalyticsEvents.RESIDENCE_SCREEN_SHOWN)
|
||||
viewModel.loadMyResidences()
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ import com.example.casera.ui.utils.hexToColor
|
||||
import com.example.casera.viewmodel.TaskCompletionViewModel
|
||||
import com.example.casera.viewmodel.TaskViewModel
|
||||
import com.example.casera.network.ApiResult
|
||||
import com.example.casera.analytics.PostHogAnalytics
|
||||
import com.example.casera.analytics.AnalyticsEvents
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@@ -47,6 +49,7 @@ fun TasksScreen(
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
PostHogAnalytics.screen(AnalyticsEvents.TASK_SCREEN_SHOWN)
|
||||
viewModel.loadTasks()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user