From 8206072b7149e7db4e11c982f176b6bc028da61d Mon Sep 17 00:00:00 2001 From: Trey t Date: Wed, 4 Mar 2026 21:02:47 -0600 Subject: [PATCH] Add PostHog analytics events matching iOS/KMM event names - Define AnalyticsEvents constants matching iOS AnalyticsEvent enum - Track user_signed_in, user_registered with identify/reset - Track residence_created, task_created, contractor_created, document_created - Track residence_shared, theme_changed - All events include platform:'web' for cross-platform filtering - Reset PostHog on logout Co-Authored-By: Claude Opus 4.6 --- src/lib/analytics.ts | 35 ++++++++++++++++++++++++++++++++ src/lib/hooks/use-contractors.ts | 2 ++ src/lib/hooks/use-documents.ts | 9 ++++++-- src/lib/hooks/use-residences.ts | 9 ++++++-- src/lib/hooks/use-sharing.ts | 2 ++ src/lib/hooks/use-tasks.ts | 9 ++++++-- src/stores/auth.ts | 12 +++++++++++ src/stores/theme.ts | 6 +++++- 8 files changed, 77 insertions(+), 7 deletions(-) diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts index ed8294c..7fa740d 100644 --- a/src/lib/analytics.ts +++ b/src/lib/analytics.ts @@ -45,3 +45,38 @@ export function resetAnalytics() { posthog.reset(); } } + +// --------------------------------------------------------------------------- +// Event names — match iOS/KMM AnalyticsEvents for cross-platform consistency +// --------------------------------------------------------------------------- + +export const AnalyticsEvents = { + // Auth + USER_REGISTERED: "user_registered", + USER_SIGNED_IN: "user_signed_in", + + // Residences + RESIDENCE_CREATED: "residence_created", + RESIDENCE_LIMIT_REACHED: "residence_limit_reached", + RESIDENCE_SHARED: "residence_shared", + + // Tasks + TASK_CREATED: "task_created", + + // Contractors + CONTRACTOR_CREATED: "contractor_created", + CONTRACTOR_PAYWALL_SHOWN: "contractor_paywall_shown", + + // Documents + DOCUMENT_CREATED: "document_created", + DOCUMENTS_PAYWALL_SHOWN: "documents_paywall_shown", + + // Sharing + SHARE_RESIDENCE_PAYWALL_SHOWN: "share_residence_paywall_shown", + + // Settings + THEME_CHANGED: "theme_changed", + + // Errors + ERROR_OCCURRED: "error_occurred", +} as const; diff --git a/src/lib/hooks/use-contractors.ts b/src/lib/hooks/use-contractors.ts index 701666e..4939d25 100644 --- a/src/lib/hooks/use-contractors.ts +++ b/src/lib/hooks/use-contractors.ts @@ -2,6 +2,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useDataProvider, useQueryKeyPrefix } from '@/lib/demo/data-provider-context'; +import { trackEvent, AnalyticsEvents } from '@/lib/analytics'; import type { CreateContractorRequest, UpdateContractorRequest } from '@/lib/api/contractors'; // --------------------------------------------------------------------------- @@ -51,6 +52,7 @@ export function useCreateContractor() { mutationFn: (data: CreateContractorRequest) => contractors.create(data), onSuccess: () => { + trackEvent(AnalyticsEvents.CONTRACTOR_CREATED, { platform: 'web' }); queryClient.invalidateQueries({ queryKey: qk('contractors') }); }, }); diff --git a/src/lib/hooks/use-documents.ts b/src/lib/hooks/use-documents.ts index b55908c..128661a 100644 --- a/src/lib/hooks/use-documents.ts +++ b/src/lib/hooks/use-documents.ts @@ -2,7 +2,8 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useDataProvider, useQueryKeyPrefix } from '@/lib/demo/data-provider-context'; -import type { DocumentListParams, CreateDocumentRequest, UpdateDocumentRequest } from '@/lib/api/documents'; +import { trackEvent, AnalyticsEvents } from '@/lib/analytics'; +import type { DocumentListParams, CreateDocumentRequest, UpdateDocumentRequest, DocumentResponse } from '@/lib/api/documents'; // --------------------------------------------------------------------------- // Query hooks @@ -49,7 +50,11 @@ export function useCreateDocument() { } return documents.create(data); }, - onSuccess: () => { + onSuccess: (result: DocumentResponse) => { + trackEvent(AnalyticsEvents.DOCUMENT_CREATED, { + type: result.document_type, + platform: 'web', + }); queryClient.invalidateQueries({ queryKey: qk('documents') }); }, }); diff --git a/src/lib/hooks/use-residences.ts b/src/lib/hooks/use-residences.ts index 8408acc..dedb1eb 100644 --- a/src/lib/hooks/use-residences.ts +++ b/src/lib/hooks/use-residences.ts @@ -2,7 +2,8 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useDataProvider, useQueryKeyPrefix } from '@/lib/demo/data-provider-context'; -import type { CreateResidenceRequest, UpdateResidenceRequest } from '@/lib/api/residences'; +import { trackEvent, AnalyticsEvents } from '@/lib/analytics'; +import type { CreateResidenceRequest, UpdateResidenceRequest, ResidenceResponse } from '@/lib/api/residences'; // --------------------------------------------------------------------------- // Query hooks @@ -39,7 +40,11 @@ export function useCreateResidence() { return useMutation({ mutationFn: (data: CreateResidenceRequest) => residences.create(data), - onSuccess: () => { + onSuccess: (result: ResidenceResponse) => { + trackEvent(AnalyticsEvents.RESIDENCE_CREATED, { + residence_type: result.property_type?.name, + platform: 'web', + }); queryClient.invalidateQueries({ queryKey: qk('residences') }); }, }); diff --git a/src/lib/hooks/use-sharing.ts b/src/lib/hooks/use-sharing.ts index d0c5a1f..eeeaf24 100644 --- a/src/lib/hooks/use-sharing.ts +++ b/src/lib/hooks/use-sharing.ts @@ -2,6 +2,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useDataProvider, useQueryKeyPrefix } from '@/lib/demo/data-provider-context'; +import { trackEvent, AnalyticsEvents } from '@/lib/analytics'; // --------------------------------------------------------------------------- // Query hooks @@ -38,6 +39,7 @@ export function useGenerateShareCode(residenceId: number) { return useMutation({ mutationFn: () => sharing.generateShareCode(residenceId), onSuccess: () => { + trackEvent(AnalyticsEvents.RESIDENCE_SHARED, { method: 'code', platform: 'web' }); queryClient.invalidateQueries({ queryKey: qk('residences', residenceId, 'share-code') }); }, }); diff --git a/src/lib/hooks/use-tasks.ts b/src/lib/hooks/use-tasks.ts index 0fccf23..03e9d18 100644 --- a/src/lib/hooks/use-tasks.ts +++ b/src/lib/hooks/use-tasks.ts @@ -2,7 +2,8 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useDataProvider, useQueryKeyPrefix } from '@/lib/demo/data-provider-context'; -import type { CreateTaskRequest, UpdateTaskRequest, CreateCompletionRequest } from '@/lib/api/tasks'; +import { trackEvent, AnalyticsEvents } from '@/lib/analytics'; +import type { CreateTaskRequest, UpdateTaskRequest, CreateCompletionRequest, TaskResponse } from '@/lib/api/tasks'; // --------------------------------------------------------------------------- // Query hooks @@ -67,7 +68,11 @@ export function useCreateTask() { const qk = useQueryKeyPrefix(); return useMutation({ mutationFn: (data: CreateTaskRequest) => tasks.create(data), - onSuccess: () => { + onSuccess: (result: TaskResponse) => { + trackEvent(AnalyticsEvents.TASK_CREATED, { + residence_id: result.residence_id, + platform: 'web', + }); queryClient.invalidateQueries({ queryKey: qk('tasks') }); queryClient.invalidateQueries({ queryKey: qk('residences') }); }, diff --git a/src/stores/auth.ts b/src/stores/auth.ts index 7ecd903..5ea7d9c 100644 --- a/src/stores/auth.ts +++ b/src/stores/auth.ts @@ -1,6 +1,7 @@ import { create } from 'zustand'; import * as authApi from '@/lib/api/auth'; import { getQueryClient } from '@/lib/query/query-client'; +import { trackEvent, identifyUser, resetAnalytics, AnalyticsEvents } from '@/lib/analytics'; import type { UserResponse } from '@/lib/api/auth'; interface AuthState { @@ -32,6 +33,11 @@ export const useAuthStore = create()((set) => ({ set({ isLoading: true, error: null }); try { const response = await authApi.login(credentials); + identifyUser(String(response.user.id), { + email: response.user.email, + name: `${response.user.first_name} ${response.user.last_name}`.trim(), + }); + trackEvent(AnalyticsEvents.USER_SIGNED_IN, { method: 'email', platform: 'web' }); set({ user: response.user, isAuthenticated: true, isLoading: false }); window.location.href = '/app'; } catch (err) { @@ -45,6 +51,7 @@ export const useAuthStore = create()((set) => ({ set({ isLoading: true, error: null }); try { await authApi.register(data); + trackEvent(AnalyticsEvents.USER_REGISTERED, { method: 'email', platform: 'web' }); set({ isLoading: false }); } catch (err) { const message = @@ -65,6 +72,7 @@ export const useAuthStore = create()((set) => ({ } finally { // Clear React Query cache to prevent stale data leaking into demo mode getQueryClient().clear(); + resetAnalytics(); set({ user: null, isAuthenticated: false, @@ -79,6 +87,10 @@ export const useAuthStore = create()((set) => ({ set({ isLoading: true, error: null }); try { const user = await authApi.getCurrentUser(); + identifyUser(String(user.id), { + email: user.email, + name: `${user.first_name} ${user.last_name}`.trim(), + }); set({ user, isAuthenticated: true, isLoading: false }); } catch { set({ user: null, isAuthenticated: false, isLoading: false }); diff --git a/src/stores/theme.ts b/src/stores/theme.ts index b8c8de4..3e0d200 100644 --- a/src/stores/theme.ts +++ b/src/stores/theme.ts @@ -1,5 +1,6 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; +import { trackEvent, AnalyticsEvents } from "@/lib/analytics"; export type ColorMode = "light" | "dark" | "system"; @@ -12,7 +13,10 @@ export const useThemeStore = create()( persist( (set) => ({ mode: "light", - setMode: (mode: ColorMode) => set({ mode }), + setMode: (mode: ColorMode) => { + trackEvent(AnalyticsEvents.THEME_CHANGED, { theme: mode, platform: 'web' }); + set({ mode }); + }, }), { name: "casera-theme",