diff --git a/apps/app/app/api/mixpanel/identify/route.ts b/apps/app/app/api/mixpanel/identify/route.ts new file mode 100644 index 00000000..0a7a568e --- /dev/null +++ b/apps/app/app/api/mixpanel/identify/route.ts @@ -0,0 +1,103 @@ +import { NextResponse } from "next/server"; +import { app, mixpanel } from "@/lib/env"; +import type { Mixpanel } from "mixpanel"; +const MixpanelLib = require("mixpanel"); + +let mixpanelClient: Mixpanel | null = null; +if (mixpanel.projectToken) { + mixpanelClient = MixpanelLib.init(mixpanel.projectToken); +} + +async function getGeoData(ip: string | null) { + if (!ip) return {}; + + try { + const response = await fetch(`http://ip-api.com/json/${ip}`); + const data = await response.json(); + return { + $city: data.city, + $region: data.regionName, + $country_code: data.countryCode, + $latitude: data.lat, + $longitude: data.lon, + }; + } catch (error) { + console.error("Error getting geolocation:", error); + return {}; + } +} + + +export async function POST(request: Request) { + console.log("mixpanel identify request", request); + if (!mixpanelClient) { + return NextResponse.json( + { error: "Mixpanel not configured" }, + { status: 500 } + ); + } + + try { + const { userId, anonymousId, properties } = await request.json(); + const { first_time_properties, ...regularProperties } = properties; + console.log("mixpanel identify request", userId, anonymousId, properties, regularProperties); + // Create alias if needed + if (anonymousId !== userId) { + mixpanelClient.alias(userId, anonymousId); + } + + const forwardedFor = request.headers.get("x-forwarded-for"); + + const ip = + app.environment === "dev" + ? "93.152.210.100" // Hardcoded development IP (truncated ip that resolves to San Francisco) + : forwardedFor + ? forwardedFor.split(",")[0].trim() + : "127.0.0.1"; + + let geoData = {}; + if (ip && ip !== "127.0.0.1" && ip !== "::1") { + geoData = await getGeoData(ip); + } + + // add geo data to regular properties + const setProperties = { + ...regularProperties, + ...geoData + } + + // Track identify event + console.log("mixpanelClient.track('$identify', { distinct_id: userId, ...regularProperties })"); + mixpanelClient.track('$identify', { + distinct_id: userId, + ...setProperties + }); + + // Set regular properties + mixpanelClient.people.set(userId, setProperties); + + // Set first-time properties that should only be set once + if (first_time_properties) { + mixpanelClient.people.set_once(userId, first_time_properties); + } + + return NextResponse.json({ status: "User identified successfully" }); + } catch (error) { + console.error("Error identifying user:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 } + ); + } +} + +export async function OPTIONS() { + return new NextResponse(null, { + status: 200, + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", + }, + }); +} \ No newline at end of file diff --git a/apps/app/app/layout.tsx b/apps/app/app/layout.tsx index 402bb163..550500d1 100644 --- a/apps/app/app/layout.tsx +++ b/apps/app/app/layout.tsx @@ -10,7 +10,6 @@ import Intercom from "@/components/intercom"; import { AlarmCheck } from "lucide-react"; import AlphaBanner from "@/components/header/alpha-banner"; import { Metadata } from "next"; -import SessionTracker from "@/components/analytics/SessionTracker"; import { MixpanelProvider } from "@/components/analytics/MixpanelProvider"; import { VersionInfo } from '@/components/footer/version-info'; @@ -27,7 +26,6 @@ const RootLayout = ({ children }: RootLayoutProperties) => ( <body className="bg-sidebar"> <DesignSystemProvider defaultTheme="dark"> <MixpanelProvider> - <SessionTracker /> <AlphaBanner /> <SidebarProvider> <GlobalSidebar> diff --git a/apps/app/app/page.tsx b/apps/app/app/page.tsx index 376b2582..30fff8e4 100644 --- a/apps/app/app/page.tsx +++ b/apps/app/app/page.tsx @@ -5,6 +5,7 @@ import { type ReactElement, Suspense } from "react"; import FeaturedPipelines from "@/components/welcome/featured"; import { validateEnv } from "@/lib/env"; import {validateServerEnv} from "@/lib/serverEnv"; +import ClientSideTracker from "@/components/analytics/ClientSideTracker"; const App = async ({ searchParams, @@ -16,6 +17,7 @@ const App = async ({ return ( <div> + <ClientSideTracker eventName="home_page_viewed" /> <div className="flex-shrink-0"> <Suspense> <Welcome /> diff --git a/apps/app/components/analytics/MixpanelProvider.tsx b/apps/app/components/analytics/MixpanelProvider.tsx index fbbaed91..f3b8a560 100644 --- a/apps/app/components/analytics/MixpanelProvider.tsx +++ b/apps/app/components/analytics/MixpanelProvider.tsx @@ -3,12 +3,57 @@ import { ReactNode, useEffect } from 'react'; import mixpanel from 'mixpanel-browser'; import { mixpanel as mixpanelConfig } from '@/lib/env'; +import { usePrivy } from '@privy-io/react-auth'; + +async function identifyUser(userId: string, anonymousId: string, user: any) { + try { + const payload = { + userId, + anonymousId, + properties: { + $name: userId, + distinct_id: userId, + $email: user?.email?.address, + user_id: userId, + user_type: 'authenticated', + $last_login: new Date().toISOString(), + authenticated: true, + first_time_properties: { + $first_login: new Date().toISOString(), + first_wallet_address: user?.wallet?.address, + first_email: user?.email?.address + } + } + }; + + const response = await fetch('/api/mixpanel/identify', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + throw new Error(`Failed to identify user: ${response.statusText}`); + } + } catch (error) { + console.error("Error in identifyUser:", error); + } +} export function MixpanelProvider({ children }: { children: ReactNode }) { + const { user, authenticated, ready } = usePrivy(); + useEffect(() => { if (mixpanelConfig.projectToken) { try { - mixpanel.init(mixpanelConfig.projectToken, { debug: true, ignore_dnt: true }); + mixpanel.init(mixpanelConfig.projectToken, { + debug: true, + ignore_dnt: true, + track_pageview: true, + persistence: 'localStorage' + }); console.log('Mixpanel initialized successfully'); } catch (error) { console.error('Error initializing Mixpanel:', error); @@ -18,5 +63,29 @@ export function MixpanelProvider({ children }: { children: ReactNode }) { } }, []); + // Handle user identification + useEffect(() => { + if (!ready) return; + + const handleIdentification = async () => { + let distinctId; + + if (authenticated && user?.id) { + distinctId = user.id; + // Use server-side identification + const anonymousId = localStorage.getItem('mixpanel_anonymous_id') || crypto.randomUUID(); + await identifyUser(user.id, anonymousId, user); + mixpanel.identify(user.id); + } else { + // For anonymous users + distinctId = localStorage.getItem('mixpanel_anonymous_id') || crypto.randomUUID(); + localStorage.setItem('mixpanel_anonymous_id', distinctId); + mixpanel.identify(distinctId); + } + }; + + handleIdentification(); + }, [user, authenticated, ready]); + return <>{children}</>; } \ No newline at end of file diff --git a/apps/app/components/analytics/SessionTracker.tsx b/apps/app/components/analytics/SessionTracker.tsx index 048f0115..7e1c58c4 100644 --- a/apps/app/components/analytics/SessionTracker.tsx +++ b/apps/app/components/analytics/SessionTracker.tsx @@ -3,46 +3,48 @@ import { useEffect } from 'react'; import track from '@/lib/track'; import { usePrivy } from '@privy-io/react-auth'; -import mixpanel from 'mixpanel-browser'; -import { mixpanel as mixpanelConfig } from '@/lib/env'; -function identifyUser(userId: string, anonymousId: string, user: any) { +async function identifyUser(userId: string, anonymousId: string, user: any) { try { - console.log("identifyUser userId:", userId); - - // First, create the alias if it doesn't exist - if (anonymousId !== userId) { - console.log("mixpanel.alias", userId, anonymousId); - mixpanel.alias(userId, anonymousId); - } - - // Then identify the user - mixpanel.identify(userId); - - // Set user properties - const userProperties = { - $name: userId, // This helps ensure the user shows up in Mixpanel - distinct_id: userId, - user_id: userId, - user_type: 'authenticated', - $last_login: new Date().toISOString(), - authenticated: true + const payload = { + userId, + anonymousId, + properties: { + $name: userId, + distinct_id: userId, + $email: user?.email?.address, + user_id: userId, + user_type: 'authenticated', + $last_login: new Date().toISOString(), + authenticated: true, + first_time_properties: { + $first_login: new Date().toISOString(), + first_wallet_address: user?.wallet?.address, + first_email: user?.email?.address + } + } }; - // Set regular properties that can change - console.log("Setting user properties:", userProperties); - mixpanel.people.set(userProperties); - mixpanel.register(userProperties); - - // Set first login timestamp - will only be set once - mixpanel.people.set_once({ - $first_login: new Date().toISOString(), - first_wallet_address: user?.wallet?.address, - first_email: user?.email?.address + console.log("Sending identify request:", payload); + + const response = await fetch('/api/mixpanel/identify', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload) }); + if (!response.ok) { + const errorData = await response.json(); + throw new Error(`Failed to identify user: ${errorData.error || response.statusText}`); + } + + const data = await response.json(); + console.log("Identify response:", data); + } catch (error) { - console.error("Error identifying user:", error); + console.error("Error in identifyUser:", error); } } @@ -56,9 +58,7 @@ async function handleDistinctId(user: any) { // Only handle user identification if there is an authenticated user if (user?.id) { - if (distinctId !== user.id) { - identifyUser(user.id, distinctId, user); - } + await identifyUser(user.id, distinctId, user); localStorage.setItem('mixpanel_user_id', user.id); localStorage.setItem('mixpanel_distinct_id', user.id); distinctId = user.id; @@ -117,8 +117,7 @@ function handleSessionEnd() { export default function SessionTracker() { const { user, authenticated, ready } = usePrivy(); - - // Existing session tracking + useEffect(() => { if (!ready) return; @@ -134,9 +133,16 @@ export default function SessionTracker() { initSession(); - return () => { + // Only handle session end when leaving the page + const handleBeforeUnload = () => { handleSessionEnd(); }; + + window.addEventListener('beforeunload', handleBeforeUnload); + + return () => { + window.removeEventListener('beforeunload', handleBeforeUnload); + }; }, [user, authenticated, ready]); return null;