From 156a0eb5f02b2a80100c5cbc9e92f1b93971f831 Mon Sep 17 00:00:00 2001 From: Charles Date: Tue, 23 Dec 2025 16:32:41 +0000 Subject: [PATCH 1/6] fix: handle undefined userId in useFriends PostHog events Default to empty string when userId is undefined to satisfy JsonType requirements for PostHog capture properties. --- frontend/app/_layout.tsx | 51 +++++++++++++------------ frontend/app/capture/details.tsx | 11 +++++- frontend/bun.lock | 5 +++ frontend/hooks/use-friends.ts | 14 ++++++- frontend/hooks/use-search.ts | 8 ++++ frontend/package.json | 1 + frontend/providers/posthog-provider.tsx | 15 ++++++++ 7 files changed, 78 insertions(+), 27 deletions(-) create mode 100644 frontend/providers/posthog-provider.tsx diff --git a/frontend/app/_layout.tsx b/frontend/app/_layout.tsx index fc5d6c6..0257c7b 100644 --- a/frontend/app/_layout.tsx +++ b/frontend/app/_layout.tsx @@ -5,6 +5,7 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { QueryProvider } from '@/providers/query-provider'; import { AuthProvider } from '@/providers/auth-provider'; import { ToastProvider } from '@/providers/toast-provider'; +import { AppPostHogProvider } from '@/providers/posthog-provider'; import { useDeepLinking } from '@/hooks/use-deep-linking'; import { Host } from 'react-native-portalize'; @@ -26,30 +27,32 @@ export default function RootLayout() { return ( - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + ); diff --git a/frontend/app/capture/details.tsx b/frontend/app/capture/details.tsx index f8f5dcb..389397c 100644 --- a/frontend/app/capture/details.tsx +++ b/frontend/app/capture/details.tsx @@ -26,6 +26,7 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { Colors } from '@/lib/constants'; import AudioEntry from '@/components/audio/audio-entry'; import EntryShareList from '@/components/friends/entry-share-list'; +import { usePostHog } from 'posthog-react-native'; interface Friend { id: string; @@ -53,6 +54,7 @@ export default function DetailsScreen() { const { addOptimisticEntry, replaceOptimisticEntry } = useUserEntries(); const { settings: privacySettings } = usePrivacySettings(); const { location } = useDeviceLocation(); + const posthog = usePostHog(); const showEveryoneDefault = privacySettings[PrivacySettings.AUTO_SHARE] ?? false; const showPrivateDefault = !showEveryoneDefault; @@ -92,7 +94,6 @@ export default function DetailsScreen() { const [showEditorPopover, setShowEditorPopover] = useState(false); - const player = useVideoPlayer(uri as string, player => { player.loop = false; // Don't auto-play video - let user control playback @@ -171,6 +172,14 @@ export default function DetailsScreen() { ? [location.city, location.region ?? location.country].filter(Boolean).join(', ') : null; + posthog?.capture('entry_captured', { + user_id: user.id, + entry_type: capture.type, + shared_with_count: selectedFriends.length, + is_private: isPrivate, + is_everyone: isEveryone + }); + // Create optimistic entry for immediate UI update const optimisticEntry = { id: tempId, diff --git a/frontend/bun.lock b/frontend/bun.lock index 6adfd84..ae63140 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -53,6 +53,7 @@ "expo-video": "~3.0.11", "expo-web-browser": "~15.0.7", "lucide-react-native": "^0.475.0", + "posthog-react-native": "^4.17.0", "react": "19.1.0", "react-dom": "19.1.0", "react-native": "0.81.4", @@ -407,6 +408,8 @@ "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + "@posthog/core": ["@posthog/core@1.9.0", "", { "dependencies": { "cross-spawn": "^7.0.6" } }, "sha512-j7KSWxJTUtNyKynLt/p0hfip/3I46dWU2dk+pt7dKRoz2l5CYueHuHK4EO7Wlgno5yo1HO4sc4s30MXMTICHJw=="], + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], @@ -1539,6 +1542,8 @@ "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + "posthog-react-native": ["posthog-react-native@4.17.0", "", { "dependencies": { "@posthog/core": "1.9.0" }, "peerDependencies": { "@react-native-async-storage/async-storage": ">=1.0.0", "@react-navigation/native": ">= 5.0.0", "expo-application": ">= 4.0.0", "expo-device": ">= 4.0.0", "expo-file-system": ">= 13.0.0", "expo-localization": ">= 11.0.0", "posthog-react-native-session-replay": ">= 1.2.0", "react-native-device-info": ">= 10.0.0", "react-native-localize": ">= 3.0.0", "react-native-navigation": ">= 6.0.0", "react-native-safe-area-context": ">= 4.0.0", "react-native-svg": ">= 15.0.0" }, "optionalPeers": ["@react-native-async-storage/async-storage", "@react-navigation/native", "expo-application", "expo-device", "expo-file-system", "expo-localization", "posthog-react-native-session-replay", "react-native-device-info", "react-native-localize", "react-native-navigation", "react-native-safe-area-context"] }, "sha512-9NLhqNjI7JymjeZ8Nkf8sZi1bejBK+tzgxaI50b+b+bm7MRAmA7AnB3KR15u6G5f1OAu05ewy/qOe8Q2MgJwzA=="], + "pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="], "pretty-format": ["pretty-format@30.0.5", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw=="], diff --git a/frontend/hooks/use-friends.ts b/frontend/hooks/use-friends.ts index 32a726d..caed278 100644 --- a/frontend/hooks/use-friends.ts +++ b/frontend/hooks/use-friends.ts @@ -6,6 +6,7 @@ import { Database } from '@/types/database'; import { deviceStorage } from '@/services/device-storage'; import { FriendService } from '@/services/friend-service'; import { useAuthContext } from '@/providers/auth-provider'; +import { usePostHog } from 'posthog-react-native'; type Friendship = Database['public']['Tables']['friendships']['Row']; type Profile = Database['public']['Tables']['profiles']['Row']; @@ -33,6 +34,7 @@ interface UseFriendsResult { export function useFriends(userId?: string): UseFriendsResult { const queryClient = useQueryClient(); const { profile } = useAuthContext(); + const posthog = usePostHog(); const { data: friendships = [], @@ -199,6 +201,10 @@ export function useFriends(userId?: string): UseFriendsResult { const acceptFriendRequest = useCallback(async (friendshipId: string) => { try { await updateFriendshipMutation.mutateAsync({ id: friendshipId, status: FRIENDSHIP_STATUS.ACCEPTED }); + posthog?.capture('invite_accepted', { + user_id: userId ?? '', + friendship_id: friendshipId + }); return { success: true }; } catch (error) { return { @@ -206,7 +212,7 @@ export function useFriends(userId?: string): UseFriendsResult { error: error instanceof Error ? error.message : 'Failed to accept friend request' }; } - }, [updateFriendshipMutation]); + }, [updateFriendshipMutation, userId]); const declineFriendRequest = useCallback(async (friendshipId: string) => { try { @@ -223,6 +229,10 @@ export function useFriends(userId?: string): UseFriendsResult { const blockFriend = useCallback(async (friendshipId: string) => { try { await updateFriendshipMutation.mutateAsync({ id: friendshipId, status: FRIENDSHIP_STATUS.BLOCKED }); + posthog?.capture('user_blocked', { + user_id: userId ?? '', + friendship_id: friendshipId + }); return { success: true }; } catch (error) { return { @@ -230,7 +240,7 @@ export function useFriends(userId?: string): UseFriendsResult { error: error instanceof Error ? error.message : 'Failed to block friend', }; } - }, [updateFriendshipMutation]); + }, [updateFriendshipMutation, userId]); const unblockFriend = useCallback(async (friendshipId: string) => { try { diff --git a/frontend/hooks/use-search.ts b/frontend/hooks/use-search.ts index b1bc7b4..4acb6b5 100644 --- a/frontend/hooks/use-search.ts +++ b/frontend/hooks/use-search.ts @@ -25,9 +25,12 @@ interface UseSearchResult { reset: () => void; } +import { usePostHog } from 'posthog-react-native'; + export function useSearch(params: UseSearchParams = {}): UseSearchResult { const { initialMessages = [], onFinish } = params; const { user } = useAuthContext(); + const posthog = usePostHog(); const [messages, setMessages] = useState(() => [...initialMessages]); const [isLoading, setIsLoading] = useState(false); @@ -57,6 +60,11 @@ export function useSearch(params: UseSearchParams = {}): UseSearchResult { const query = overrideQuery ?? input.trim(); if (!query) return; + posthog?.capture('ai_search', { + query: query, + user_id: user.id + }); + // Cancel any in-flight request if (abortRef.current) { abortRef.current.abort(); diff --git a/frontend/package.json b/frontend/package.json index 29f0be6..50e8a98 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -67,6 +67,7 @@ "expo-video": "~3.0.11", "expo-web-browser": "~15.0.7", "lucide-react-native": "^0.475.0", + "posthog-react-native": "^4.17.0", "react": "19.1.0", "react-dom": "19.1.0", "react-native": "0.81.4", diff --git a/frontend/providers/posthog-provider.tsx b/frontend/providers/posthog-provider.tsx new file mode 100644 index 0000000..627c11b --- /dev/null +++ b/frontend/providers/posthog-provider.tsx @@ -0,0 +1,15 @@ +import { PostHogProvider } from 'posthog-react-native' +import Constants from 'expo-constants' + +export function AppPostHogProvider({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} From b7768e8f5288112b905e7a53479f350dbf42c0f7 Mon Sep 17 00:00:00 2001 From: Charles Date: Tue, 23 Dec 2025 17:10:44 +0000 Subject: [PATCH 2/6] feat(analytics): integrate PostHog for core user actions - Initialize PostHog client in [constants/posthog.ts](cci:7://file:///c:/Users/charl/keepsafe/frontend/constants/posthog.ts:0:0-0:0) - Track `invite_accepted` event in `use-invite-acceptance` hook - Track `friend_blocked` event in `use-friends` hook - Track `entry_captured` event with metadata in `capture/details` screen --- frontend/app.json | 3 +- frontend/app/_layout.tsx | 51 ++++++++++++------------- frontend/app/capture/details.tsx | 18 ++++----- frontend/bun.lock | 20 +++++++--- frontend/constants/posthog.ts | 5 +++ frontend/hooks/use-friends.ts | 16 ++------ frontend/hooks/use-invite-acceptance.ts | 6 +++ frontend/hooks/use-search.ts | 8 ---- frontend/package.json | 6 ++- frontend/providers/posthog-provider.tsx | 15 -------- 10 files changed, 68 insertions(+), 80 deletions(-) create mode 100644 frontend/constants/posthog.ts delete mode 100644 frontend/providers/posthog-provider.tsx diff --git a/frontend/app.json b/frontend/app.json index 56b31ed..4b9a56d 100644 --- a/frontend/app.json +++ b/frontend/app.json @@ -122,7 +122,8 @@ "microphonePermission": "Allow Keepsafe to access your microphone." } ], - "expo-background-task" + "expo-background-task", + "expo-localization" ], "experiments": { "typedRoutes": true diff --git a/frontend/app/_layout.tsx b/frontend/app/_layout.tsx index 0257c7b..fc5d6c6 100644 --- a/frontend/app/_layout.tsx +++ b/frontend/app/_layout.tsx @@ -5,7 +5,6 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { QueryProvider } from '@/providers/query-provider'; import { AuthProvider } from '@/providers/auth-provider'; import { ToastProvider } from '@/providers/toast-provider'; -import { AppPostHogProvider } from '@/providers/posthog-provider'; import { useDeepLinking } from '@/hooks/use-deep-linking'; import { Host } from 'react-native-portalize'; @@ -27,32 +26,30 @@ export default function RootLayout() { return ( - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + ); diff --git a/frontend/app/capture/details.tsx b/frontend/app/capture/details.tsx index 389397c..a3acead 100644 --- a/frontend/app/capture/details.tsx +++ b/frontend/app/capture/details.tsx @@ -10,6 +10,7 @@ import { useUserEntries } from '@/hooks/use-user-entries'; import { usePrivacySettings } from '@/hooks/use-privacy-settings'; import { PrivacySettings } from '@/types/privacy'; import { MediaCapture } from '@/types/media'; +import { posthog } from '@/constants/posthog'; import { moderateScale, verticalScale } from 'react-native-size-matters'; import * as Crypto from 'expo-crypto'; @@ -26,7 +27,6 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { Colors } from '@/lib/constants'; import AudioEntry from '@/components/audio/audio-entry'; import EntryShareList from '@/components/friends/entry-share-list'; -import { usePostHog } from 'posthog-react-native'; interface Friend { id: string; @@ -54,7 +54,6 @@ export default function DetailsScreen() { const { addOptimisticEntry, replaceOptimisticEntry } = useUserEntries(); const { settings: privacySettings } = usePrivacySettings(); const { location } = useDeviceLocation(); - const posthog = usePostHog(); const showEveryoneDefault = privacySettings[PrivacySettings.AUTO_SHARE] ?? false; const showPrivateDefault = !showEveryoneDefault; @@ -94,6 +93,7 @@ export default function DetailsScreen() { const [showEditorPopover, setShowEditorPopover] = useState(false); + const player = useVideoPlayer(uri as string, player => { player.loop = false; // Don't auto-play video - let user control playback @@ -172,14 +172,6 @@ export default function DetailsScreen() { ? [location.city, location.region ?? location.country].filter(Boolean).join(', ') : null; - posthog?.capture('entry_captured', { - user_id: user.id, - entry_type: capture.type, - shared_with_count: selectedFriends.length, - is_private: isPrivate, - is_everyone: isEveryone - }); - // Create optimistic entry for immediate UI update const optimisticEntry = { id: tempId, @@ -224,6 +216,12 @@ export default function DetailsScreen() { }); if (result.success) { + posthog.capture('entry_captured', { + type: capture.type, + is_private: isPrivate, + is_everyone: isEveryone, + friends_count: selectedFriends.length + }); toast(result.message, 'success'); setTimeout(() => { router.push('/capture'); diff --git a/frontend/bun.lock b/frontend/bun.lock index ae63140..97dad3c 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -21,6 +21,7 @@ "axios": "^1.12.2", "date-fns": "^4.1.0", "expo": "54", + "expo-application": "~7.0.8", "expo-audio": "~1.0.13", "expo-av": "~16.0.7", "expo-background-task": "~1.0.8", @@ -31,15 +32,16 @@ "expo-contacts": "~15.0.8", "expo-crypto": "~15.0.7", "expo-dev-client": "~6.0.12", - "expo-device": "^8.0.8", + "expo-device": "~8.0.10", "expo-document-picker": "~14.0.7", - "expo-file-system": "~19.0.12", + "expo-file-system": "~19.0.21", "expo-font": "~14.0.8", "expo-haptics": "~15.0.7", "expo-image": "~3.0.8", "expo-image-picker": "~17.0.8", "expo-linear-gradient": "~15.0.7", "expo-linking": "~8.0.8", + "expo-localization": "~17.0.8", "expo-location": "~19.0.7", "expo-notifications": "~0.32.11", "expo-router": "~6.0.1", @@ -958,7 +960,7 @@ "expo": ["expo@54.0.2", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "54.0.2", "@expo/config": "~12.0.8", "@expo/config-plugins": "~54.0.1", "@expo/devtools": "0.1.7", "@expo/fingerprint": "0.15.0", "@expo/metro": "~0.1.1", "@expo/metro-config": "54.0.2", "@expo/vector-icons": "^15.0.2", "@ungap/structured-clone": "^1.3.0", "babel-preset-expo": "~54.0.0", "expo-asset": "~12.0.8", "expo-constants": "~18.0.8", "expo-file-system": "~19.0.12", "expo-font": "~14.0.8", "expo-keep-awake": "~15.0.7", "expo-modules-autolinking": "3.0.10", "expo-modules-core": "3.0.15", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", "whatwg-url-without-unicode": "8.0.0-3" }, "peerDependencies": { "@expo/dom-webview": "*", "@expo/metro-runtime": "*", "react": "*", "react-native": "*", "react-native-webview": "*" }, "optionalPeers": ["@expo/dom-webview", "@expo/metro-runtime", "react-native-webview"], "bin": { "expo": "bin/cli", "fingerprint": "bin/fingerprint", "expo-modules-autolinking": "bin/autolinking" } }, "sha512-0YcsvMuiZlVFVZRdD4PlhwsYrTmcN1qm5b1IRSLIeLZjH6ZnQwBb3KBSnK8WRC9V6EUPSbuLGpNT5AEewrtakQ=="], - "expo-application": ["expo-application@7.0.7", "", { "peerDependencies": { "expo": "*" } }, "sha512-Jt1/qqnoDUbZ+bK91+dHaZ1vrPDtRBOltRa681EeedkisqguuEeUx4UHqwVyDK2oHWsK6lO3ojetoA4h8OmNcg=="], + "expo-application": ["expo-application@7.0.8", "", { "peerDependencies": { "expo": "*" } }, "sha512-qFGyxk7VJbrNOQWBbE09XUuGuvkOgFS9QfToaK2FdagM2aQ+x3CvGV2DuVgl/l4ZxPgIf3b/MNh9xHpwSwn74Q=="], "expo-asset": ["expo-asset@12.0.8", "", { "dependencies": { "@expo/image-utils": "^0.8.7", "expo-constants": "~18.0.8" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-jj2U8zw9+7orST2rlQGULYiqPoECOuUyffs2NguGrq84bYbkM041T7TOMXH2raPVJnM9lEAP54ezI6XL+GVYqw=="], @@ -988,13 +990,13 @@ "expo-dev-menu-interface": ["expo-dev-menu-interface@2.0.0", "", { "peerDependencies": { "expo": "*" } }, "sha512-BvAMPt6x+vyXpThsyjjOYyjwfjREV4OOpQkZ0tNl+nGpsPfcY9mc6DRACoWnH9KpLzyIt3BOgh3cuy/h/OxQjw=="], - "expo-device": ["expo-device@8.0.8", "", { "dependencies": { "ua-parser-js": "^0.7.33" }, "peerDependencies": { "expo": "*" } }, "sha512-t515WOkeVgIeO3izj+FoXodKTHiSxZ2uF5E9YvCwiR4kANAjvyjFP3vSls2Utjx5ss8y652pZTgh3tOYQmwuZA=="], + "expo-device": ["expo-device@8.0.10", "", { "dependencies": { "ua-parser-js": "^0.7.33" }, "peerDependencies": { "expo": "*" } }, "sha512-jd5BxjaF7382JkDMaC+P04aXXknB2UhWaVx5WiQKA05ugm/8GH5uaz9P9ckWdMKZGQVVEOC8MHaUADoT26KmFA=="], "expo-document-picker": ["expo-document-picker@14.0.7", "", { "peerDependencies": { "expo": "*" } }, "sha512-81Jh8RDD0GYBUoSTmIBq30hXXjmkDV1ZY2BNIp1+3HR5PDSh2WmdhD/Ezz5YFsv46hIXHsQc+Kh1q8vn6OLT9Q=="], "expo-eas-client": ["expo-eas-client@1.0.8", "", {}, "sha512-5or11NJhSeDoHHI6zyvQDW2cz/yFyE+1Cz8NTs5NK8JzC7J0JrkUgptWtxyfB6Xs/21YRNifd3qgbBN3hfKVgA=="], - "expo-file-system": ["expo-file-system@19.0.12", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-gqpxpnjfhzXLcqMOi49isB5S1Af49P9410fsaFfnLZWN3X6Dwc8EplDwbaolOI/wnGwP81P+/nDn5RNmU6m7mQ=="], + "expo-file-system": ["expo-file-system@19.0.21", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-s3DlrDdiscBHtab/6W1osrjGL+C2bvoInPJD7sOwmxfJ5Woynv2oc+Fz1/xVXaE/V7HE/+xrHC/H45tu6lZzzg=="], "expo-font": ["expo-font@14.0.8", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-bTUHaJWRZ7ywP8dg3f+wfOwv6RwMV3mWT2CDUIhsK70GjNGlCtiWOCoHsA5Od/esPaVxqc37cCBvQGQRFStRlA=="], @@ -1014,6 +1016,8 @@ "expo-linking": ["expo-linking@8.0.8", "", { "dependencies": { "expo-constants": "~18.0.8", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-MyeMcbFDKhXh4sDD1EHwd0uxFQNAc6VCrwBkNvvvufUsTYFq3glTA9Y8a+x78CPpjNqwNAamu74yIaIz7IEJyg=="], + "expo-localization": ["expo-localization@17.0.8", "", { "dependencies": { "rtl-detect": "^1.0.2" }, "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-UrdwklZBDJ+t+ZszMMiE0SXZ2eJxcquCuQcl6EvGHM9K+e6YqKVRQ+w8qE+iIB3H75v2RJy6MHAaLK+Mqeo04g=="], + "expo-location": ["expo-location@19.0.7", "", { "peerDependencies": { "expo": "*" } }, "sha512-YNkh4r9E6ECbPkBCAMG5A5yHDgS0pw+Rzyd0l2ZQlCtjkhlODB55nMCKr5CZnUI0mXTkaSm8CwfoCO8n2MpYfg=="], "expo-manifests": ["expo-manifests@1.0.8", "", { "dependencies": { "@expo/config": "~12.0.8", "expo-json-utils": "~0.15.0" }, "peerDependencies": { "expo": "*" } }, "sha512-nA5PwU2uiUd+2nkDWf9e71AuFAtbrb330g/ecvuu52bmaXtN8J8oiilc9BDvAX0gg2fbtOaZdEdjBYopt1jdlQ=="], @@ -1688,6 +1692,8 @@ "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": "bin.js" }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + "rtl-detect": ["rtl-detect@1.1.2", "", {}, "sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], @@ -2142,6 +2148,8 @@ "expo/@expo/config": ["@expo/config@12.0.8", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "@expo/config-plugins": "~54.0.0", "@expo/config-types": "^54.0.7", "@expo/json-file": "^10.0.7", "deepmerge": "^4.3.1", "getenv": "^2.0.0", "glob": "^10.4.2", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0", "resolve-workspace-root": "^2.0.0", "semver": "^7.6.0", "slugify": "^1.3.4", "sucrase": "3.35.0" } }, "sha512-yFadXa5Cmja57EVOSyEYV1hF7kCaSbPnd1twx0MfvTr1Yj2abIbrEu2MUZqcvElNQOtgADnLRP0YJiuEdgoO5A=="], + "expo/expo-file-system": ["expo-file-system@19.0.12", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-gqpxpnjfhzXLcqMOi49isB5S1Af49P9410fsaFfnLZWN3X6Dwc8EplDwbaolOI/wnGwP81P+/nDn5RNmU6m7mQ=="], + "expo/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], "expo-constants/@expo/config": ["@expo/config@12.0.8", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "@expo/config-plugins": "~54.0.0", "@expo/config-types": "^54.0.7", "@expo/json-file": "^10.0.7", "deepmerge": "^4.3.1", "getenv": "^2.0.0", "glob": "^10.4.2", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0", "resolve-workspace-root": "^2.0.0", "semver": "^7.6.0", "slugify": "^1.3.4", "sucrase": "3.35.0" } }, "sha512-yFadXa5Cmja57EVOSyEYV1hF7kCaSbPnd1twx0MfvTr1Yj2abIbrEu2MUZqcvElNQOtgADnLRP0YJiuEdgoO5A=="], @@ -2152,6 +2160,8 @@ "expo-modules-autolinking/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + "expo-notifications/expo-application": ["expo-application@7.0.7", "", { "peerDependencies": { "expo": "*" } }, "sha512-Jt1/qqnoDUbZ+bK91+dHaZ1vrPDtRBOltRa681EeedkisqguuEeUx4UHqwVyDK2oHWsK6lO3ojetoA4h8OmNcg=="], + "expo-router/@expo/metro-runtime": ["@expo/metro-runtime@6.1.1", "", { "dependencies": { "anser": "^1.4.9", "pretty-format": "^29.7.0", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-dom": "*", "react-native": "*" }, "optionalPeers": ["react-dom"] }, "sha512-H5ZFj7nisMJ5a4joMGpF4Xt/m4hWDAroMNv5ld/2iniWoXLvNt+YQpMdyecu/lHpydKAjHzXcyE08hTGgURaIA=="], "expo-router/semver": ["semver@7.6.3", "", { "bin": "bin/semver.js" }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], diff --git a/frontend/constants/posthog.ts b/frontend/constants/posthog.ts new file mode 100644 index 0000000..cbe46ac --- /dev/null +++ b/frontend/constants/posthog.ts @@ -0,0 +1,5 @@ +import { PostHog } from 'posthog-react-native'; + +export const posthog = new PostHog(process.env.EXPO_PUBLIC_POSTHOG_API_KEY!, { + host: process.env.EXPO_PUBLIC_POSTHOG_HOST!, +}); \ No newline at end of file diff --git a/frontend/hooks/use-friends.ts b/frontend/hooks/use-friends.ts index caed278..58c4b47 100644 --- a/frontend/hooks/use-friends.ts +++ b/frontend/hooks/use-friends.ts @@ -6,7 +6,7 @@ import { Database } from '@/types/database'; import { deviceStorage } from '@/services/device-storage'; import { FriendService } from '@/services/friend-service'; import { useAuthContext } from '@/providers/auth-provider'; -import { usePostHog } from 'posthog-react-native'; +import { posthog } from '@/constants/posthog'; type Friendship = Database['public']['Tables']['friendships']['Row']; type Profile = Database['public']['Tables']['profiles']['Row']; @@ -34,7 +34,6 @@ interface UseFriendsResult { export function useFriends(userId?: string): UseFriendsResult { const queryClient = useQueryClient(); const { profile } = useAuthContext(); - const posthog = usePostHog(); const { data: friendships = [], @@ -201,10 +200,6 @@ export function useFriends(userId?: string): UseFriendsResult { const acceptFriendRequest = useCallback(async (friendshipId: string) => { try { await updateFriendshipMutation.mutateAsync({ id: friendshipId, status: FRIENDSHIP_STATUS.ACCEPTED }); - posthog?.capture('invite_accepted', { - user_id: userId ?? '', - friendship_id: friendshipId - }); return { success: true }; } catch (error) { return { @@ -212,7 +207,7 @@ export function useFriends(userId?: string): UseFriendsResult { error: error instanceof Error ? error.message : 'Failed to accept friend request' }; } - }, [updateFriendshipMutation, userId]); + }, [updateFriendshipMutation]); const declineFriendRequest = useCallback(async (friendshipId: string) => { try { @@ -229,10 +224,7 @@ export function useFriends(userId?: string): UseFriendsResult { const blockFriend = useCallback(async (friendshipId: string) => { try { await updateFriendshipMutation.mutateAsync({ id: friendshipId, status: FRIENDSHIP_STATUS.BLOCKED }); - posthog?.capture('user_blocked', { - user_id: userId ?? '', - friendship_id: friendshipId - }); + posthog.capture('friend_blocked', { friendship_id: friendshipId }); return { success: true }; } catch (error) { return { @@ -240,7 +232,7 @@ export function useFriends(userId?: string): UseFriendsResult { error: error instanceof Error ? error.message : 'Failed to block friend', }; } - }, [updateFriendshipMutation, userId]); + }, [updateFriendshipMutation]); const unblockFriend = useCallback(async (friendshipId: string) => { try { diff --git a/frontend/hooks/use-invite-acceptance.ts b/frontend/hooks/use-invite-acceptance.ts index b42fbe4..1a1e598 100644 --- a/frontend/hooks/use-invite-acceptance.ts +++ b/frontend/hooks/use-invite-acceptance.ts @@ -2,6 +2,7 @@ import { useState, useCallback } from 'react'; import { useMutation, useQuery } from '@tanstack/react-query'; import { supabase } from '@/lib/supabase'; import { TABLES, FRIENDSHIP_STATUS } from '@/constants/supabase'; +import { posthog } from '@/constants/posthog'; export interface InviteData { id: string; @@ -130,6 +131,11 @@ export function useInviteAcceptance(inviteId?: string): UseInviteAcceptanceResul userId: userId }); + posthog.capture('invite_accepted', { + invitee_id: inviteeId, + inviter_id: result.inviterName // Note: this might be name, not ID, based on line 73. Ideally we'd have ID. + }); + return { success: true, message: 'Invitation accepted successfully!', diff --git a/frontend/hooks/use-search.ts b/frontend/hooks/use-search.ts index 4acb6b5..b1bc7b4 100644 --- a/frontend/hooks/use-search.ts +++ b/frontend/hooks/use-search.ts @@ -25,12 +25,9 @@ interface UseSearchResult { reset: () => void; } -import { usePostHog } from 'posthog-react-native'; - export function useSearch(params: UseSearchParams = {}): UseSearchResult { const { initialMessages = [], onFinish } = params; const { user } = useAuthContext(); - const posthog = usePostHog(); const [messages, setMessages] = useState(() => [...initialMessages]); const [isLoading, setIsLoading] = useState(false); @@ -60,11 +57,6 @@ export function useSearch(params: UseSearchParams = {}): UseSearchResult { const query = overrideQuery ?? input.trim(); if (!query) return; - posthog?.capture('ai_search', { - query: query, - user_id: user.id - }); - // Cancel any in-flight request if (abortRef.current) { abortRef.current.abort(); diff --git a/frontend/package.json b/frontend/package.json index 50e8a98..f4ddd3c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -35,6 +35,7 @@ "axios": "^1.12.2", "date-fns": "^4.1.0", "expo": "54", + "expo-application": "~7.0.8", "expo-audio": "~1.0.13", "expo-av": "~16.0.7", "expo-background-task": "~1.0.8", @@ -45,15 +46,16 @@ "expo-contacts": "~15.0.8", "expo-crypto": "~15.0.7", "expo-dev-client": "~6.0.12", - "expo-device": "^8.0.8", + "expo-device": "~8.0.10", "expo-document-picker": "~14.0.7", - "expo-file-system": "~19.0.12", + "expo-file-system": "~19.0.21", "expo-font": "~14.0.8", "expo-haptics": "~15.0.7", "expo-image": "~3.0.8", "expo-image-picker": "~17.0.8", "expo-linear-gradient": "~15.0.7", "expo-linking": "~8.0.8", + "expo-localization": "~17.0.8", "expo-location": "~19.0.7", "expo-notifications": "~0.32.11", "expo-router": "~6.0.1", diff --git a/frontend/providers/posthog-provider.tsx b/frontend/providers/posthog-provider.tsx deleted file mode 100644 index 627c11b..0000000 --- a/frontend/providers/posthog-provider.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { PostHogProvider } from 'posthog-react-native' -import Constants from 'expo-constants' - -export function AppPostHogProvider({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ) -} From dba94ad6bf9a45e5ef36d00976f772a98e10b13d Mon Sep 17 00:00:00 2001 From: Charles Date: Sun, 4 Jan 2026 22:56:06 +0000 Subject: [PATCH 3/6] feat: implement capture details screen for reviewing, editing, and sharing media entries --- frontend/app/capture/details.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/frontend/app/capture/details.tsx b/frontend/app/capture/details.tsx index a3acead..88edd7a 100644 --- a/frontend/app/capture/details.tsx +++ b/frontend/app/capture/details.tsx @@ -216,12 +216,16 @@ export default function DetailsScreen() { }); if (result.success) { - posthog.capture('entry_captured', { - type: capture.type, - is_private: isPrivate, - is_everyone: isEveryone, - friends_count: selectedFriends.length - }); + try { + posthog.capture('entry_captured', { + type: capture.type, + is_private: isPrivate, + is_everyone: isEveryone, + friends_count: selectedFriends.length + }); + } catch (error) { + if (__DEV__) console.warn('Analytics capture failed:', error); + } toast(result.message, 'success'); setTimeout(() => { router.push('/capture'); From 814b42859a50819a8b652e79ed808805d79e0427 Mon Sep 17 00:00:00 2001 From: Charles Date: Sun, 4 Jan 2026 23:02:12 +0000 Subject: [PATCH 4/6] feat: implement friend management and invite acceptance hooks, and integrate PostHog analytics --- frontend/constants/posthog.ts | 27 ++++++++++++++++++++++--- frontend/hooks/use-friends.ts | 7 ++++++- frontend/hooks/use-invite-acceptance.ts | 10 +++++---- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/frontend/constants/posthog.ts b/frontend/constants/posthog.ts index cbe46ac..9e50952 100644 --- a/frontend/constants/posthog.ts +++ b/frontend/constants/posthog.ts @@ -1,5 +1,26 @@ import { PostHog } from 'posthog-react-native'; -export const posthog = new PostHog(process.env.EXPO_PUBLIC_POSTHOG_API_KEY!, { - host: process.env.EXPO_PUBLIC_POSTHOG_HOST!, -}); \ No newline at end of file +const apiKey = process.env.EXPO_PUBLIC_POSTHOG_API_KEY; +const host = process.env.EXPO_PUBLIC_POSTHOG_HOST; + +if (!apiKey || !host) { + if (__DEV__) { + console.warn('PostHog environment variables not configured. Analytics will be disabled.'); + } +} + +export const posthog = apiKey && host + ? new PostHog(apiKey, { host }) + : { + // Provide a no-op mock for when PostHog is not configured + capture: () => {}, + identify: () => {}, + reset: () => {}, + screen: () => {}, + group: () => {}, + alias: () => {}, + reloadFeatureFlags: () => {}, + isFeatureEnabled: () => false, + getFeatureFlag: () => null, + getFeatureFlagPayload: () => null, + } as unknown as PostHog; \ No newline at end of file diff --git a/frontend/hooks/use-friends.ts b/frontend/hooks/use-friends.ts index 1a993f9..8ad1e1a 100644 --- a/frontend/hooks/use-friends.ts +++ b/frontend/hooks/use-friends.ts @@ -158,7 +158,12 @@ export function useFriends(userId?: string): UseFriendsResult { try { await updateFriendshipMutation.mutateAsync({ id: friendshipId, status: FRIENDSHIP_STATUS.BLOCKED }); - posthog.capture('friend_blocked', { friendship_id: friendshipId }); + try { + // Omitting friendship_id for privacy compliance + posthog.capture('friend_blocked', {}); + } catch (error) { + if (__DEV__) console.warn('Analytics capture failed:', error); + } await updateFriendshipMutation.mutateAsync({ id: friendshipId, status: FRIENDSHIP_STATUS.BLOCKED, diff --git a/frontend/hooks/use-invite-acceptance.ts b/frontend/hooks/use-invite-acceptance.ts index 1a1e598..8c7f828 100644 --- a/frontend/hooks/use-invite-acceptance.ts +++ b/frontend/hooks/use-invite-acceptance.ts @@ -131,10 +131,12 @@ export function useInviteAcceptance(inviteId?: string): UseInviteAcceptanceResul userId: userId }); - posthog.capture('invite_accepted', { - invitee_id: inviteeId, - inviter_id: result.inviterName // Note: this might be name, not ID, based on line 73. Ideally we'd have ID. - }); + if (inviteeId && userId) { + posthog.capture('invite_accepted', { + inviter_id: inviteeId, + invitee_id: userId + }); + } return { success: true, From e2ab18401e6f114f4fe48bc78590bfa70a6b1a89 Mon Sep 17 00:00:00 2001 From: Charles Date: Sun, 4 Jan 2026 23:21:09 +0000 Subject: [PATCH 5/6] feat: Add `useFriends` hook for managing friend relationships and integrate PostHog analytics setup. --- frontend/constants/posthog.ts | 10 +++++----- frontend/hooks/use-friends.ts | 11 +++++------ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/frontend/constants/posthog.ts b/frontend/constants/posthog.ts index 9e50952..94c5298 100644 --- a/frontend/constants/posthog.ts +++ b/frontend/constants/posthog.ts @@ -13,12 +13,12 @@ export const posthog = apiKey && host ? new PostHog(apiKey, { host }) : { // Provide a no-op mock for when PostHog is not configured - capture: () => {}, - identify: () => {}, + capture: (_event: string, _properties?: object) => {}, + identify: (_distinctId: string, _properties?: object) => {}, reset: () => {}, - screen: () => {}, - group: () => {}, - alias: () => {}, + screen: (_screenName: string, _properties?: object) => {}, + group: (_groupType: string, _groupKey: string, _properties?: object) => {}, + alias: (_alias: string) => {}, reloadFeatureFlags: () => {}, isFeatureEnabled: () => false, getFeatureFlag: () => null, diff --git a/frontend/hooks/use-friends.ts b/frontend/hooks/use-friends.ts index 8ad1e1a..0bc7609 100644 --- a/frontend/hooks/use-friends.ts +++ b/frontend/hooks/use-friends.ts @@ -157,18 +157,17 @@ export function useFriends(userId?: string): UseFriendsResult { } try { - await updateFriendshipMutation.mutateAsync({ id: friendshipId, status: FRIENDSHIP_STATUS.BLOCKED }); + await updateFriendshipMutation.mutateAsync({ + id: friendshipId, + status: FRIENDSHIP_STATUS.BLOCKED, + blocked_by: userId + }); try { // Omitting friendship_id for privacy compliance posthog.capture('friend_blocked', {}); } catch (error) { if (__DEV__) console.warn('Analytics capture failed:', error); } - await updateFriendshipMutation.mutateAsync({ - id: friendshipId, - status: FRIENDSHIP_STATUS.BLOCKED, - blocked_by: userId - }); return { success: true }; } catch (error) { return { From 7b493577fbb700eb34ff68b31d9b08fadff9c24a Mon Sep 17 00:00:00 2001 From: Charles Date: Sun, 4 Jan 2026 23:29:18 +0000 Subject: [PATCH 6/6] refactor: Simplify PostHog mock methods to return promises and clean up tsconfig.json exclude formatting --- frontend/constants/posthog.ts | 12 ++++++------ frontend/tsconfig.json | 6 +----- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/frontend/constants/posthog.ts b/frontend/constants/posthog.ts index 94c5298..8d830a7 100644 --- a/frontend/constants/posthog.ts +++ b/frontend/constants/posthog.ts @@ -13,12 +13,12 @@ export const posthog = apiKey && host ? new PostHog(apiKey, { host }) : { // Provide a no-op mock for when PostHog is not configured - capture: (_event: string, _properties?: object) => {}, - identify: (_distinctId: string, _properties?: object) => {}, - reset: () => {}, - screen: (_screenName: string, _properties?: object) => {}, - group: (_groupType: string, _groupKey: string, _properties?: object) => {}, - alias: (_alias: string) => {}, + capture: (_event: string, _properties?: object) => Promise.resolve(), + identify: (_distinctId: string, _properties?: object) => Promise.resolve(), + reset: () => Promise.resolve(), + screen: (_screenName: string, _properties?: object) => Promise.resolve(), + group: (_groupType: string, _groupKey: string, _properties?: object) => Promise.resolve(), + alias: (_alias: string) => Promise.resolve(), reloadFeatureFlags: () => {}, isFeatureEnabled: () => false, getFeatureFlag: () => null, diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 39efc9f..b3be46f 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -16,9 +16,5 @@ "expo-env.d.ts", "nativewind-env.d.ts" ], - "exclude": [ - "node_modules", - "**/*.flow.js", - "**/*.flow" - ] + "exclude": ["node_modules", "**/*.flow.js", "**/*.flow"] }