feat: added posthog

This commit is contained in:
DESKTOP-RTLN3BA\$punk 2025-12-25 13:25:13 -08:00
parent 80e4f1b798
commit c96be7d9e1
18 changed files with 506 additions and 19 deletions

View file

@ -2,6 +2,7 @@
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect } from "react";
import { trackUserLoggedIn } from "@/lib/analytics";
import { getAndClearRedirectPath, setBearerToken } from "@/lib/auth-utils";
interface TokenHandlerProps {
@ -40,6 +41,10 @@ const TokenHandler = ({
localStorage.setItem(storageKey, token);
setBearerToken(token);
// Track successful login (works for both email and Google OAuth)
const authType = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE || "GOOGLE";
trackUserLoggedIn({ method: authType === "GOOGLE" ? "google" : "email" });
// Check if there's a saved redirect path from before the auth flow
const savedRedirectPath = getAndClearRedirectPath();

View file

@ -0,0 +1,76 @@
"use client";
import { useAtomValue } from "jotai";
import posthog from "posthog-js";
import { PostHogProvider as PHProvider, usePostHog } from "posthog-js/react";
import { useEffect, useRef } from "react";
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
// Initialize PostHog only on client side
if (typeof window !== "undefined" && process.env.NEXT_PUBLIC_POSTHOG_KEY) {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://us.i.posthog.com",
person_profiles: "identified_only",
capture_pageview: true,
capture_pageleave: true,
autocapture: false, // We'll use manual event tracking for better control
persistence: "localStorage",
loaded: (posthog) => {
if (process.env.NODE_ENV === "development") {
// Uncomment to debug in development
// posthog.debug();
}
},
});
}
/**
* Component that handles user identification with PostHog
* Placed inside the provider hierarchy to access user data
*/
function PostHogUserIdentifier() {
const ph = usePostHog();
const { data: user, isSuccess } = useAtomValue(currentUserAtom);
const hasIdentified = useRef(false);
useEffect(() => {
if (isSuccess && user && !hasIdentified.current) {
// Identify the user with PostHog
ph.identify(user.id, {
email: user.email,
is_active: user.is_active,
is_superuser: user.is_superuser,
is_verified: user.is_verified,
});
hasIdentified.current = true;
}
}, [ph, user, isSuccess]);
// Reset identification flag when user logs out (user becomes null)
useEffect(() => {
if (!user && hasIdentified.current) {
ph.reset();
hasIdentified.current = false;
}
}, [ph, user]);
return null;
}
/**
* PostHog Analytics Provider
* Wraps the app to enable analytics tracking and user identification
*/
export function PostHogProvider({ children }: { children: React.ReactNode }) {
// Don't render provider if PostHog key is not configured
if (!process.env.NEXT_PUBLIC_POSTHOG_KEY) {
return <>{children}</>;
}
return (
<PHProvider client={posthog}>
<PostHogUserIdentifier />
{children}
</PHProvider>
);
}

View file

@ -146,7 +146,7 @@ export function AppSidebarProvider({
setIsDeletingThread(true);
try {
await deleteThread(threadToDelete.id);
await deleteThread(threadToDelete.id, searchSpaceId);
// Invalidate threads query to refresh the list
queryClient.invalidateQueries({ queryKey: ["threads", searchSpaceId] });
} catch (error) {

View file

@ -121,7 +121,7 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
async (threadId: number) => {
setDeletingThreadId(threadId);
try {
await deleteThread(threadId);
await deleteThread(threadId, searchSpaceId);
toast.success(t("chat_deleted") || "Chat deleted successfully");
queryClient.invalidateQueries({ queryKey: ["all-threads", searchSpaceId] });
queryClient.invalidateQueries({ queryKey: ["search-threads", searchSpaceId] });

View file

@ -2,9 +2,11 @@
import { makeAssistantToolUI } from "@assistant-ui/react";
import { AlertCircleIcon, Loader2Icon, MicIcon } from "lucide-react";
import { useParams } from "next/navigation";
import { useCallback, useEffect, useRef, useState } from "react";
import { z } from "zod";
import { Audio } from "@/components/tool-ui/audio";
import { trackPodcastGenerated } from "@/lib/analytics";
import { baseApiService } from "@/lib/apis/base-api.service";
import { authenticatedFetch } from "@/lib/auth-utils";
import { clearActivePodcastTaskId, setActivePodcastTaskId } from "@/lib/chat/podcast-state";
@ -287,6 +289,9 @@ function PodcastPlayer({
function PodcastTaskPoller({ taskId, title }: { taskId: string; title: string }) {
const [taskStatus, setTaskStatus] = useState<TaskStatusResponse>({ status: "processing" });
const pollingRef = useRef<NodeJS.Timeout | null>(null);
const hasTrackedRef = useRef(false);
const params = useParams();
const searchSpaceId = params.search_space_id ? Number(params.search_space_id) : undefined;
// Set active podcast state when this component mounts
useEffect(() => {
@ -317,6 +322,20 @@ function PodcastTaskPoller({ taskId, title }: { taskId: string; title: string })
}
// Clear the active podcast state when task completes
clearActivePodcastTaskId();
// Track successful podcast generation (only once)
if (
response.status === "success" &&
response.podcast_id &&
searchSpaceId &&
!hasTrackedRef.current
) {
hasTrackedRef.current = true;
trackPodcastGenerated({
search_space_id: searchSpaceId,
podcast_id: response.podcast_id,
});
}
}
} catch (err) {
console.error("Error polling task status:", err);
@ -335,7 +354,7 @@ function PodcastTaskPoller({ taskId, title }: { taskId: string; title: string })
clearInterval(pollingRef.current);
}
};
}, [taskId]);
}, [taskId, searchSpaceId]);
// Show loading state while processing
if (taskStatus.status === "processing") {