Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Evan/simplify mixpanel tracking #119

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions apps/app/app/api/mixpanel/identify/route.ts
Original file line number Diff line number Diff line change
@@ -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",
},
});
}
2 changes: 0 additions & 2 deletions apps/app/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -27,7 +26,6 @@ const RootLayout = ({ children }: RootLayoutProperties) => (
<body className="bg-sidebar">
<DesignSystemProvider defaultTheme="dark">
<MixpanelProvider>
<SessionTracker />
<AlphaBanner />
<SidebarProvider>
<GlobalSidebar>
Expand Down
2 changes: 2 additions & 0 deletions apps/app/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -16,6 +17,7 @@ const App = async ({

return (
<div>
<ClientSideTracker eventName="home_page_viewed" />
<div className="flex-shrink-0">
<Suspense>
<Welcome />
Expand Down
71 changes: 70 additions & 1 deletion apps/app/components/analytics/MixpanelProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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}</>;
}
84 changes: 45 additions & 39 deletions apps/app/components/analytics/SessionTracker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand All @@ -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;
Expand Down Expand Up @@ -117,8 +117,7 @@ function handleSessionEnd() {

export default function SessionTracker() {
const { user, authenticated, ready } = usePrivy();

// Existing session tracking

useEffect(() => {
if (!ready) return;

Expand All @@ -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;
Expand Down