A personal media tracker built on the AT Protocol opnshelf.xyz
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: initial posthog integration

+816 -27
+1
apps/mobile/.gitignore
··· 41 41 # generated native folders 42 42 /ios 43 43 /android 44 + .env
+46
apps/mobile/app.config.js
··· 1 + export default { 2 + expo: { 3 + name: "OpnShelf", 4 + slug: "opnshelf", 5 + version: "1.0.0", 6 + scheme: "opnshelf", 7 + orientation: "default", 8 + icon: "./assets/images/icon.png", 9 + userInterfaceStyle: "light", 10 + newArchEnabled: true, 11 + splash: { 12 + image: "./assets/images/splash-icon.png", 13 + resizeMode: "contain", 14 + backgroundColor: "#0f172a", 15 + }, 16 + ios: { 17 + supportsTablet: true, 18 + bundleIdentifier: "com.rowanpaul.opnshelf", 19 + infoPlist: { 20 + ITSAppUsesNonExemptEncryption: false, 21 + }, 22 + }, 23 + android: { 24 + adaptiveIcon: { 25 + foregroundImage: "./assets/images/adaptive-icon.png", 26 + backgroundColor: "#0f172a", 27 + }, 28 + edgeToEdgeEnabled: true, 29 + predictiveBackGestureEnabled: false, 30 + package: "com.rowanpaul.opnshelf", 31 + }, 32 + web: { 33 + favicon: "./assets/images/favicon.png", 34 + }, 35 + extra: { 36 + eas: { 37 + projectId: "87d86952-59ab-4711-9f5f-f9477b2d14f6", 38 + }, 39 + posthogApiKey: process.env.POSTHOG_API_KEY, 40 + posthogHost: 41 + process.env.POSTHOG_HOST || "https://eu.i.posthog.com", 42 + }, 43 + owner: "rowanpaul", 44 + plugins: ["@react-native-community/datetimepicker"], 45 + }, 46 + };
+2 -1
apps/mobile/app.json
··· 39 39 }, 40 40 "owner": "rowanpaul", 41 41 "plugins": [ 42 - "@react-native-community/datetimepicker" 42 + "@react-native-community/datetimepicker", 43 + "expo-localization" 43 44 ] 44 45 } 45 46 }
+5 -1
apps/mobile/app/(tabs)/profile/index.tsx
··· 10 10 Settings, 11 11 User, 12 12 } from "lucide-react-native"; 13 + import { usePostHog } from "posthog-react-native"; 13 14 import { useCallback } from "react"; 14 15 import { StyleSheet, Text, TouchableOpacity, View } from "react-native"; 15 16 import { SafeAreaView } from "react-native-safe-area-context"; ··· 25 26 const { user, isLoading: isAuthLoading, isAuthenticated, logout } = useAuth(); 26 27 const { showToast } = useToast(); 27 28 const { colors } = useTheme(); 29 + const posthog = usePostHog(); 28 30 29 31 const { data: profile } = useQuery({ 30 32 ...authControllerMeOptions(), ··· 33 35 34 36 const handleAuthAction = useCallback(async () => { 35 37 if (isAuthenticated) { 38 + posthog.capture("user_logged_out"); 39 + posthog.reset(); 36 40 await logout(); 37 41 showToast("Logged out successfully", "success"); 38 42 } else { 39 43 router.push("/login"); 40 44 } 41 - }, [isAuthenticated, logout, showToast]); 45 + }, [isAuthenticated, logout, showToast, posthog]); 42 46 43 47 if (isAuthLoading) { 44 48 return (
+12
apps/mobile/app/(tabs)/search.tsx
··· 15 15 import { FlashList, type ListRenderItem } from "@shopify/flash-list"; 16 16 import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; 17 17 import { router } from "expo-router"; 18 + import { usePostHog } from "posthog-react-native"; 18 19 import { useCallback, useEffect, useMemo, useRef, useState } from "react"; 19 20 import { 20 21 ActivityIndicator, ··· 52 53 const { showToast } = useToast(); 53 54 const { colors } = useTheme(); 54 55 const queryClient = useQueryClient(); 56 + const posthog = usePostHog(); 55 57 56 58 useEffect(() => { 57 59 if (debounceRef.current) { ··· 68 70 } 69 71 }; 70 72 }, [query]); 73 + 74 + // Track search events when the debounced query changes 75 + useEffect(() => { 76 + if (debouncedQuery.length > 0) { 77 + posthog.capture("media_searched", { 78 + query: debouncedQuery, 79 + media_type: mediaType, 80 + }); 81 + } 82 + }, [debouncedQuery, posthog, mediaType]); 71 83 72 84 const { 73 85 data: trackedMovies,
+47 -14
apps/mobile/app/_layout.tsx
··· 1 1 import { QueryClientProvider } from "@tanstack/react-query"; 2 - import { Stack } from "expo-router"; 2 + import { Stack, useGlobalSearchParams, usePathname } from "expo-router"; 3 3 import { StatusBar } from "expo-status-bar"; 4 - import { useEffect, useState } from "react"; 4 + import { PostHogProvider } from "posthog-react-native"; 5 + import { useEffect, useRef, useState } from "react"; 5 6 import { LogBox } from "react-native"; 6 7 import { MD3DarkTheme, PaperProvider } from "react-native-paper"; 7 8 import { DevToolsBubble } from "react-native-react-query-devtools"; ··· 11 12 import { AuthProvider, useAuth } from "@/contexts/auth"; 12 13 import { ThemeProvider } from "@/contexts/theme"; 13 14 import { initializeApiClient } from "@/lib/api"; 15 + import { posthog } from "@/lib/posthog"; 14 16 import { queryClient } from "@/lib/query-client"; 15 17 16 18 if (__DEV__) { ··· 83 85 ); 84 86 } 85 87 88 + function ScreenTracker() { 89 + const pathname = usePathname(); 90 + const params = useGlobalSearchParams(); 91 + const previousPathname = useRef<string | undefined>(undefined); 92 + 93 + // Manual screen tracking for Expo Router 94 + // @see https://docs.expo.dev/router/reference/screen-tracking/ 95 + useEffect(() => { 96 + if (previousPathname.current !== pathname) { 97 + posthog.screen(pathname, { 98 + previous_screen: previousPathname.current ?? null, 99 + ...params, 100 + }); 101 + previousPathname.current = pathname; 102 + } 103 + }, [pathname, params]); 104 + 105 + return null; 106 + } 107 + 86 108 export default function RootLayout() { 87 109 useEffect(() => { 88 110 initializeApiClient(); ··· 90 112 91 113 return ( 92 114 <SafeAreaProvider> 93 - <QueryClientProvider client={queryClient}> 94 - <PaperProvider theme={MD3DarkTheme}> 95 - <ThemeProvider> 96 - <AuthProvider> 97 - <LocaleInitializer> 98 - <AppContent /> 99 - </LocaleInitializer> 100 - </AuthProvider> 101 - </ThemeProvider> 102 - </PaperProvider> 103 - {__DEV__ && <DevToolsBubble queryClient={queryClient} />} 104 - </QueryClientProvider> 115 + <PostHogProvider 116 + client={posthog} 117 + autocapture={{ 118 + captureScreens: false, // Manual tracking with Expo Router 119 + captureTouches: true, 120 + propsToCapture: ["testID"], 121 + maxElementsCaptured: 20, 122 + }} 123 + > 124 + <QueryClientProvider client={queryClient}> 125 + <PaperProvider theme={MD3DarkTheme}> 126 + <ThemeProvider> 127 + <AuthProvider> 128 + <LocaleInitializer> 129 + <ScreenTracker /> 130 + <AppContent /> 131 + </LocaleInitializer> 132 + </AuthProvider> 133 + </ThemeProvider> 134 + </PaperProvider> 135 + {__DEV__ && <DevToolsBubble queryClient={queryClient} />} 136 + </QueryClientProvider> 137 + </PostHogProvider> 105 138 </SafeAreaProvider> 106 139 ); 107 140 }
+34 -2
apps/mobile/app/auth/complete.tsx
··· 1 1 import { authControllerMeOptions } from "@opnshelf/api"; 2 2 import { useQueryClient } from "@tanstack/react-query"; 3 3 import { useLocalSearchParams, useRouter } from "expo-router"; 4 + import { usePostHog } from "posthog-react-native"; 4 5 import { useEffect } from "react"; 5 6 import { ActivityIndicator, Image, Text, View } from "react-native"; 6 7 import { useToast } from "@/contexts/toast"; ··· 12 13 const params = useLocalSearchParams<{ session?: string }>(); 13 14 const { session } = params; 14 15 const { showToast } = useToast(); 16 + const posthog = usePostHog(); 15 17 16 18 useEffect(() => { 17 19 async function completeAuth() { ··· 20 22 await saveSessionToken(session); 21 23 } 22 24 23 - await queryClient.fetchQuery({ 25 + const user = await queryClient.fetchQuery({ 24 26 ...authControllerMeOptions(), 25 27 staleTime: 0, 26 28 }); 27 29 30 + // Identify the user in PostHog after successful login 31 + if (user) { 32 + posthog.identify(user.did, { 33 + $set: { 34 + handle: user.handle, 35 + did: user.did, 36 + }, 37 + $set_once: { 38 + first_login_date: new Date().toISOString(), 39 + }, 40 + }); 41 + posthog.capture("user_logged_in", { 42 + handle: user.handle, 43 + }); 44 + } 45 + 28 46 await new Promise((resolve) => setTimeout(resolve, 100)); 29 47 30 48 router.replace("/(tabs)"); 31 49 } catch (error) { 32 50 console.error("Auth complete failed:", error); 51 + posthog.capture("$exception", { 52 + $exception_list: [ 53 + { 54 + type: error instanceof Error ? error.name : "Error", 55 + value: 56 + error instanceof Error ? error.message : "Auth complete failed", 57 + stacktrace: { 58 + type: "raw", 59 + frames: error instanceof Error ? (error.stack ?? "") : "", 60 + }, 61 + }, 62 + ], 63 + $exception_source: "auth_complete", 64 + }); 33 65 showToast("Sign in failed. Please try again."); 34 66 router.replace("/login"); 35 67 } 36 68 } 37 69 38 70 completeAuth(); 39 - }, [router, queryClient, session, showToast]); 71 + }, [router, queryClient, session, showToast, posthog]); 40 72 41 73 return ( 42 74 <View
+13 -1
apps/mobile/app/list/[slug].tsx
··· 11 11 import { Image } from "expo-image"; 12 12 import { useLocalSearchParams, useRouter } from "expo-router"; 13 13 import { ArrowLeft, List, Trash2 } from "lucide-react-native"; 14 + import { usePostHog } from "posthog-react-native"; 14 15 import { useCallback, useState } from "react"; 15 16 import { 16 17 type NativeScrollEvent, ··· 46 47 const { showToast } = useToast(); 47 48 const { colors } = useTheme(); 48 49 const queryClient = useQueryClient(); 50 + const posthog = usePostHog(); 49 51 const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); 50 52 const [showCompactHeader, setShowCompactHeader] = useState(false); 51 53 ··· 70 72 const removeMutation = useMutation({ 71 73 mutationKey: ["lists", slug, "removeItem"], 72 74 ...listsControllerRemoveItemFromListMutation(), 73 - onSuccess: () => { 75 + onSuccess: (_, variables) => { 74 76 queryClient.invalidateQueries({ 75 77 queryKey: listsControllerGetListQueryKey({ 76 78 path: { slug: slug || "" }, 77 79 }), 78 80 }); 79 81 showToast("Removed from list", "success"); 82 + posthog.capture("media_removed_from_list", { 83 + media_type: variables.path.mediaType, 84 + media_id: variables.path.mediaId, 85 + ...(slug ? { list_slug: slug } : {}), 86 + ...(list?.name ? { list_name: list.name } : {}), 87 + }); 80 88 }, 81 89 onError: () => { 82 90 showToast("Failed to remove. Please try again.", "error"); ··· 91 99 queryKey: listsControllerGetUserListsQueryKey(), 92 100 }); 93 101 showToast("List deleted", "success"); 102 + posthog.capture("list_deleted", { 103 + ...(slug ? { list_slug: slug } : {}), 104 + ...(list?.name ? { list_name: list.name } : {}), 105 + }); 94 106 router.push("/(tabs)/profile/lists"); 95 107 }, 96 108 onError: () => {
+5
apps/mobile/app/login.tsx
··· 2 2 import { getLoginUrl, getSignupUrl } from "@opnshelf/api"; 3 3 import { useLocalSearchParams, useRouter } from "expo-router"; 4 4 import * as WebBrowser from "expo-web-browser"; 5 + import { usePostHog } from "posthog-react-native"; 5 6 import { useEffect, useRef, useState } from "react"; 6 7 import { 7 8 ActivityIndicator, ··· 31 32 const shownErrorRef = useRef<string | null>(null); 32 33 const { colors } = useTheme(); 33 34 const { showToast } = useToast(); 35 + const posthog = usePostHog(); 34 36 35 37 const { user, isLoading: isAuthLoading } = useAuth(); 36 38 ··· 96 98 97 99 const handleSignup = async () => { 98 100 setIsSubmitting(true); 101 + posthog.capture("user_signed_up", { 102 + method: "atmosphere", 103 + }); 99 104 try { 100 105 const timezone = detectUserTimezone(); 101 106 const signupUrl = getSignupUrl(timezone || undefined, "mobile");
+18 -1
apps/mobile/app/movie/[id].tsx
··· 22 22 import { Image } from "expo-image"; 23 23 import { LinearGradient } from "expo-linear-gradient"; 24 24 import { useLocalSearchParams, useRouter } from "expo-router"; 25 + import { usePostHog } from "posthog-react-native"; 25 26 import { useCallback, useMemo, useState } from "react"; 26 27 import { 27 28 ActivityIndicator, ··· 91 92 const { colors: themeColors } = useTheme(); 92 93 const { showToast } = useToast(); 93 94 const queryClient = useQueryClient(); 95 + const posthog = usePostHog(); 94 96 95 97 const { data: user, refetch: refetchUser } = useQuery({ 96 98 ...authControllerMeOptions(), ··· 234 236 }); 235 237 setShowDateModal(false); 236 238 showToast("Added to your shelf", "success"); 239 + posthog.capture("movie_marked_watched", { 240 + movie_id: movieId, 241 + ...(movie?.title ? { movie_title: movie.title } : {}), 242 + ...(movie?.release_date 243 + ? { movie_year: new Date(movie.release_date).getFullYear() } 244 + : {}), 245 + }); 237 246 }, 238 247 onError: () => { 239 248 showToast("Failed to add. Please try again.", "error"); ··· 255 264 }), 256 265 }); 257 266 showToast("Removed from your shelf", "success"); 267 + posthog.capture("movie_unmarked_watched", { 268 + movie_id: movieId, 269 + ...(movie?.title ? { movie_title: movie.title } : {}), 270 + }); 258 271 }, 259 272 onError: () => { 260 273 showToast("Failed to remove. Please try again.", "error"); ··· 315 328 message: `Check out ${displayTitle} on OpnShelf!\n\n${shareUrl}`, 316 329 title: `Check out ${displayTitle} on OpnShelf`, 317 330 }); 331 + posthog.capture("movie_shared", { 332 + movie_id: movieId, 333 + ...(displayTitle ? { movie_title: displayTitle } : {}), 334 + }); 318 335 } catch { 319 336 showToast("Failed to share", "error"); 320 337 } 321 - }, [movie?.title, movieId, title, showToast]); 338 + }, [movie?.title, movieId, title, showToast, posthog]); 322 339 323 340 const openDateModal = useCallback(() => { 324 341 setCustomDate(new Date());
+4
apps/mobile/app/settings.tsx
··· 14 14 Trash2, 15 15 User, 16 16 } from "lucide-react-native"; 17 + import { usePostHog } from "posthog-react-native"; 17 18 import { useCallback, useEffect, useState } from "react"; 18 19 import { 19 20 Modal, ··· 126 127 const { user, logout } = useAuth(); 127 128 const { colors } = useTheme(); 128 129 const queryClient = useQueryClient(); 130 + const posthog = usePostHog(); 129 131 130 132 const [showTimezoneModal, setShowTimezoneModal] = useState(false); 131 133 const [showDeleteModal, setShowDeleteModal] = useState(false); ··· 170 172 ...usersControllerDeleteMyAccountMutation(), 171 173 onSuccess: async () => { 172 174 showToast("Account deleted", "success"); 175 + posthog.capture("account_deleted"); 176 + posthog.reset(); 173 177 await logout(); 174 178 router.replace("/"); 175 179 },
+18
apps/mobile/app/show/[id].tsx
··· 15 15 import { Image } from "expo-image"; 16 16 import { LinearGradient } from "expo-linear-gradient"; 17 17 import { useLocalSearchParams, useRouter } from "expo-router"; 18 + import { usePostHog } from "posthog-react-native"; 18 19 import { useCallback, useMemo, useState } from "react"; 19 20 import { 20 21 type NativeScrollEvent, ··· 61 62 const { colors: themeColors } = useTheme(); 62 63 const { showToast } = useToast(); 63 64 const queryClient = useQueryClient(); 65 + const posthog = usePostHog(); 64 66 65 67 const [showListModal, setShowListModal] = useState(false); 66 68 const [showDateModal, setShowDateModal] = useState(false); ··· 170 172 queryKey: ["showsControllerGetShowWatchHistory"], 171 173 }); 172 174 showToast(`Marked ${data.count} episodes as watched`); 175 + posthog.capture("show_marked_watched", { 176 + show_id: id, 177 + episode_count: data.count, 178 + ...(show?.name ? { show_name: show.name } : {}), 179 + ...(show?.first_air_date 180 + ? { show_year: new Date(show.first_air_date).getFullYear() } 181 + : {}), 182 + }); 173 183 }, 174 184 onError: () => { 175 185 showToast("Failed to mark show as watched. Please try again.", "error"); ··· 203 213 }), 204 214 }); 205 215 showToast("Removed all episodes from your shelf"); 216 + posthog.capture("show_unmarked_watched", { 217 + show_id: id, 218 + ...(show?.name ? { show_name: show.name } : {}), 219 + }); 206 220 }, 207 221 onError: () => { 208 222 showToast("Failed to remove from shelf. Please try again.", "error"); ··· 222 236 await Share.share({ 223 237 message: `Check out ${show?.name} on OpnShelf!\n\n${shareUrl}`, 224 238 title: show?.name, 239 + }); 240 + posthog.capture("show_shared", { 241 + show_id: id, 242 + ...(show?.name ? { show_name: show.name } : {}), 225 243 }); 226 244 } catch { 227 245 // User cancelled or error
apps/mobile/build-1772224934693.aab

This is a binary file and will not be displayed.

apps/mobile/build-1772225079736.ipa

This is a binary file and will not be displayed.

+8
apps/mobile/components/AddToListModal.tsx
··· 18 18 Text, 19 19 View, 20 20 } from "react-native"; 21 + import { usePostHog } from "posthog-react-native"; 21 22 import { borderRadius, spacing } from "@/constants/spacing"; 22 23 import { useTheme } from "@/contexts/theme"; 23 24 ··· 38 39 }: AddToListModalProps) { 39 40 const queryClient = useQueryClient(); 40 41 const { colors } = useTheme(); 42 + const posthog = usePostHog(); 41 43 42 44 const { data: listsForMovie, isLoading } = useQuery({ 43 45 ...listsControllerGetListsForItemOptions({ ··· 59 61 }); 60 62 queryClient.invalidateQueries({ 61 63 queryKey: listsControllerGetListQueryKey({ path: { slug } }), 64 + }); 65 + posthog.capture("media_added_to_list", { 66 + list_slug: slug, 67 + media_type: mediaType, 68 + media_id: mediaId, 69 + media_title: mediaTitle, 62 70 }); 63 71 }, 64 72 });
+7 -1
apps/mobile/components/CreateListModal.tsx
··· 13 13 Text, 14 14 View, 15 15 } from "react-native"; 16 + import { usePostHog } from "posthog-react-native"; 16 17 import { borderRadius, spacing } from "@/constants/spacing"; 17 18 import { useTheme } from "@/contexts/theme"; 18 19 import { Button } from "@/components/ui/Button"; ··· 31 32 const [description, setDescription] = useState(""); 32 33 const queryClient = useQueryClient(); 33 34 const { colors } = useTheme(); 35 + const posthog = usePostHog(); 34 36 35 37 const createListMutation = useMutation({ 36 38 mutationKey: ["lists", "create"], 37 39 ...listsControllerCreateListMutation(), 38 - onSuccess: () => { 40 + onSuccess: (_, variables) => { 39 41 queryClient.invalidateQueries({ 40 42 queryKey: listsControllerGetUserListsQueryKey(), 43 + }); 44 + posthog.capture("list_created", { 45 + list_name: variables.body.name, 46 + has_description: !!variables.body.description, 41 47 }); 42 48 setName(""); 43 49 setDescription("");
+70
apps/mobile/lib/posthog.ts
··· 1 + import PostHog from "posthog-react-native"; 2 + import Constants from "expo-constants"; 3 + 4 + // Configuration loaded from app.config.js extras via expo-constants 5 + // Environment variables are read at build time in app.config.js 6 + const apiKey = Constants.expoConfig?.extra?.posthogApiKey as string | undefined; 7 + const host = 8 + (Constants.expoConfig?.extra?.posthogHost as string) || 9 + "https://eu.i.posthog.com"; 10 + const isPostHogConfigured = !!apiKey && apiKey !== "phc_your_api_key_here"; 11 + 12 + if (__DEV__) { 13 + console.log("PostHog config:", { 14 + apiKey: apiKey ? "SET" : "NOT SET", 15 + host, 16 + isConfigured: isPostHogConfigured, 17 + }); 18 + } 19 + 20 + if (!isPostHogConfigured) { 21 + console.warn( 22 + "PostHog API key not configured. Analytics will be disabled. " + 23 + "Set POSTHOG_API_KEY in your .env file to enable analytics.", 24 + ); 25 + } 26 + 27 + /** 28 + * PostHog client instance for Expo 29 + * 30 + * Configuration loaded from app.config.js extras via expo-constants. 31 + * Required peer dependencies: expo-file-system, expo-application, 32 + * expo-device, expo-localization 33 + * 34 + * @see https://posthog.com/docs/libraries/react-native 35 + */ 36 + export const posthog = new PostHog(apiKey || "placeholder_key", { 37 + // PostHog API host 38 + host, 39 + 40 + // Disable PostHog if API key is not configured 41 + disabled: !isPostHogConfigured, 42 + 43 + // Capture app lifecycle events: 44 + // - Application Installed, Application Updated 45 + // - Application Opened, Application Became Active, Application Backgrounded 46 + captureAppLifecycleEvents: true, 47 + 48 + // Batching: queue events and flush periodically to optimize battery usage 49 + flushAt: 20, 50 + flushInterval: 10000, 51 + maxBatchSize: 100, 52 + maxQueueSize: 1000, 53 + 54 + // Feature flags 55 + preloadFeatureFlags: true, 56 + sendFeatureFlagEvent: true, 57 + featureFlagsRequestTimeoutMs: 10000, 58 + 59 + // Network settings 60 + requestTimeout: 10000, 61 + fetchRetryCount: 3, 62 + fetchRetryDelay: 3000, 63 + }); 64 + 65 + // Enable debug mode in development for verbose logging 66 + if (__DEV__) { 67 + posthog.debug(true); 68 + } 69 + 70 + export const isPostHogEnabled = isPostHogConfigured;
+5
apps/mobile/package.json
··· 26 26 "@tanstack/react-query": "^5.90.20", 27 27 "date-fns": "^4.1.0", 28 28 "expo": "~54.0.33", 29 + "expo-application": "~7.0.8", 29 30 "expo-constants": "~18.0.13", 30 31 "expo-dev-client": "~6.0.20", 32 + "expo-device": "~8.0.10", 33 + "expo-file-system": "~19.0.21", 31 34 "expo-font": "~14.0.11", 32 35 "expo-haptics": "~15.0.8", 33 36 "expo-image": "~3.0.11", 34 37 "expo-linear-gradient": "^15.0.8", 35 38 "expo-linking": "~8.0.11", 39 + "expo-localization": "~17.0.8", 36 40 "expo-router": "~6.0.23", 37 41 "expo-secure-store": "^15.0.8", 38 42 "expo-splash-screen": "~31.0.13", ··· 41 45 "expo-system-ui": "~6.0.9", 42 46 "expo-web-browser": "~15.0.10", 43 47 "lucide-react-native": "^0.563.0", 48 + "posthog-react-native": "^4.36.1", 44 49 "react": "19.1.0", 45 50 "react-dom": "19.1.0", 46 51 "react-native": "0.81.5",
+3 -1
apps/web/package.json
··· 13 13 "start": "node .output/server/index.mjs" 14 14 }, 15 15 "dependencies": { 16 + "@material/material-color-utilities": "0.3.0", 16 17 "@opnshelf/api": "workspace:*", 17 - "@material/material-color-utilities": "0.3.0", 18 + "@posthog/react": "^1.8.1", 18 19 "@radix-ui/react-switch": "^1.2.6", 19 20 "@t3-oss/env-core": "^0.13.8", 20 21 "@tailwindcss/vite": "^4.0.6", ··· 29 30 "date-fns": "^4.1.0", 30 31 "lucide-react": "^0.561.0", 31 32 "nitro": "npm:nitro-nightly@latest", 33 + "posthog-js": "^1.356.1", 32 34 "radix-ui": "^1.4.3", 33 35 "react": "^19.2.0", 34 36 "react-day-picker": "^9.13.1",
+8 -1
apps/web/src/components/CreateListDialog.tsx
··· 2 2 listsControllerCreateListMutation, 3 3 listsControllerGetUserListsQueryKey, 4 4 } from "@opnshelf/api"; 5 + import { usePostHog } from "@posthog/react"; 5 6 import { useMutation, useQueryClient } from "@tanstack/react-query"; 6 7 import { ListPlus } from "lucide-react"; 7 8 import { useId, useState } from "react"; ··· 25 26 const queryClient = useQueryClient(); 26 27 const id = useId(); 27 28 const { seedColor } = useTheme(); 29 + const posthog = usePostHog(); 28 30 29 31 const createListMutation = useMutation({ 30 32 mutationKey: ["lists", "create"], 31 33 ...listsControllerCreateListMutation(), 32 - onSuccess: () => { 34 + onSuccess: (data) => { 33 35 queryClient.invalidateQueries({ 34 36 queryKey: listsControllerGetUserListsQueryKey(), 37 + }); 38 + posthog.capture("list_created", { 39 + list_name: name.trim(), 40 + has_description: !!description.trim(), 41 + list_id: (data as { id?: string })?.id, 35 42 }); 36 43 setOpen(false); 37 44 setName("");
+19 -1
apps/web/src/components/detail/DetailActions.tsx
··· 1 + import { usePostHog } from "@posthog/react"; 1 2 import { Calendar, Check, ListPlus, RotateCcw, Share2 } from "lucide-react"; 2 3 import { useState } from "react"; 3 4 import { toast } from "sonner"; ··· 29 30 30 31 export function DetailActions({ 31 32 mediaType, 33 + mediaId, 32 34 colors, 33 35 isWatched, 34 36 watchedDate, ··· 45 47 onLogin, 46 48 }: DetailActionsProps) { 47 49 const [copied, setCopied] = useState(false); 50 + const posthog = usePostHog(); 48 51 49 52 const handleShare = async () => { 50 53 const url = window.location.href; 54 + posthog.capture("content_shared", { 55 + media_type: mediaType, 56 + media_id: mediaId, 57 + share_method: navigator.share ? "native" : "clipboard", 58 + url, 59 + }); 51 60 if (navigator.share) { 52 61 try { 53 62 await navigator.share({ url }); ··· 64 73 toast.error("Failed to copy link"); 65 74 } 66 75 } 76 + }; 77 + 78 + const handleShowListModal = () => { 79 + posthog.capture("add_to_list_opened", { 80 + media_type: mediaType, 81 + media_id: mediaId, 82 + already_in_lists: listsCount, 83 + }); 84 + onShowListModal?.(); 67 85 }; 68 86 69 87 const isInAnyList = listsCount > 0; ··· 188 206 ? `In ${listsCount} list${listsCount > 1 ? "s" : ""}` 189 207 : "Add to List" 190 208 } 191 - onClick={onShowListModal} 209 + onClick={handleShowListModal} 192 210 isActive={isInAnyList} 193 211 activeColor={colors.primary} 194 212 />
+2
apps/web/src/env.ts
··· 15 15 client: { 16 16 VITE_APP_TITLE: z.string().min(1).optional(), 17 17 VITE_API_URL: z.string().url().default("http://127.0.0.1:3001"), 18 + VITE_PUBLIC_POSTHOG_KEY: z.string().min(1).optional(), 19 + VITE_PUBLIC_POSTHOG_HOST: z.string().url().optional(), 18 20 }, 19 21 20 22 /**
+13 -1
apps/web/src/routes/__root.tsx
··· 1 1 import { configureApiClient } from "@opnshelf/api"; 2 + import { PostHogProvider } from "@posthog/react"; 2 3 import { TanStackDevtools } from "@tanstack/react-devtools"; 3 4 import { type QueryClient, QueryClientProvider } from "@tanstack/react-query"; 4 5 import { ··· 86 87 <HeadContent /> 87 88 </head> 88 89 <body> 89 - {children} 90 + <PostHogProvider 91 + apiKey={env.VITE_PUBLIC_POSTHOG_KEY ?? ""} 92 + options={{ 93 + api_host: "/ingest", 94 + ui_host: env.VITE_PUBLIC_POSTHOG_HOST || "https://eu.posthog.com", 95 + defaults: "2025-05-24", 96 + capture_exceptions: true, 97 + debug: import.meta.env.DEV, 98 + }} 99 + > 100 + {children} 101 + </PostHogProvider> 90 102 <Scripts /> 91 103 </body> 92 104 </html>
+5 -1
apps/web/src/routes/auth/complete.tsx
··· 1 + import { usePostHog } from "@posthog/react"; 1 2 import { useQueryClient } from "@tanstack/react-query"; 2 3 import { createFileRoute, useNavigate } from "@tanstack/react-router"; 3 4 import { useEffect } from "react"; ··· 19 20 const navigate = useNavigate(); 20 21 const queryClient = useQueryClient(); 21 22 const { seedColor } = useTheme(); 23 + const posthog = usePostHog(); 22 24 23 25 useEffect(() => { 24 26 queryClient.invalidateQueries({ queryKey: ["auth"] }); 25 27 sessionStorage.removeItem("oauth_pending"); 28 + 29 + posthog.capture("auth_completed"); 26 30 27 31 const storedRedirect = sessionStorage.getItem("auth_redirect"); 28 32 sessionStorage.removeItem("auth_redirect"); ··· 32 36 } else { 33 37 navigate({ to: "/profile/shelf" }); 34 38 } 35 - }, [navigate, queryClient]); 39 + }, [navigate, queryClient, posthog]); 36 40 37 41 return ( 38 42 <div
+8
apps/web/src/routes/login.tsx
··· 3 3 getLoginUrl, 4 4 getSignupUrl, 5 5 } from "@opnshelf/api"; 6 + import { usePostHog } from "@posthog/react"; 6 7 import { useQuery } from "@tanstack/react-query"; 7 8 import { createFileRoute, useNavigate } from "@tanstack/react-router"; 8 9 import { AlertCircle, ChevronDown, ChevronUp } from "lucide-react"; ··· 38 39 const handleId = useId(); 39 40 const shownErrorRef = useRef<string | null>(null); 40 41 const { seedColor } = useTheme(); 42 + const posthog = usePostHog(); 41 43 42 44 const { data: user, isLoading: isAuthLoading } = useQuery({ 43 45 ...authControllerMeOptions(), ··· 66 68 if (redirect) { 67 69 sessionStorage.setItem("auth_redirect", redirect); 68 70 } 71 + 72 + posthog.capture("login_initiated", { 73 + handle: loginHandle || undefined, 74 + has_redirect: !!redirect, 75 + }); 69 76 70 77 const timezone = detectUserTimezone(); 71 78 const loginUrl = getLoginUrl( ··· 257 264 <button 258 265 type="button" 259 266 onClick={() => { 267 + posthog.capture("signup_initiated"); 260 268 const timezone = detectUserTimezone(); 261 269 window.location.href = getSignupUrl(timezone || undefined); 262 270 }}
+13
apps/web/src/routes/movies.$movieId.$title.tsx
··· 13 13 type TrackedMovieDto, 14 14 usersControllerGetMySettingsOptions, 15 15 } from "@opnshelf/api"; 16 + import { usePostHog } from "@posthog/react"; 16 17 import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; 17 18 import { createFileRoute, useRouter } from "@tanstack/react-router"; 18 19 import { Calendar, Clock, History, Loader2, Star, Trash2 } from "lucide-react"; ··· 104 105 const queryClient = useQueryClient(); 105 106 const router = useRouter(); 106 107 const { seedColor } = useTheme(); 108 + const posthog = usePostHog(); 107 109 108 110 const [showDateModal, setShowDateModal] = useState(false); 109 111 const [showHistoryDialog, setShowHistoryDialog] = useState(false); ··· 196 198 path: { userDid: user?.did || "", movieId }, 197 199 }), 198 200 }); 201 + posthog.capture("movie_marked_watched", { 202 + movie_id: movieId, 203 + movie_title: movie?.title, 204 + release_year: movie?.release_date 205 + ? new Date(movie.release_date).getFullYear() 206 + : undefined, 207 + }); 199 208 toast.success("Added to your shelf"); 200 209 setShowDateModal(false); 201 210 }, ··· 217 226 queryKey: moviesControllerGetMovieWatchHistoryQueryKey({ 218 227 path: { userDid: user?.did || "", movieId }, 219 228 }), 229 + }); 230 + posthog.capture("movie_unmarked_watched", { 231 + movie_id: movieId, 232 + movie_title: movie?.title, 220 233 }); 221 234 toast.success("Removed from your shelf"); 222 235 },
+6
apps/web/src/routes/profile.settings.tsx
··· 5 5 usersControllerGetMySettingsOptions, 6 6 usersControllerUpdateMySettingsMutation, 7 7 } from "@opnshelf/api"; 8 + import { usePostHog } from "@posthog/react"; 8 9 import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; 9 10 import { createFileRoute, useRouter } from "@tanstack/react-router"; 10 11 import { ··· 138 139 const { seedColor } = useTheme(); 139 140 const timezoneId = useId(); 140 141 const deletePdsId = useId(); 142 + const posthog = usePostHog(); 141 143 142 144 const { data: user, isLoading: isAuthLoading } = useQuery({ 143 145 ...authControllerMeOptions(), ··· 180 182 mutationKey: ["users", "account", "delete"], 181 183 ...usersControllerDeleteMyAccountMutation(), 182 184 onSuccess: () => { 185 + posthog.capture("account_deleted", { 186 + deleted_pds_data: deletePDSData, 187 + }); 188 + posthog.reset(); 183 189 setShowDeleteDialog(false); 184 190 toast.success("Account deleted"); 185 191 queryClient.setQueryData(authControllerMeQueryKey(), null);
+9 -1
apps/web/src/routes/search.tsx
··· 7 7 type TmdbMovieResultDto, 8 8 type UnifiedSearchResultDto, 9 9 } from "@opnshelf/api"; 10 + import { usePostHog } from "@posthog/react"; 10 11 import { useQuery } from "@tanstack/react-query"; 11 12 import { createFileRoute, useNavigate } from "@tanstack/react-router"; 12 13 import { Search, X } from "lucide-react"; ··· 34 35 const [query, setQuery] = useState(searchQuery); 35 36 const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null); 36 37 const lastNavigatedQueryRef = useRef<string>(searchQuery); 38 + const posthog = usePostHog(); 37 39 38 40 const { data: user } = useQuery({ 39 41 ...authControllerMeOptions(), ··· 81 83 if (trimmed !== searchQuery) { 82 84 debounceRef.current = setTimeout(() => { 83 85 lastNavigatedQueryRef.current = trimmed; 86 + if (trimmed.length > 0) { 87 + posthog.capture("search_performed", { 88 + query: trimmed, 89 + filter_type: type, 90 + }); 91 + } 84 92 navigate({ 85 93 search: { q: trimmed, type }, 86 94 replace: true, ··· 94 102 clearTimeout(debounceRef.current); 95 103 } 96 104 }; 97 - }, [query, searchQuery, type, navigate]); 105 + }, [query, searchQuery, type, navigate, posthog.capture]); 98 106 99 107 const hasQuery = searchQuery.length > 0; 100 108 const isMovies = type === "movies";
+12
apps/web/src/routes/shows.$showId.$title.tsx
··· 9 9 showsControllerUnmarkWatchedMutation, 10 10 type TmdbShowDetailDto, 11 11 } from "@opnshelf/api"; 12 + import { usePostHog } from "@posthog/react"; 12 13 import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; 13 14 import { 14 15 createFileRoute, ··· 98 99 const router = useRouter(); 99 100 const queryClient = useQueryClient(); 100 101 const { seedColor } = useTheme(); 102 + const posthog = usePostHog(); 101 103 102 104 const [showListModal, setShowListModal] = useState(false); 103 105 const [showDateModal, setShowDateModal] = useState(false); ··· 159 161 path: { userDid: user?.did || "", showId }, 160 162 }), 161 163 }); 164 + posthog.capture("show_marked_watched", { 165 + show_id: showId, 166 + show_name: show?.name, 167 + episodes_marked: data.count, 168 + season_count: show?.number_of_seasons, 169 + }); 162 170 toast.success(`Marked ${data.count} episodes as watched`); 163 171 }, 164 172 onError: () => { ··· 179 187 queryKey: showsControllerGetShowWatchHistoryQueryKey({ 180 188 path: { userDid: user?.did || "", showId }, 181 189 }), 190 + }); 191 + posthog.capture("show_unmarked_watched", { 192 + show_id: showId, 193 + show_name: show?.name, 182 194 }); 183 195 toast.success("Removed all episodes from your shelf"); 184 196 },
+8
apps/web/vite.config.ts
··· 11 11 server: { 12 12 port: 3000, 13 13 host: true, // listen on 0.0.0.0 so both localhost and 127.0.0.1 work 14 + proxy: { 15 + "/ingest": { 16 + target: "https://eu.i.posthog.com", 17 + changeOrigin: true, 18 + rewrite: (path) => path.replace(/^\/ingest/, ""), 19 + secure: false, 20 + }, 21 + }, 14 22 }, 15 23 resolve: { 16 24 alias: {
+415
pnpm-lock.yaml
··· 50 50 expo: 51 51 specifier: ~54.0.33 52 52 version: 54.0.33(@babel/core@7.28.6)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) 53 + expo-application: 54 + specifier: ~7.0.8 55 + version: 7.0.8(expo@54.0.33) 53 56 expo-constants: 54 57 specifier: ~18.0.13 55 58 version: 18.0.13(expo@54.0.33)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0)) 56 59 expo-dev-client: 57 60 specifier: ~6.0.20 58 61 version: 6.0.20(expo@54.0.33) 62 + expo-device: 63 + specifier: ~8.0.10 64 + version: 8.0.10(expo@54.0.33) 65 + expo-file-system: 66 + specifier: ~19.0.21 67 + version: 19.0.21(expo@54.0.33)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0)) 59 68 expo-font: 60 69 specifier: ~14.0.11 61 70 version: 14.0.11(expo@54.0.33)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) ··· 71 80 expo-linking: 72 81 specifier: ~8.0.11 73 82 version: 8.0.11(expo@54.0.33)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) 83 + expo-localization: 84 + specifier: ~17.0.8 85 + version: 17.0.8(expo@54.0.33)(react@19.1.0) 74 86 expo-router: 75 87 specifier: ~6.0.23 76 88 version: 6.0.23(52130e04a12e6efd1d6e48b3f2ad01c3) ··· 95 107 lucide-react-native: 96 108 specifier: ^0.563.0 97 109 version: 0.563.0(react-native-svg@15.15.3(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) 110 + posthog-react-native: 111 + specifier: ^4.36.1 112 + version: 4.36.1(@react-navigation/native@7.1.28(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(expo-application@7.0.8(expo@54.0.33))(expo-device@8.0.10(expo@54.0.33))(expo-file-system@19.0.21(expo@54.0.33)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0)))(expo-localization@17.0.8(expo@54.0.33)(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-svg@15.15.3(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) 98 113 react: 99 114 specifier: 19.1.0 100 115 version: 19.1.0 ··· 153 168 '@opnshelf/api': 154 169 specifier: workspace:* 155 170 version: link:../../packages/api 171 + '@posthog/react': 172 + specifier: ^1.8.1 173 + version: 1.8.1(@types/react@19.2.10)(posthog-js@1.356.1)(react@19.2.4) 156 174 '@radix-ui/react-switch': 157 175 specifier: ^1.2.6 158 176 version: 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ··· 195 213 nitro: 196 214 specifier: npm:nitro-nightly@latest 197 215 version: nitro-nightly@3.0.1-20260127-164246-ef01b092(@electric-sql/pglite@0.3.15)(chokidar@5.0.0)(lru-cache@11.2.5)(mysql2@3.15.3)(rollup@4.57.0)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) 216 + posthog-js: 217 + specifier: ^1.356.1 218 + version: 1.356.1 198 219 radix-ui: 199 220 specifier: ^1.4.3 200 221 version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ··· 2187 2208 resolution: {integrity: sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA==} 2188 2209 engines: {node: '>=20.0'} 2189 2210 2211 + '@opentelemetry/api-logs@0.208.0': 2212 + resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==} 2213 + engines: {node: '>=8.0.0'} 2214 + 2215 + '@opentelemetry/api@1.9.0': 2216 + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} 2217 + engines: {node: '>=8.0.0'} 2218 + 2219 + '@opentelemetry/core@2.2.0': 2220 + resolution: {integrity: sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==} 2221 + engines: {node: ^18.19.0 || >=20.6.0} 2222 + peerDependencies: 2223 + '@opentelemetry/api': '>=1.0.0 <1.10.0' 2224 + 2225 + '@opentelemetry/core@2.5.1': 2226 + resolution: {integrity: sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA==} 2227 + engines: {node: ^18.19.0 || >=20.6.0} 2228 + peerDependencies: 2229 + '@opentelemetry/api': '>=1.0.0 <1.10.0' 2230 + 2231 + '@opentelemetry/exporter-logs-otlp-http@0.208.0': 2232 + resolution: {integrity: sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==} 2233 + engines: {node: ^18.19.0 || >=20.6.0} 2234 + peerDependencies: 2235 + '@opentelemetry/api': ^1.3.0 2236 + 2237 + '@opentelemetry/otlp-exporter-base@0.208.0': 2238 + resolution: {integrity: sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==} 2239 + engines: {node: ^18.19.0 || >=20.6.0} 2240 + peerDependencies: 2241 + '@opentelemetry/api': ^1.3.0 2242 + 2243 + '@opentelemetry/otlp-transformer@0.208.0': 2244 + resolution: {integrity: sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==} 2245 + engines: {node: ^18.19.0 || >=20.6.0} 2246 + peerDependencies: 2247 + '@opentelemetry/api': ^1.3.0 2248 + 2249 + '@opentelemetry/resources@2.2.0': 2250 + resolution: {integrity: sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==} 2251 + engines: {node: ^18.19.0 || >=20.6.0} 2252 + peerDependencies: 2253 + '@opentelemetry/api': '>=1.3.0 <1.10.0' 2254 + 2255 + '@opentelemetry/resources@2.5.1': 2256 + resolution: {integrity: sha512-BViBCdE/GuXRlp9k7nS1w6wJvY5fnFX5XvuEtWsTAOQFIO89Eru7lGW3WbfbxtCuZ/GbrJfAziXG0w0dpxL7eQ==} 2257 + engines: {node: ^18.19.0 || >=20.6.0} 2258 + peerDependencies: 2259 + '@opentelemetry/api': '>=1.3.0 <1.10.0' 2260 + 2261 + '@opentelemetry/sdk-logs@0.208.0': 2262 + resolution: {integrity: sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==} 2263 + engines: {node: ^18.19.0 || >=20.6.0} 2264 + peerDependencies: 2265 + '@opentelemetry/api': '>=1.4.0 <1.10.0' 2266 + 2267 + '@opentelemetry/sdk-metrics@2.2.0': 2268 + resolution: {integrity: sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==} 2269 + engines: {node: ^18.19.0 || >=20.6.0} 2270 + peerDependencies: 2271 + '@opentelemetry/api': '>=1.9.0 <1.10.0' 2272 + 2273 + '@opentelemetry/sdk-trace-base@2.2.0': 2274 + resolution: {integrity: sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==} 2275 + engines: {node: ^18.19.0 || >=20.6.0} 2276 + peerDependencies: 2277 + '@opentelemetry/api': '>=1.3.0 <1.10.0' 2278 + 2279 + '@opentelemetry/semantic-conventions@1.40.0': 2280 + resolution: {integrity: sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==} 2281 + engines: {node: '>=14'} 2282 + 2190 2283 '@oxc-minify/binding-android-arm-eabi@0.111.0': 2191 2284 resolution: {integrity: sha512-MkDWMUkYjfzcIA/StNBN/mi17WjdKnt7Fa2ESOND3b333dLCfaiS3zy+p7IYvAPV+osaK8DtcmUVlstX6l9Smw==} 2192 2285 engines: {node: ^20.19.0 || >=22.12.0} ··· 2452 2545 resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} 2453 2546 engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} 2454 2547 2548 + '@posthog/core@1.23.1': 2549 + resolution: {integrity: sha512-GViD5mOv/mcbZcyzz3z9CS0R79JzxVaqEz4sP5Dsea178M/j3ZWe6gaHDZB9yuyGfcmIMQ/8K14yv+7QrK4sQQ==} 2550 + 2551 + '@posthog/react@1.8.1': 2552 + resolution: {integrity: sha512-/tRMKPm8PKvCgmKjgKM4ojSbOpdzzKyDNK8upbK6cKedZ1wdtOcSAMiLEyNCatFB2dqwB85LcjrhkaO8lFnHNQ==} 2553 + peerDependencies: 2554 + '@types/react': '>=16.8.0' 2555 + posthog-js: '>=1.257.2' 2556 + react: '>=16.8.0' 2557 + peerDependenciesMeta: 2558 + '@types/react': 2559 + optional: true 2560 + 2561 + '@posthog/types@1.356.1': 2562 + resolution: {integrity: sha512-miIUjs4LiBDMOxKkC87HEJLIih0pNGMAjxx+mW4X7jLpN41n0PLMW7swRE6uuxcMV0z3H6MllRSCYmsokkyfuQ==} 2563 + 2455 2564 '@prisma/adapter-pg@7.3.0': 2456 2565 resolution: {integrity: sha512-iuYQMbIPO6i9O45Fv8TB7vWu00BXhCaNAShenqF7gLExGDbnGp5BfFB4yz1K59zQ59jF6tQ9YHrg0P6/J3OoLg==} 2457 2566 ··· 2510 2619 react: ^18.0.0 || ^19.0.0 2511 2620 react-dom: ^18.0.0 || ^19.0.0 2512 2621 2622 + '@protobufjs/aspromise@1.1.2': 2623 + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} 2624 + 2625 + '@protobufjs/base64@1.1.2': 2626 + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} 2627 + 2628 + '@protobufjs/codegen@2.0.4': 2629 + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} 2630 + 2631 + '@protobufjs/eventemitter@1.1.0': 2632 + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} 2633 + 2634 + '@protobufjs/fetch@1.1.0': 2635 + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} 2636 + 2637 + '@protobufjs/float@1.0.2': 2638 + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} 2639 + 2640 + '@protobufjs/inquire@1.1.0': 2641 + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} 2642 + 2643 + '@protobufjs/path@1.1.2': 2644 + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} 2645 + 2646 + '@protobufjs/pool@1.1.0': 2647 + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} 2648 + 2649 + '@protobufjs/utf8@1.1.0': 2650 + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} 2651 + 2513 2652 '@radix-ui/number@1.1.1': 2514 2653 resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} 2515 2654 ··· 3968 4107 '@types/supertest@6.0.3': 3969 4108 resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} 3970 4109 4110 + '@types/trusted-types@2.0.7': 4111 + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} 4112 + 3971 4113 '@types/validator@13.15.10': 3972 4114 resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} 3973 4115 ··· 5107 5249 resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} 5108 5250 engines: {node: '>= 4'} 5109 5251 5252 + dompurify@3.3.1: 5253 + resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} 5254 + 5110 5255 domutils@3.2.2: 5111 5256 resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} 5112 5257 ··· 5311 5456 resolution: {integrity: sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==} 5312 5457 engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} 5313 5458 5459 + expo-application@7.0.8: 5460 + resolution: {integrity: sha512-qFGyxk7VJbrNOQWBbE09XUuGuvkOgFS9QfToaK2FdagM2aQ+x3CvGV2DuVgl/l4ZxPgIf3b/MNh9xHpwSwn74Q==} 5461 + peerDependencies: 5462 + expo: '*' 5463 + 5314 5464 expo-asset@12.0.12: 5315 5465 resolution: {integrity: sha512-CsXFCQbx2fElSMn0lyTdRIyKlSXOal6ilLJd+yeZ6xaC7I9AICQgscY5nj0QcwgA+KYYCCEQEBndMsmj7drOWQ==} 5316 5466 peerDependencies: ··· 5341 5491 5342 5492 expo-dev-menu@7.0.18: 5343 5493 resolution: {integrity: sha512-4kTdlHrnZCAWCT6tZRQHSSjZ7vECFisL4T+nsG/GJDo/jcHNaOVGV5qPV9wzlTxyMk3YOPggRw4+g7Ownrg5eA==} 5494 + peerDependencies: 5495 + expo: '*' 5496 + 5497 + expo-device@8.0.10: 5498 + resolution: {integrity: sha512-jd5BxjaF7382JkDMaC+P04aXXknB2UhWaVx5WiQKA05ugm/8GH5uaz9P9ckWdMKZGQVVEOC8MHaUADoT26KmFA==} 5344 5499 peerDependencies: 5345 5500 expo: '*' 5346 5501 ··· 5395 5550 react: '*' 5396 5551 react-native: '*' 5397 5552 5553 + expo-localization@17.0.8: 5554 + resolution: {integrity: sha512-UrdwklZBDJ+t+ZszMMiE0SXZ2eJxcquCuQcl6EvGHM9K+e6YqKVRQ+w8qE+iIB3H75v2RJy6MHAaLK+Mqeo04g==} 5555 + peerDependencies: 5556 + expo: '*' 5557 + react: '*' 5558 + 5398 5559 expo-manifests@1.0.10: 5399 5560 resolution: {integrity: sha512-oxDUnURPcL4ZsOBY6X1DGWGuoZgVAFzp6PISWV7lPP2J0r8u1/ucuChBgpK7u1eLGFp6sDIPwXyEUCkI386XSQ==} 5400 5561 peerDependencies: ··· 5559 5720 peerDependenciesMeta: 5560 5721 picomatch: 5561 5722 optional: true 5723 + 5724 + fflate@0.4.8: 5725 + resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} 5562 5726 5563 5727 file-type@16.5.4: 5564 5728 resolution: {integrity: sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==} ··· 7180 7344 resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} 7181 7345 engines: {node: '>=12'} 7182 7346 7347 + posthog-js@1.356.1: 7348 + resolution: {integrity: sha512-4EQliSyTp3j/xOaWpZmu7fk1b4S+J3qy4JOu5Xy3/MYFxv1SlAylgifRdCbXZxCQWb6PViaNvwRf4EmburgfWA==} 7349 + 7350 + posthog-react-native@4.36.1: 7351 + resolution: {integrity: sha512-zAVTvFMoHtNgZcbaVthv3DB8EJCsebs9en4/GwsLuULSq6iuNtHdGGpvScIEB173okqvCudAIDJW49SMBTbLHg==} 7352 + peerDependencies: 7353 + '@react-native-async-storage/async-storage': '>=1.0.0' 7354 + '@react-navigation/native': '>= 5.0.0' 7355 + expo-application: '>= 4.0.0' 7356 + expo-device: '>= 4.0.0' 7357 + expo-file-system: '>= 13.0.0' 7358 + expo-localization: '>= 11.0.0' 7359 + posthog-react-native-session-replay: '>= 1.3.0' 7360 + react-native-device-info: '>= 10.0.0' 7361 + react-native-localize: '>= 3.0.0' 7362 + react-native-navigation: '>= 6.0.0' 7363 + react-native-safe-area-context: '>= 4.0.0' 7364 + react-native-svg: '>= 15.0.0' 7365 + peerDependenciesMeta: 7366 + '@react-native-async-storage/async-storage': 7367 + optional: true 7368 + '@react-navigation/native': 7369 + optional: true 7370 + expo-application: 7371 + optional: true 7372 + expo-device: 7373 + optional: true 7374 + expo-file-system: 7375 + optional: true 7376 + expo-localization: 7377 + optional: true 7378 + posthog-react-native-session-replay: 7379 + optional: true 7380 + react-native-device-info: 7381 + optional: true 7382 + react-native-localize: 7383 + optional: true 7384 + react-native-navigation: 7385 + optional: true 7386 + react-native-safe-area-context: 7387 + optional: true 7388 + 7183 7389 powershell-utils@0.1.0: 7184 7390 resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} 7185 7391 engines: {node: '>=20'} 7186 7392 7393 + preact@10.28.4: 7394 + resolution: {integrity: sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==} 7395 + 7187 7396 prebuild-install@7.1.3: 7188 7397 resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} 7189 7398 engines: {node: '>=10'} ··· 7248 7457 proper-lockfile@4.1.2: 7249 7458 resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} 7250 7459 7460 + protobufjs@7.5.4: 7461 + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} 7462 + engines: {node: '>=12.0.0'} 7463 + 7251 7464 proxy-addr@2.0.7: 7252 7465 resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} 7253 7466 engines: {node: '>= 0.10'} ··· 7272 7485 qs@6.14.1: 7273 7486 resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} 7274 7487 engines: {node: '>=0.6'} 7488 + 7489 + query-selector-shadow-dom@1.0.1: 7490 + resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==} 7275 7491 7276 7492 query-string@7.1.3: 7277 7493 resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} ··· 7626 7842 resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} 7627 7843 engines: {node: '>= 18'} 7628 7844 7845 + rtl-detect@1.1.2: 7846 + resolution: {integrity: sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ==} 7847 + 7629 7848 run-applescript@7.1.0: 7630 7849 resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} 7631 7850 engines: {node: '>=18'} ··· 8307 8526 engines: {node: '>=14.17'} 8308 8527 hasBin: true 8309 8528 8529 + ua-parser-js@0.7.41: 8530 + resolution: {integrity: sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==} 8531 + hasBin: true 8532 + 8310 8533 ua-parser-js@1.0.41: 8311 8534 resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==} 8312 8535 hasBin: true ··· 8650 8873 8651 8874 wcwidth@1.0.1: 8652 8875 resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} 8876 + 8877 + web-vitals@5.1.0: 8878 + resolution: {integrity: sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==} 8653 8879 8654 8880 webidl-conversions@3.0.1: 8655 8881 resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} ··· 11339 11565 11340 11566 '@oozcitak/util@10.0.0': {} 11341 11567 11568 + '@opentelemetry/api-logs@0.208.0': 11569 + dependencies: 11570 + '@opentelemetry/api': 1.9.0 11571 + 11572 + '@opentelemetry/api@1.9.0': {} 11573 + 11574 + '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0)': 11575 + dependencies: 11576 + '@opentelemetry/api': 1.9.0 11577 + '@opentelemetry/semantic-conventions': 1.40.0 11578 + 11579 + '@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0)': 11580 + dependencies: 11581 + '@opentelemetry/api': 1.9.0 11582 + '@opentelemetry/semantic-conventions': 1.40.0 11583 + 11584 + '@opentelemetry/exporter-logs-otlp-http@0.208.0(@opentelemetry/api@1.9.0)': 11585 + dependencies: 11586 + '@opentelemetry/api': 1.9.0 11587 + '@opentelemetry/api-logs': 0.208.0 11588 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) 11589 + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) 11590 + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) 11591 + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) 11592 + 11593 + '@opentelemetry/otlp-exporter-base@0.208.0(@opentelemetry/api@1.9.0)': 11594 + dependencies: 11595 + '@opentelemetry/api': 1.9.0 11596 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) 11597 + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) 11598 + 11599 + '@opentelemetry/otlp-transformer@0.208.0(@opentelemetry/api@1.9.0)': 11600 + dependencies: 11601 + '@opentelemetry/api': 1.9.0 11602 + '@opentelemetry/api-logs': 0.208.0 11603 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) 11604 + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) 11605 + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) 11606 + '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) 11607 + '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) 11608 + protobufjs: 7.5.4 11609 + 11610 + '@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0)': 11611 + dependencies: 11612 + '@opentelemetry/api': 1.9.0 11613 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) 11614 + '@opentelemetry/semantic-conventions': 1.40.0 11615 + 11616 + '@opentelemetry/resources@2.5.1(@opentelemetry/api@1.9.0)': 11617 + dependencies: 11618 + '@opentelemetry/api': 1.9.0 11619 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) 11620 + '@opentelemetry/semantic-conventions': 1.40.0 11621 + 11622 + '@opentelemetry/sdk-logs@0.208.0(@opentelemetry/api@1.9.0)': 11623 + dependencies: 11624 + '@opentelemetry/api': 1.9.0 11625 + '@opentelemetry/api-logs': 0.208.0 11626 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) 11627 + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) 11628 + 11629 + '@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0)': 11630 + dependencies: 11631 + '@opentelemetry/api': 1.9.0 11632 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) 11633 + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) 11634 + 11635 + '@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0)': 11636 + dependencies: 11637 + '@opentelemetry/api': 1.9.0 11638 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) 11639 + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) 11640 + '@opentelemetry/semantic-conventions': 1.40.0 11641 + 11642 + '@opentelemetry/semantic-conventions@1.40.0': {} 11643 + 11342 11644 '@oxc-minify/binding-android-arm-eabi@0.111.0': 11343 11645 optional: true 11344 11646 ··· 11472 11774 11473 11775 '@pkgr/core@0.2.9': {} 11474 11776 11777 + '@posthog/core@1.23.1': 11778 + dependencies: 11779 + cross-spawn: 7.0.6 11780 + 11781 + '@posthog/react@1.8.1(@types/react@19.2.10)(posthog-js@1.356.1)(react@19.2.4)': 11782 + dependencies: 11783 + posthog-js: 1.356.1 11784 + react: 19.2.4 11785 + optionalDependencies: 11786 + '@types/react': 19.2.10 11787 + 11788 + '@posthog/types@1.356.1': {} 11789 + 11475 11790 '@prisma/adapter-pg@7.3.0': 11476 11791 dependencies: 11477 11792 '@prisma/driver-adapter-utils': 7.3.0 ··· 11558 11873 '@types/react': 19.2.10 11559 11874 react: 19.2.4 11560 11875 react-dom: 19.2.4(react@19.2.4) 11876 + 11877 + '@protobufjs/aspromise@1.1.2': {} 11878 + 11879 + '@protobufjs/base64@1.1.2': {} 11880 + 11881 + '@protobufjs/codegen@2.0.4': {} 11882 + 11883 + '@protobufjs/eventemitter@1.1.0': {} 11884 + 11885 + '@protobufjs/fetch@1.1.0': 11886 + dependencies: 11887 + '@protobufjs/aspromise': 1.1.2 11888 + '@protobufjs/inquire': 1.1.0 11889 + 11890 + '@protobufjs/float@1.0.2': {} 11891 + 11892 + '@protobufjs/inquire@1.1.0': {} 11893 + 11894 + '@protobufjs/path@1.1.2': {} 11895 + 11896 + '@protobufjs/pool@1.1.0': {} 11897 + 11898 + '@protobufjs/utf8@1.1.0': {} 11561 11899 11562 11900 '@radix-ui/number@1.1.1': {} 11563 11901 ··· 13380 13718 '@types/methods': 1.1.4 13381 13719 '@types/superagent': 8.1.9 13382 13720 13721 + '@types/trusted-types@2.0.7': 13722 + optional: true 13723 + 13383 13724 '@types/validator@13.15.10': {} 13384 13725 13385 13726 '@types/yargs-parser@21.0.3': {} ··· 14555 14896 dependencies: 14556 14897 domelementtype: 2.3.0 14557 14898 14899 + dompurify@3.3.1: 14900 + optionalDependencies: 14901 + '@types/trusted-types': 2.0.7 14902 + 14558 14903 domutils@3.2.2: 14559 14904 dependencies: 14560 14905 dom-serializer: 2.0.0 ··· 14752 15097 jest-mock: 30.2.0 14753 15098 jest-util: 30.2.0 14754 15099 15100 + expo-application@7.0.8(expo@54.0.33): 15101 + dependencies: 15102 + expo: 54.0.33(@babel/core@7.28.6)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) 15103 + 14755 15104 expo-asset@12.0.12(expo@54.0.33)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): 14756 15105 dependencies: 14757 15106 '@expo/image-utils': 0.8.8 ··· 14800 15149 expo: 54.0.33(@babel/core@7.28.6)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) 14801 15150 expo-dev-menu-interface: 2.0.0(expo@54.0.33) 14802 15151 15152 + expo-device@8.0.10(expo@54.0.33): 15153 + dependencies: 15154 + expo: 54.0.33(@babel/core@7.28.6)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) 15155 + ua-parser-js: 0.7.41 15156 + 14803 15157 expo-file-system@19.0.21(expo@54.0.33)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0)): 14804 15158 dependencies: 14805 15159 expo: 54.0.33(@babel/core@7.28.6)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) ··· 14846 15200 transitivePeerDependencies: 14847 15201 - expo 14848 15202 - supports-color 15203 + 15204 + expo-localization@17.0.8(expo@54.0.33)(react@19.1.0): 15205 + dependencies: 15206 + expo: 54.0.33(@babel/core@7.28.6)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) 15207 + react: 19.1.0 15208 + rtl-detect: 1.1.2 14849 15209 14850 15210 expo-manifests@1.0.10(expo@54.0.33): 14851 15211 dependencies: ··· 15100 15460 fdir@6.5.0(picomatch@4.0.3): 15101 15461 optionalDependencies: 15102 15462 picomatch: 4.0.3 15463 + 15464 + fflate@0.4.8: {} 15103 15465 15104 15466 file-type@16.5.4: 15105 15467 dependencies: ··· 17057 17419 17058 17420 postgres@3.4.7: {} 17059 17421 17422 + posthog-js@1.356.1: 17423 + dependencies: 17424 + '@opentelemetry/api': 1.9.0 17425 + '@opentelemetry/api-logs': 0.208.0 17426 + '@opentelemetry/exporter-logs-otlp-http': 0.208.0(@opentelemetry/api@1.9.0) 17427 + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) 17428 + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) 17429 + '@posthog/core': 1.23.1 17430 + '@posthog/types': 1.356.1 17431 + core-js: 3.48.0 17432 + dompurify: 3.3.1 17433 + fflate: 0.4.8 17434 + preact: 10.28.4 17435 + query-selector-shadow-dom: 1.0.1 17436 + web-vitals: 5.1.0 17437 + 17438 + posthog-react-native@4.36.1(@react-navigation/native@7.1.28(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(expo-application@7.0.8(expo@54.0.33))(expo-device@8.0.10(expo@54.0.33))(expo-file-system@19.0.21(expo@54.0.33)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0)))(expo-localization@17.0.8(expo@54.0.33)(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-svg@15.15.3(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)): 17439 + dependencies: 17440 + '@posthog/core': 1.23.1 17441 + react-native-svg: 15.15.3(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) 17442 + optionalDependencies: 17443 + '@react-navigation/native': 7.1.28(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) 17444 + expo-application: 7.0.8(expo@54.0.33) 17445 + expo-device: 8.0.10(expo@54.0.33) 17446 + expo-file-system: 19.0.21(expo@54.0.33)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0)) 17447 + expo-localization: 17.0.8(expo@54.0.33)(react@19.1.0) 17448 + react-native-safe-area-context: 5.6.2(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) 17449 + 17060 17450 powershell-utils@0.1.0: {} 17061 17451 17452 + preact@10.28.4: {} 17453 + 17062 17454 prebuild-install@7.1.3: 17063 17455 dependencies: 17064 17456 detect-libc: 2.1.2 ··· 17134 17526 retry: 0.12.0 17135 17527 signal-exit: 3.0.7 17136 17528 17529 + protobufjs@7.5.4: 17530 + dependencies: 17531 + '@protobufjs/aspromise': 1.1.2 17532 + '@protobufjs/base64': 1.1.2 17533 + '@protobufjs/codegen': 2.0.4 17534 + '@protobufjs/eventemitter': 1.1.0 17535 + '@protobufjs/fetch': 1.1.0 17536 + '@protobufjs/float': 1.0.2 17537 + '@protobufjs/inquire': 1.1.0 17538 + '@protobufjs/path': 1.1.2 17539 + '@protobufjs/pool': 1.1.0 17540 + '@protobufjs/utf8': 1.1.0 17541 + '@types/node': 22.19.7 17542 + long: 5.3.2 17543 + 17137 17544 proxy-addr@2.0.7: 17138 17545 dependencies: 17139 17546 forwarded: 0.2.0 ··· 17156 17563 qs@6.14.1: 17157 17564 dependencies: 17158 17565 side-channel: 1.1.0 17566 + 17567 + query-selector-shadow-dom@1.0.1: {} 17159 17568 17160 17569 query-string@7.1.3: 17161 17570 dependencies: ··· 17677 18086 transitivePeerDependencies: 17678 18087 - supports-color 17679 18088 18089 + rtl-detect@1.1.2: {} 18090 + 17680 18091 run-applescript@7.1.0: {} 17681 18092 17682 18093 rxjs@7.8.1: ··· 18337 18748 18338 18749 typescript@5.9.3: {} 18339 18750 18751 + ua-parser-js@0.7.41: {} 18752 + 18340 18753 ua-parser-js@1.0.41: {} 18341 18754 18342 18755 ufo@1.6.3: {} ··· 18628 19041 wcwidth@1.0.1: 18629 19042 dependencies: 18630 19043 defaults: 1.0.4 19044 + 19045 + web-vitals@5.1.0: {} 18631 19046 18632 19047 webidl-conversions@3.0.1: {} 18633 19048