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 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-03-04 21:02:47 -06:00
parent bd9b0ffb34
commit 8206072b71
8 changed files with 77 additions and 7 deletions
+35
View File
@@ -45,3 +45,38 @@ export function resetAnalytics() {
posthog.reset(); 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;
+2
View File
@@ -2,6 +2,7 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useDataProvider, useQueryKeyPrefix } from '@/lib/demo/data-provider-context'; import { useDataProvider, useQueryKeyPrefix } from '@/lib/demo/data-provider-context';
import { trackEvent, AnalyticsEvents } from '@/lib/analytics';
import type { CreateContractorRequest, UpdateContractorRequest } from '@/lib/api/contractors'; import type { CreateContractorRequest, UpdateContractorRequest } from '@/lib/api/contractors';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -51,6 +52,7 @@ export function useCreateContractor() {
mutationFn: (data: CreateContractorRequest) => mutationFn: (data: CreateContractorRequest) =>
contractors.create(data), contractors.create(data),
onSuccess: () => { onSuccess: () => {
trackEvent(AnalyticsEvents.CONTRACTOR_CREATED, { platform: 'web' });
queryClient.invalidateQueries({ queryKey: qk('contractors') }); queryClient.invalidateQueries({ queryKey: qk('contractors') });
}, },
}); });
+7 -2
View File
@@ -2,7 +2,8 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useDataProvider, useQueryKeyPrefix } from '@/lib/demo/data-provider-context'; 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 // Query hooks
@@ -49,7 +50,11 @@ export function useCreateDocument() {
} }
return documents.create(data); return documents.create(data);
}, },
onSuccess: () => { onSuccess: (result: DocumentResponse) => {
trackEvent(AnalyticsEvents.DOCUMENT_CREATED, {
type: result.document_type,
platform: 'web',
});
queryClient.invalidateQueries({ queryKey: qk('documents') }); queryClient.invalidateQueries({ queryKey: qk('documents') });
}, },
}); });
+7 -2
View File
@@ -2,7 +2,8 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useDataProvider, useQueryKeyPrefix } from '@/lib/demo/data-provider-context'; 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 // Query hooks
@@ -39,7 +40,11 @@ export function useCreateResidence() {
return useMutation({ return useMutation({
mutationFn: (data: CreateResidenceRequest) => mutationFn: (data: CreateResidenceRequest) =>
residences.create(data), residences.create(data),
onSuccess: () => { onSuccess: (result: ResidenceResponse) => {
trackEvent(AnalyticsEvents.RESIDENCE_CREATED, {
residence_type: result.property_type?.name,
platform: 'web',
});
queryClient.invalidateQueries({ queryKey: qk('residences') }); queryClient.invalidateQueries({ queryKey: qk('residences') });
}, },
}); });
+2
View File
@@ -2,6 +2,7 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useDataProvider, useQueryKeyPrefix } from '@/lib/demo/data-provider-context'; import { useDataProvider, useQueryKeyPrefix } from '@/lib/demo/data-provider-context';
import { trackEvent, AnalyticsEvents } from '@/lib/analytics';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Query hooks // Query hooks
@@ -38,6 +39,7 @@ export function useGenerateShareCode(residenceId: number) {
return useMutation({ return useMutation({
mutationFn: () => sharing.generateShareCode(residenceId), mutationFn: () => sharing.generateShareCode(residenceId),
onSuccess: () => { onSuccess: () => {
trackEvent(AnalyticsEvents.RESIDENCE_SHARED, { method: 'code', platform: 'web' });
queryClient.invalidateQueries({ queryKey: qk('residences', residenceId, 'share-code') }); queryClient.invalidateQueries({ queryKey: qk('residences', residenceId, 'share-code') });
}, },
}); });
+7 -2
View File
@@ -2,7 +2,8 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useDataProvider, useQueryKeyPrefix } from '@/lib/demo/data-provider-context'; 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 // Query hooks
@@ -67,7 +68,11 @@ export function useCreateTask() {
const qk = useQueryKeyPrefix(); const qk = useQueryKeyPrefix();
return useMutation({ return useMutation({
mutationFn: (data: CreateTaskRequest) => tasks.create(data), 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('tasks') });
queryClient.invalidateQueries({ queryKey: qk('residences') }); queryClient.invalidateQueries({ queryKey: qk('residences') });
}, },
+12
View File
@@ -1,6 +1,7 @@
import { create } from 'zustand'; import { create } from 'zustand';
import * as authApi from '@/lib/api/auth'; import * as authApi from '@/lib/api/auth';
import { getQueryClient } from '@/lib/query/query-client'; import { getQueryClient } from '@/lib/query/query-client';
import { trackEvent, identifyUser, resetAnalytics, AnalyticsEvents } from '@/lib/analytics';
import type { UserResponse } from '@/lib/api/auth'; import type { UserResponse } from '@/lib/api/auth';
interface AuthState { interface AuthState {
@@ -32,6 +33,11 @@ export const useAuthStore = create<AuthState>()((set) => ({
set({ isLoading: true, error: null }); set({ isLoading: true, error: null });
try { try {
const response = await authApi.login(credentials); 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 }); set({ user: response.user, isAuthenticated: true, isLoading: false });
window.location.href = '/app'; window.location.href = '/app';
} catch (err) { } catch (err) {
@@ -45,6 +51,7 @@ export const useAuthStore = create<AuthState>()((set) => ({
set({ isLoading: true, error: null }); set({ isLoading: true, error: null });
try { try {
await authApi.register(data); await authApi.register(data);
trackEvent(AnalyticsEvents.USER_REGISTERED, { method: 'email', platform: 'web' });
set({ isLoading: false }); set({ isLoading: false });
} catch (err) { } catch (err) {
const message = const message =
@@ -65,6 +72,7 @@ export const useAuthStore = create<AuthState>()((set) => ({
} finally { } finally {
// Clear React Query cache to prevent stale data leaking into demo mode // Clear React Query cache to prevent stale data leaking into demo mode
getQueryClient().clear(); getQueryClient().clear();
resetAnalytics();
set({ set({
user: null, user: null,
isAuthenticated: false, isAuthenticated: false,
@@ -79,6 +87,10 @@ export const useAuthStore = create<AuthState>()((set) => ({
set({ isLoading: true, error: null }); set({ isLoading: true, error: null });
try { try {
const user = await authApi.getCurrentUser(); 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 }); set({ user, isAuthenticated: true, isLoading: false });
} catch { } catch {
set({ user: null, isAuthenticated: false, isLoading: false }); set({ user: null, isAuthenticated: false, isLoading: false });
+5 -1
View File
@@ -1,5 +1,6 @@
import { create } from "zustand"; import { create } from "zustand";
import { persist } from "zustand/middleware"; import { persist } from "zustand/middleware";
import { trackEvent, AnalyticsEvents } from "@/lib/analytics";
export type ColorMode = "light" | "dark" | "system"; export type ColorMode = "light" | "dark" | "system";
@@ -12,7 +13,10 @@ export const useThemeStore = create<ThemeState>()(
persist( persist(
(set) => ({ (set) => ({
mode: "light", mode: "light",
setMode: (mode: ColorMode) => set({ mode }), setMode: (mode: ColorMode) => {
trackEvent(AnalyticsEvents.THEME_CHANGED, { theme: mode, platform: 'web' });
set({ mode });
},
}), }),
{ {
name: "casera-theme", name: "casera-theme",