feat: enhance login tracking and logout functionality

- Added session storage flag to track local login success, ensuring OAuth flows do not double track login events.
- Implemented tracking for logout events in both UserDropdown and AppSidebar components, resetting PostHog identity accordingly.
- Minor formatting adjustments in GoogleLoginButton and footer-new components for consistency.
This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-01-02 01:10:16 -08:00
parent 458c152032
commit d20aef2957
8 changed files with 84 additions and 4 deletions

View file

@ -14,7 +14,7 @@ export function GoogleLoginButton() {
trackLoginAttempt("google"); trackLoginAttempt("google");
// IMPORTANT: Use the redirect-based authorize endpoint for cross-origin OAuth // IMPORTANT: Use the redirect-based authorize endpoint for cross-origin OAuth
// This fixes CSRF cookie issues in Firefox/Safari where cookies set via // This fixes CSRF cookie issues in Firefox/Safari where cookies set via
// cross-origin fetch requests may not be sent on subsequent redirects. // cross-origin fetch requests may not be sent on subsequent redirects.
// The authorize-redirect endpoint does a server-side redirect to Google // The authorize-redirect endpoint does a server-side redirect to Google
// and sets the CSRF cookie properly for same-site context. // and sets the CSRF cookie properly for same-site context.

View file

@ -54,6 +54,11 @@ export function LocalLoginForm() {
// Track successful login // Track successful login
trackLoginSuccess("local"); trackLoginSuccess("local");
// Set flag so TokenHandler knows local login was already tracked
if (typeof window !== "undefined") {
sessionStorage.setItem("login_success_tracked", "true");
}
// Success toast // Success toast
toast.success(t("login_success"), { toast.success(t("login_success"), {
id: loadingToast, id: loadingToast,

View file

@ -3,6 +3,7 @@
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
import { getAndClearRedirectPath, setBearerToken } from "@/lib/auth-utils"; import { getAndClearRedirectPath, setBearerToken } from "@/lib/auth-utils";
import { trackLoginSuccess } from "@/lib/posthog/events";
interface TokenHandlerProps { interface TokenHandlerProps {
redirectPath?: string; // Default path to redirect after storing token (if no saved path) redirectPath?: string; // Default path to redirect after storing token (if no saved path)
@ -36,6 +37,16 @@ const TokenHandler = ({
if (token) { if (token) {
try { try {
// Track login success for OAuth flows (e.g., Google)
// Local login already tracks success before redirecting here
const alreadyTracked = sessionStorage.getItem("login_success_tracked");
if (!alreadyTracked) {
// This is an OAuth flow (Google login) - track success
trackLoginSuccess("google");
}
// Clear the flag for future logins
sessionStorage.removeItem("login_success_tracked");
// Store token in localStorage using both methods for compatibility // Store token in localStorage using both methods for compatibility
localStorage.setItem(storageKey, token); localStorage.setItem(storageKey, token);
setBearerToken(token); setBearerToken(token);

View file

@ -13,6 +13,7 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { resetUser, trackLogout } from "@/lib/posthog/events";
export function UserDropdown({ export function UserDropdown({
user, user,
@ -27,6 +28,10 @@ export function UserDropdown({
const handleLogout = () => { const handleLogout = () => {
try { try {
// Track logout event and reset PostHog identity
trackLogout();
resetUser();
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
localStorage.removeItem("surfsense_bearer_token"); localStorage.removeItem("surfsense_bearer_token");
router.push("/"); router.push("/");

View file

@ -96,9 +96,8 @@ export function FooterNew() {
</div> </div>
<div className="mt-2 ml-2"> <div className="mt-2 ml-2">
&copy; SurfSense {new Date().getFullYear()}. All rights reserved. &copy; SurfSense {new Date().getFullYear()}. All rights reserved.
</div> </div>
</div> </div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-10 items-start mt-10 sm:mt-0 md:mt-0"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-10 items-start mt-10 sm:mt-0 md:mt-0">
<div className="flex justify-center space-y-4 flex-col w-full"> <div className="flex justify-center space-y-4 flex-col w-full">

View file

@ -0,0 +1,49 @@
"use client";
import { useAtomValue } from "jotai";
import { useEffect, useRef } from "react";
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
import { identifyUser, resetUser } from "@/lib/posthog/events";
/**
* Component that handles PostHog user identification.
* - Identifies users when they're logged in (user data is available)
* - Resets the PostHog identity when user logs out
*
* This should be rendered inside the PostHogProvider.
*/
export function PostHogIdentify() {
const { data: user, isSuccess, isError } = useAtomValue(currentUserAtom);
const previousUserIdRef = useRef<string | null>(null);
useEffect(() => {
// Only run on client side
if (typeof window === "undefined") return;
// User is logged in and we have their data
if (isSuccess && user?.id) {
const userId = String(user.id);
// Only identify if this is a new user or different from previous
if (previousUserIdRef.current !== userId) {
identifyUser(userId, {
email: user.email,
// Add any other user properties you want to track
is_superuser: user.is_superuser,
is_verified: user.is_verified,
});
previousUserIdRef.current = userId;
}
}
// User is not logged in (query failed due to auth error)
// and we previously had a user identified
if (isError && previousUserIdRef.current !== null) {
resetUser();
previousUserIdRef.current = null;
}
}, [user, isSuccess, isError]);
// This component doesn't render anything
return null;
}

View file

@ -3,6 +3,7 @@
import { PostHogProvider as PHProvider } from "@posthog/react"; import { PostHogProvider as PHProvider } from "@posthog/react";
import posthog from "posthog-js"; import posthog from "posthog-js";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { PostHogIdentify } from "./PostHogIdentify";
interface PostHogProviderProps { interface PostHogProviderProps {
children: ReactNode; children: ReactNode;
@ -11,5 +12,10 @@ interface PostHogProviderProps {
export function PostHogProvider({ children }: PostHogProviderProps) { export function PostHogProvider({ children }: PostHogProviderProps) {
// posthog-js is already initialized in instrumentation-client.ts // posthog-js is already initialized in instrumentation-client.ts
// We just need to wrap the app with the PostHogProvider for hook access // We just need to wrap the app with the PostHogProvider for hook access
return <PHProvider client={posthog}>{children}</PHProvider>; return (
<PHProvider client={posthog}>
<PostHogIdentify />
{children}
</PHProvider>
);
} }

View file

@ -42,6 +42,7 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { resetUser, trackLogout } from "@/lib/posthog/events";
/** /**
* Generates a consistent color based on a string (email) * Generates a consistent color based on a string (email)
@ -343,6 +344,10 @@ export const AppSidebar = memo(function AppSidebar({
const handleLogout = () => { const handleLogout = () => {
try { try {
// Track logout event and reset PostHog identity
trackLogout();
resetUser();
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
localStorage.removeItem("surfsense_bearer_token"); localStorage.removeItem("surfsense_bearer_token");
router.push("/"); router.push("/");