diff --git a/surfsense_web/app/(home)/login/GoogleLoginButton.tsx b/surfsense_web/app/(home)/login/GoogleLoginButton.tsx index 6eef58aed..d4d9d4b4a 100644 --- a/surfsense_web/app/(home)/login/GoogleLoginButton.tsx +++ b/surfsense_web/app/(home)/login/GoogleLoginButton.tsx @@ -3,12 +3,16 @@ import { IconBrandGoogleFilled } from "@tabler/icons-react"; import { motion } from "motion/react"; import { useTranslations } from "next-intl"; import { Logo } from "@/components/Logo"; +import { trackLoginAttempt, trackLoginFailure } from "@/lib/posthog/events"; import { AmbientBackground } from "./AmbientBackground"; export function GoogleLoginButton() { const t = useTranslations("auth"); const handleGoogleLogin = () => { + // Track Google login attempt + trackLoginAttempt("google"); + // Redirect to Google OAuth authorization URL // credentials: 'include' is required to accept the CSRF cookie from cross-origin response fetch(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/auth/google/authorize`, { @@ -24,10 +28,12 @@ export function GoogleLoginButton() { if (data.authorization_url) { window.location.href = data.authorization_url; } else { + trackLoginFailure("google", "No authorization URL received"); console.error("No authorization URL received"); } }) .catch((error) => { + trackLoginFailure("google", error?.message || "Unknown error"); console.error("Error during Google login:", error); }); }; diff --git a/surfsense_web/app/(home)/login/LocalLoginForm.tsx b/surfsense_web/app/(home)/login/LocalLoginForm.tsx index 0157c9faf..44e9b27c2 100644 --- a/surfsense_web/app/(home)/login/LocalLoginForm.tsx +++ b/surfsense_web/app/(home)/login/LocalLoginForm.tsx @@ -10,6 +10,7 @@ import { toast } from "sonner"; import { loginMutationAtom } from "@/atoms/auth/auth-mutation.atoms"; import { getAuthErrorDetails, isNetworkError, shouldRetry } from "@/lib/auth-errors"; import { ValidationError } from "@/lib/error"; +import { trackLoginAttempt, trackLoginFailure, trackLoginSuccess } from "@/lib/posthog/events"; export function LocalLoginForm() { const t = useTranslations("auth"); @@ -37,6 +38,9 @@ export function LocalLoginForm() { e.preventDefault(); setError({ title: null, message: null }); // Clear any previous errors + // Track login attempt + trackLoginAttempt("local"); + // Show loading toast const loadingToast = toast.loading(tCommon("loading")); @@ -47,6 +51,9 @@ export function LocalLoginForm() { grant_type: "password", }); + // Track successful login + trackLoginSuccess("local"); + // Success toast toast.success(t("login_success"), { id: loadingToast, @@ -60,6 +67,7 @@ export function LocalLoginForm() { }, 500); } catch (err) { if (err instanceof ValidationError) { + trackLoginFailure("local", err.message); setError({ title: err.name, message: err.message }); toast.error(err.name, { id: loadingToast, @@ -78,6 +86,9 @@ export function LocalLoginForm() { errorCode = "NETWORK_ERROR"; } + // Track login failure + trackLoginFailure("local", errorCode); + // Get detailed error information from auth-errors utility const errorDetails = getAuthErrorDetails(errorCode); diff --git a/surfsense_web/app/(home)/register/page.tsx b/surfsense_web/app/(home)/register/page.tsx index c535832be..4a8dce546 100644 --- a/surfsense_web/app/(home)/register/page.tsx +++ b/surfsense_web/app/(home)/register/page.tsx @@ -11,6 +11,11 @@ import { registerMutationAtom } from "@/atoms/auth/auth-mutation.atoms"; import { Logo } from "@/components/Logo"; import { getAuthErrorDetails, isNetworkError, shouldRetry } from "@/lib/auth-errors"; import { AppError, ValidationError } from "@/lib/error"; +import { + trackRegistrationAttempt, + trackRegistrationFailure, + trackRegistrationSuccess, +} from "@/lib/posthog/events"; import { AmbientBackground } from "../login/AmbientBackground"; export default function RegisterPage() { @@ -52,6 +57,9 @@ export default function RegisterPage() { setError({ title: null, message: null }); // Clear any previous errors + // Track registration attempt + trackRegistrationAttempt(); + // Show loading toast const loadingToast = toast.loading(t("creating_account")); @@ -64,6 +72,9 @@ export default function RegisterPage() { is_verified: false, }); + // Track successful registration + trackRegistrationSuccess(); + // Success toast toast.success(t("register_success"), { id: loadingToast, @@ -81,6 +92,7 @@ export default function RegisterPage() { case 403: { const friendlyMessage = "Registrations are currently closed. If you need access, contact your administrator."; + trackRegistrationFailure("Registration disabled"); setError({ title: "Registration is disabled", message: friendlyMessage }); toast.error("Registration is disabled", { id: loadingToast, @@ -94,6 +106,7 @@ export default function RegisterPage() { } if (err instanceof ValidationError) { + trackRegistrationFailure(err.message); setError({ title: err.name, message: err.message }); toast.error(err.name, { id: loadingToast, @@ -113,6 +126,9 @@ export default function RegisterPage() { errorCode = "NETWORK_ERROR"; } + // Track registration failure + trackRegistrationFailure(errorCode); + // Get detailed error information from auth-errors utility const errorDetails = getAuthErrorDetails(errorCode); diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx index f288bfa59..dbea83fb7 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx @@ -37,6 +37,12 @@ import { getThreadMessages, type MessageRecord, } from "@/lib/chat/thread-persistence"; +import { + trackChatCreated, + trackChatError, + trackChatMessageSent, + trackChatResponseReceived, +} from "@/lib/posthog/events"; /** * Extract thinking steps from message content @@ -305,6 +311,10 @@ export default function NewChatPage() { const newThread = await createThread(searchSpaceId, "New Chat"); currentThreadId = newThread.id; setThreadId(currentThreadId); + + // Track chat creation + trackChatCreated(searchSpaceId, currentThreadId); + // Update URL silently using browser API (not router.replace) to avoid // interrupting the ongoing fetch/streaming with React navigation window.history.replaceState( @@ -331,6 +341,13 @@ export default function NewChatPage() { }; setMessages((prev) => [...prev, userMessage]); + // Track message sent + trackChatMessageSent(searchSpaceId, currentThreadId, { + hasAttachments: messageAttachments.length > 0, + hasMentionedDocuments: mentionedDocumentIds.length > 0, + messageLength: userQuery.length, + }); + // Store mentioned documents with this message for display if (mentionedDocuments.length > 0) { const docsInfo: MentionedDocumentInfo[] = mentionedDocuments.map((doc) => ({ @@ -653,6 +670,9 @@ export default function NewChatPage() { role: "assistant", content: finalContent, }).catch((err) => console.error("Failed to persist assistant message:", err)); + + // Track successful response + trackChatResponseReceived(searchSpaceId, currentThreadId); } } catch (error) { if (error instanceof Error && error.name === "AbortError") { @@ -660,6 +680,14 @@ export default function NewChatPage() { return; } console.error("[NewChatPage] Chat error:", error); + + // Track chat error + trackChatError( + searchSpaceId, + currentThreadId, + error instanceof Error ? error.message : "Unknown error" + ); + toast.error("Failed to get response. Please try again."); // Update assistant message with error setMessages((prev) => diff --git a/surfsense_web/app/dashboard/[search_space_id]/settings/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/settings/page.tsx index ad96402a4..48efcd922 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/settings/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/settings/page.tsx @@ -13,11 +13,12 @@ import { } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { LLMRoleManager } from "@/components/settings/llm-role-manager"; import { ModelConfigManager } from "@/components/settings/model-config-manager"; import { PromptConfigManager } from "@/components/settings/prompt-config-manager"; import { Button } from "@/components/ui/button"; +import { trackSettingsViewed } from "@/lib/posthog/events"; import { cn } from "@/lib/utils"; interface SettingsNavItem { @@ -271,6 +272,11 @@ export default function SettingsPage() { const [activeSection, setActiveSection] = useState("models"); const [isSidebarOpen, setIsSidebarOpen] = useState(false); + // Track settings section view + useEffect(() => { + trackSettingsViewed(searchSpaceId, activeSection); + }, [searchSpaceId, activeSection]); + const handleBackToApp = useCallback(() => { router.push(`/dashboard/${searchSpaceId}/new-chat`); }, [router, searchSpaceId]); diff --git a/surfsense_web/app/dashboard/[search_space_id]/sources/add/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/sources/add/page.tsx index 3c9b57f98..e0729e29b 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/sources/add/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/sources/add/page.tsx @@ -9,6 +9,7 @@ import { ConnectorsTab } from "@/components/sources/ConnectorsTab"; import { DocumentUploadTab } from "@/components/sources/DocumentUploadTab"; import { YouTubeTab } from "@/components/sources/YouTubeTab"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { trackSourcesTabViewed } from "@/lib/posthog/events"; export default function AddSourcesPage() { const params = useParams(); @@ -30,9 +31,16 @@ export default function AddSourcesPage() { router.push(`/dashboard/${search_space_id}/connectors/add/webcrawler-connector`); } else { setActiveTab(value); + // Track tab view + trackSourcesTabViewed(Number(search_space_id), value); } }; + // Track initial tab view + useEffect(() => { + trackSourcesTabViewed(Number(search_space_id), activeTab); + }, []); + return (
- - - - - {children} - - - - - + + + + + + {children} + + + + + + ); diff --git a/surfsense_web/components/providers/PostHogProvider.tsx b/surfsense_web/components/providers/PostHogProvider.tsx new file mode 100644 index 000000000..6f62afce7 --- /dev/null +++ b/surfsense_web/components/providers/PostHogProvider.tsx @@ -0,0 +1,15 @@ +"use client"; + +import { PostHogProvider as PHProvider } from "@posthog/react"; +import posthog from "posthog-js"; +import type { ReactNode } from "react"; + +interface PostHogProviderProps { + children: ReactNode; +} + +export function PostHogProvider({ children }: PostHogProviderProps) { + // posthog-js is already initialized in instrumentation-client.ts + // We just need to wrap the app with the PostHogProvider for hook access + return {children}; +} diff --git a/surfsense_web/components/sources/DocumentUploadTab.tsx b/surfsense_web/components/sources/DocumentUploadTab.tsx index 7ad82d124..168db35cc 100644 --- a/surfsense_web/components/sources/DocumentUploadTab.tsx +++ b/surfsense_web/components/sources/DocumentUploadTab.tsx @@ -9,13 +9,17 @@ import { useCallback, useState } from "react"; import { useDropzone } from "react-dropzone"; import { toast } from "sonner"; import { uploadDocumentMutationAtom } from "@/atoms/documents/document-mutation.atoms"; - import { Alert, AlertDescription } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Progress } from "@/components/ui/progress"; import { Separator } from "@/components/ui/separator"; +import { + trackDocumentUploadFailure, + trackDocumentUploadStarted, + trackDocumentUploadSuccess, +} from "@/lib/posthog/events"; import { GridPattern } from "./GridPattern"; interface DocumentUploadTabProps { @@ -154,6 +158,10 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) { const handleUpload = async () => { setUploadProgress(0); + // Track upload started + const totalSize = files.reduce((sum, file) => sum + file.size, 0); + trackDocumentUploadStarted(Number(searchSpaceId), files.length, totalSize); + // Create a progress interval to simulate progress const progressInterval = setInterval(() => { setUploadProgress((prev) => { @@ -172,6 +180,10 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) { onSuccess: () => { clearInterval(progressInterval); setUploadProgress(100); + + // Track upload success + trackDocumentUploadSuccess(Number(searchSpaceId), files.length); + toast(t("upload_initiated"), { description: t("upload_initiated_desc"), }); @@ -180,6 +192,10 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) { onError: (error: any) => { clearInterval(progressInterval); setUploadProgress(0); + + // Track upload failure + trackDocumentUploadFailure(Number(searchSpaceId), error.message || "Upload failed"); + toast(t("upload_error"), { description: `${t("upload_error_desc")}: ${error.message || "Upload failed"}`, }); diff --git a/surfsense_web/instrumentation-client.ts b/surfsense_web/instrumentation-client.ts new file mode 100644 index 000000000..0eec5eabe --- /dev/null +++ b/surfsense_web/instrumentation-client.ts @@ -0,0 +1,13 @@ +import posthog from "posthog-js"; + +if (process.env.NEXT_PUBLIC_POSTHOG_KEY) { + posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, { + api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST, + defaults: "2025-11-30", + // Disable automatic pageview capture, as we capture manually with PostHogProvider + // This ensures proper pageview tracking with Next.js client-side navigation + capture_pageview: "history_change", + // Enable session recording + capture_pageleave: true, + }); +} diff --git a/surfsense_web/lib/posthog/events.ts b/surfsense_web/lib/posthog/events.ts new file mode 100644 index 000000000..fae713f80 --- /dev/null +++ b/surfsense_web/lib/posthog/events.ts @@ -0,0 +1,291 @@ +import posthog from "posthog-js"; + +/** + * PostHog Analytics Event Definitions + * + * This file defines all custom analytics events tracked in SurfSense. + * Events follow a consistent naming convention: category_action + * + * Categories: + * - auth: Authentication events + * - search_space: Search space management + * - document: Document management + * - chat: Chat and messaging + * - connector: External connector events + * - contact: Contact form events + * - settings: Settings changes + */ + +// ============================================ +// AUTH EVENTS +// ============================================ + +export function trackLoginAttempt(method: "local" | "google") { + posthog.capture("auth_login_attempt", { + method, + }); +} + +export function trackLoginSuccess(method: "local" | "google") { + posthog.capture("auth_login_success", { + method, + }); +} + +export function trackLoginFailure(method: "local" | "google", error?: string) { + posthog.capture("auth_login_failure", { + method, + error, + }); +} + +export function trackRegistrationAttempt() { + posthog.capture("auth_registration_attempt"); +} + +export function trackRegistrationSuccess() { + posthog.capture("auth_registration_success"); +} + +export function trackRegistrationFailure(error?: string) { + posthog.capture("auth_registration_failure", { + error, + }); +} + +export function trackLogout() { + posthog.capture("auth_logout"); +} + +// ============================================ +// SEARCH SPACE EVENTS +// ============================================ + +export function trackSearchSpaceCreated(searchSpaceId: number, name: string) { + posthog.capture("search_space_created", { + search_space_id: searchSpaceId, + name, + }); +} + +export function trackSearchSpaceDeleted(searchSpaceId: number) { + posthog.capture("search_space_deleted", { + search_space_id: searchSpaceId, + }); +} + +export function trackSearchSpaceViewed(searchSpaceId: number) { + posthog.capture("search_space_viewed", { + search_space_id: searchSpaceId, + }); +} + +// ============================================ +// CHAT EVENTS +// ============================================ + +export function trackChatCreated(searchSpaceId: number, chatId: number) { + posthog.capture("chat_created", { + search_space_id: searchSpaceId, + chat_id: chatId, + }); +} + +export function trackChatMessageSent( + searchSpaceId: number, + chatId: number, + options?: { + hasAttachments?: boolean; + hasMentionedDocuments?: boolean; + messageLength?: number; + } +) { + posthog.capture("chat_message_sent", { + search_space_id: searchSpaceId, + chat_id: chatId, + has_attachments: options?.hasAttachments ?? false, + has_mentioned_documents: options?.hasMentionedDocuments ?? false, + message_length: options?.messageLength, + }); +} + +export function trackChatResponseReceived(searchSpaceId: number, chatId: number) { + posthog.capture("chat_response_received", { + search_space_id: searchSpaceId, + chat_id: chatId, + }); +} + +export function trackChatError(searchSpaceId: number, chatId: number, error?: string) { + posthog.capture("chat_error", { + search_space_id: searchSpaceId, + chat_id: chatId, + error, + }); +} + +// ============================================ +// DOCUMENT EVENTS +// ============================================ + +export function trackDocumentUploadStarted( + searchSpaceId: number, + fileCount: number, + totalSizeBytes: number +) { + posthog.capture("document_upload_started", { + search_space_id: searchSpaceId, + file_count: fileCount, + total_size_bytes: totalSizeBytes, + }); +} + +export function trackDocumentUploadSuccess(searchSpaceId: number, fileCount: number) { + posthog.capture("document_upload_success", { + search_space_id: searchSpaceId, + file_count: fileCount, + }); +} + +export function trackDocumentUploadFailure(searchSpaceId: number, error?: string) { + posthog.capture("document_upload_failure", { + search_space_id: searchSpaceId, + error, + }); +} + +export function trackDocumentDeleted(searchSpaceId: number, documentId: number) { + posthog.capture("document_deleted", { + search_space_id: searchSpaceId, + document_id: documentId, + }); +} + +export function trackDocumentBulkDeleted(searchSpaceId: number, count: number) { + posthog.capture("document_bulk_deleted", { + search_space_id: searchSpaceId, + count, + }); +} + +export function trackYouTubeImport(searchSpaceId: number, url: string) { + posthog.capture("youtube_import_started", { + search_space_id: searchSpaceId, + url, + }); +} + +// ============================================ +// CONNECTOR EVENTS +// ============================================ + +export function trackConnectorSetupStarted(searchSpaceId: number, connectorType: string) { + posthog.capture("connector_setup_started", { + search_space_id: searchSpaceId, + connector_type: connectorType, + }); +} + +export function trackConnectorSetupSuccess( + searchSpaceId: number, + connectorType: string, + connectorId: number +) { + posthog.capture("connector_setup_success", { + search_space_id: searchSpaceId, + connector_type: connectorType, + connector_id: connectorId, + }); +} + +export function trackConnectorSetupFailure( + searchSpaceId: number, + connectorType: string, + error?: string +) { + posthog.capture("connector_setup_failure", { + search_space_id: searchSpaceId, + connector_type: connectorType, + error, + }); +} + +export function trackConnectorDeleted( + searchSpaceId: number, + connectorType: string, + connectorId: number +) { + posthog.capture("connector_deleted", { + search_space_id: searchSpaceId, + connector_type: connectorType, + connector_id: connectorId, + }); +} + +export function trackConnectorSynced( + searchSpaceId: number, + connectorType: string, + connectorId: number +) { + posthog.capture("connector_synced", { + search_space_id: searchSpaceId, + connector_type: connectorType, + connector_id: connectorId, + }); +} + +// ============================================ +// SETTINGS EVENTS +// ============================================ + +export function trackSettingsViewed(searchSpaceId: number, section: string) { + posthog.capture("settings_viewed", { + search_space_id: searchSpaceId, + section, + }); +} + +export function trackSettingsUpdated(searchSpaceId: number, section: string, setting: string) { + posthog.capture("settings_updated", { + search_space_id: searchSpaceId, + section, + setting, + }); +} + +// ============================================ +// FEATURE USAGE EVENTS +// ============================================ + +export function trackPodcastGenerated(searchSpaceId: number, chatId: number) { + posthog.capture("podcast_generated", { + search_space_id: searchSpaceId, + chat_id: chatId, + }); +} + +export function trackSourcesTabViewed(searchSpaceId: number, tab: string) { + posthog.capture("sources_tab_viewed", { + search_space_id: searchSpaceId, + tab, + }); +} + +// ============================================ +// USER IDENTIFICATION +// ============================================ + +/** + * Identify a user for PostHog analytics + * Call this after successful authentication + */ +export function identifyUser(userId: string, properties?: Record) { + posthog.identify(userId, properties); +} + +/** + * Reset user identity (call on logout) + */ +export function resetUser() { + posthog.reset(); +} diff --git a/surfsense_web/lib/posthog/server.ts b/surfsense_web/lib/posthog/server.ts new file mode 100644 index 000000000..5bfd79ca7 --- /dev/null +++ b/surfsense_web/lib/posthog/server.ts @@ -0,0 +1,17 @@ +import { PostHog } from "posthog-node"; + +export default function PostHogClient() { + if (!process.env.NEXT_PUBLIC_POSTHOG_KEY) { + throw new Error("NEXT_PUBLIC_POSTHOG_KEY is not set"); + } + + const posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY, { + host: process.env.NEXT_PUBLIC_POSTHOG_HOST, + // Because server-side functions in Next.js can be short-lived, + // we set flushAt to 1 and flushInterval to 0 to ensure events are sent immediately + flushAt: 1, + flushInterval: 0, + }); + + return posthogClient; +} diff --git a/surfsense_web/package.json b/surfsense_web/package.json index ed4c762a3..5d602c2ab 100644 --- a/surfsense_web/package.json +++ b/surfsense_web/package.json @@ -32,6 +32,7 @@ "@hookform/resolvers": "^5.2.2", "@next/third-parties": "^16.1.0", "@number-flow/react": "^0.5.10", + "@posthog/react": "^1.5.2", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-avatar": "^1.1.10", @@ -81,6 +82,8 @@ "next-themes": "^0.4.6", "pg": "^8.16.3", "postgres": "^3.4.7", + "posthog-js": "^1.310.1", + "posthog-node": "^5.18.0", "react": "^19.2.3", "react-day-picker": "^9.8.1", "react-dom": "^19.2.3", diff --git a/surfsense_web/pnpm-lock.yaml b/surfsense_web/pnpm-lock.yaml index 22a251e12..a94b63c0d 100644 --- a/surfsense_web/pnpm-lock.yaml +++ b/surfsense_web/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: '@number-flow/react': specifier: ^0.5.10 version: 0.5.10(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@posthog/react': + specifier: ^1.5.2 + version: 1.5.2(@types/react@19.2.7)(posthog-js@1.310.1)(react@19.2.3) '@radix-ui/react-accordion': specifier: ^1.2.11 version: 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -188,6 +191,12 @@ importers: postgres: specifier: ^3.4.7 version: 3.4.7 + posthog-js: + specifier: ^1.310.1 + version: 1.310.1 + posthog-node: + specifier: ^5.18.0 + version: 5.18.0 react: specifier: ^19.2.3 version: 19.2.3 @@ -1492,6 +1501,19 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} + '@posthog/core@1.9.0': + resolution: {integrity: sha512-j7KSWxJTUtNyKynLt/p0hfip/3I46dWU2dk+pt7dKRoz2l5CYueHuHK4EO7Wlgno5yo1HO4sc4s30MXMTICHJw==} + + '@posthog/react@1.5.2': + resolution: {integrity: sha512-KHdXbV1yba7Y2l8BVmwXlySWxqKVLNQ5ZiVvWOf7r3Eo7GIFxCM4CaNK/z83kKWn8KTskmKy7AGF6Hl6INWK3g==} + peerDependencies: + '@types/react': '>=16.8.0' + posthog-js: '>=1.257.2' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -3273,6 +3295,9 @@ packages: confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + core-js@3.47.0: + resolution: {integrity: sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==} + cose-base@1.0.3: resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} @@ -3933,6 +3958,9 @@ packages: picomatch: optional: true + fflate@0.4.8: + resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -5215,6 +5243,16 @@ packages: resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} engines: {node: '>=12'} + posthog-js@1.310.1: + resolution: {integrity: sha512-UkR6zzlWNtqHDXHJl2Yk062DOmZyVKTPL5mX4j4V+u3RiYbMHJe47+PpMMUsvK1R2e1r/m9uSlHaJMJRzyUjGg==} + + posthog-node@5.18.0: + resolution: {integrity: sha512-SLBEs+sCThxzTGSSDEe97nZHuFFYh6DupObR1yQdvQND3CJh0ogZ0Sa1Vb+Tbrnf0cWbfBC9XNkm44yhaWf3aA==} + engines: {node: '>=20'} + + preact@10.28.1: + resolution: {integrity: sha512-u1/ixq/lVQI0CakKNvLDEcW5zfCjUQfZdK9qqWuIJtsezuyG6pk9TWj75GMuI/EzRSZB/VAE43sNWWZfiy8psw==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -6090,6 +6128,9 @@ packages: web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + web-vitals@4.2.4: + resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} + webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} @@ -7264,6 +7305,17 @@ snapshots: '@parcel/watcher-win32-ia32': 2.5.1 '@parcel/watcher-win32-x64': 2.5.1 + '@posthog/core@1.9.0': + dependencies: + cross-spawn: 7.0.6 + + '@posthog/react@1.5.2(@types/react@19.2.7)(posthog-js@1.310.1)(react@19.2.3)': + dependencies: + posthog-js: 1.310.1 + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.7 + '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.0.0': @@ -9110,6 +9162,8 @@ snapshots: confbox@0.1.8: {} + core-js@3.47.0: {} + cose-base@1.0.3: dependencies: layout-base: 1.0.2 @@ -9939,6 +9993,8 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fflate@0.4.8: {} + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -11662,6 +11718,20 @@ snapshots: postgres@3.4.7: {} + posthog-js@1.310.1: + dependencies: + '@posthog/core': 1.9.0 + core-js: 3.47.0 + fflate: 0.4.8 + preact: 10.28.1 + web-vitals: 4.2.4 + + posthog-node@5.18.0: + dependencies: + '@posthog/core': 1.9.0 + + preact@10.28.1: {} + prelude-ls@1.2.1: {} prismjs@1.27.0: {} @@ -12767,6 +12837,8 @@ snapshots: web-namespaces@2.0.1: {} + web-vitals@4.2.4: {} + webidl-conversions@7.0.0: {} whatwg-encoding@3.1.1: