Identifying Users
Attach user context to your analytics events
User identification connects analytics events to specific users. Once you identify a user, all subsequent events automatically include their information.
Why Identify Users?
Without identification, you only see anonymous events:
❓ Someone clicked "Upgrade Plan" button
❓ Someone viewed the pricing page
❓ Someone started checkoutWith identification, you see the full story:
✅ john@example.com clicked "Upgrade Plan" button
✅ john@example.com (Pro plan user) viewed pricing
✅ john@example.com started checkout for Enterprise planThis enables powerful analytics like:
- User funnels - Track conversion paths per user
- Cohort analysis - Group users by attributes
- Retention tracking - See who comes back
- Personalization - Tailor experiences based on user data
Client-Side Identification
Client-side tracking is stateful—call identify() once and user context persists.
Basic Usage
import { analytics } from '@/lib/analytics';
// Identify the user (usually after login)
analytics.identify('user-123', {
email: 'john@example.com',
name: 'John Doe',
plan: 'pro'
});
// All subsequent events include user context automatically
analytics.track('button_clicked', {
buttonId: 'upgrade'
});
// Providers receive: {
// userId: 'user-123',
// email: 'john@example.com',
// traits: { name: 'John Doe', plan: 'pro' }
// }When to Call identify()
Call identify() when you know who the user is:
// ✅ After successful login
async function handleLogin(email: string, password: string) {
const user = await login(email, password);
analytics.identify(user.id, {
email: user.email,
name: user.name,
plan: user.plan
});
}
// ✅ After successful signup
async function handleSignup(email: string, password: string) {
const user = await signup(email, password);
analytics.identify(user.id, {
email: user.email,
name: user.name,
plan: 'free'
});
}
// ✅ On page load if user is already logged in
useEffect(() => {
if (currentUser) {
analytics.identify(currentUser.id, {
email: currentUser.email,
name: currentUser.name,
plan: currentUser.plan
});
}
}, [currentUser]);Resetting on Logout
Call reset() when users log out:
async function handleLogout() {
await logout();
// Clear user context
analytics.reset();
}This ensures the next user's events aren't attributed to the previous user.
Server-Side Identification
Server-side tracking is stateless—pass user context with each event.
Basic Usage
import { serverAnalytics } from '@/lib/server-analytics';
await serverAnalytics.track('api_request', {
endpoint: '/users',
method: 'POST'
}, {
// Pass user context for this event only
userId: 'user-123',
user: {
email: 'john@example.com',
traits: {
plan: 'pro',
company: 'Acme Corp'
}
}
});API Route Example
import { serverAnalytics } from '@/lib/server-analytics';
export async function POST(req: Request) {
const body = await req.json();
const user = await getCurrentUser(req);
// Track with user context
await serverAnalytics.track('user_created', {
email: body.email
}, {
userId: user.id,
user: {
email: user.email,
traits: {
plan: user.plan,
role: user.role
}
}
});
await serverAnalytics.shutdown();
return Response.json({ success: true });
}Server Actions (Next.js)
'use server';
import { serverAnalytics } from '@/lib/server-analytics';
import { auth } from '@/lib/auth';
export async function createProject(formData: FormData) {
const user = await auth();
await serverAnalytics.track('project_created', {
name: formData.get('name') as string
}, {
userId: user.id,
user: {
email: user.email,
traits: {
plan: user.plan
}
}
});
await serverAnalytics.shutdown();
}Type-Safe User Traits
Define a type for user traits to get autocomplete and type checking:
import { createClientAnalytics } from '@stacksee/analytics/client';
import type { AppEvents } from './events';
// Define user traits type
interface UserTraits {
email: string;
name: string;
plan: 'free' | 'pro' | 'enterprise';
company?: string;
role?: 'admin' | 'user' | 'viewer';
createdAt?: string;
}
// Pass as second generic parameter
export const analytics = createClientAnalytics<AppEvents, UserTraits>({
providers: [/* ... */]
});
// Now identify() is fully typed!
analytics.identify('user-123', {
email: 'john@example.com',
name: 'John Doe',
plan: 'pro', // ✅ Autocomplete works!
role: 'admin'
// wrongProperty: true // ❌ TypeScript error!
});User ID Best Practices
Use Stable IDs
// ✅ Good - stable database ID
analytics.identify('user-550e8400-e29b-41d4-a716-446655440000', {
email: 'john@example.com'
});
// ❌ Bad - email can change
analytics.identify('john@example.com', {
email: 'john@example.com'
});
// ❌ Bad - session ID changes on every login
analytics.identify(sessionId, {
email: 'john@example.com'
});Use Consistent Format
// ✅ Good - consistent UUID format
analytics.identify('550e8400-e29b-41d4-a716-446655440000', { /* ... */ });
// ✅ Good - consistent prefixed ID
analytics.identify('user_550e8400', { /* ... */ });
// ❌ Bad - inconsistent formats
analytics.identify('user123', { /* ... */ });
analytics.identify('550e8400-e29b-41d4-a716-446655440000', { /* ... */ });User Traits Best Practices
Include Useful Segmentation Data
analytics.identify('user-123', {
// Identity
email: 'john@example.com',
name: 'John Doe',
// Plan information
plan: 'pro',
subscriptionStatus: 'active',
trialEndsAt: '2024-12-31',
// Company information
company: 'Acme Corp',
companySize: '50-100',
industry: 'Technology',
// Account metadata
role: 'admin',
createdAt: '2024-01-15',
lastLoginAt: '2024-12-01'
});Keep Traits Updated
Update traits when user data changes:
// User upgrades plan
async function handleUpgrade(userId: string, newPlan: string) {
await upgradePlan(userId, newPlan);
// Update analytics traits
analytics.identify(userId, {
plan: newPlan,
upgradedAt: new Date().toISOString()
});
}Don't Store Sensitive Data
// ✅ Good - safe to send
analytics.identify('user-123', {
email: 'john@example.com',
plan: 'pro'
});
// ❌ Bad - contains sensitive data
analytics.identify('user-123', {
email: 'john@example.com',
password: 'secret123', // Never!
creditCard: '4111...', // Never!
ssn: '123-45-6789' // Never!
});Common Patterns
React Context + Auth
'use client';
import { createContext, useContext, useEffect } from 'react';
import { analytics } from '@/lib/analytics';
interface User {
id: string;
email: string;
name: string;
plan: string;
}
const AuthContext = createContext<{
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
} | null>(null);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
// Identify user on mount if logged in
useEffect(() => {
if (user) {
analytics.identify(user.id, {
email: user.email,
name: user.name,
plan: user.plan
});
}
}, [user]);
const login = async (email: string, password: string) => {
const user = await loginUser(email, password);
setUser(user);
// User is identified by useEffect above
};
const logout = async () => {
await logoutUser();
setUser(null);
analytics.reset();
};
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}Next.js Middleware
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { serverAnalytics } from '@/lib/server-analytics';
export async function middleware(request: NextRequest) {
const user = await getUserFromRequest(request);
if (user) {
// Track API request with user context
await serverAnalytics.track('api_request', {
path: request.nextUrl.pathname,
method: request.method
}, {
userId: user.id,
user: {
email: user.email,
traits: {
plan: user.plan
}
}
});
await serverAnalytics.shutdown();
}
return NextResponse.next();
}SvelteKit Load Function
import { serverAnalytics } from '$lib/server-analytics';
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ locals }) => {
const user = locals.user;
if (user) {
await serverAnalytics.track('page_view', {
path: locals.pathname
}, {
userId: user.id,
user: {
email: user.email,
traits: {
plan: user.plan
}
}
});
await serverAnalytics.shutdown();
}
return { user };
};Debugging User Identification
Enable debug mode to see user context:
const analytics = createClientAnalytics({
providers: [/* ... */],
debug: true // Enable debug logging
});
analytics.identify('user-123', {
email: 'john@example.com',
plan: 'pro'
});
// Console: [Analytics] User identified: user-123 { email: 'john@example.com', plan: 'pro' }
analytics.track('button_clicked', { buttonId: 'upgrade' });
// Console: [Analytics] Event tracked with user context: { userId: 'user-123', ... }Client vs Server Comparison
| Feature | Client | Server |
|---|---|---|
| Method | identify() once | Pass user with each track() |
| State | Persistent | Per-request |
| Reset | Call reset() | No reset needed |
| Use Case | Single user per session | Multiple users per instance |
| When | On login, page load | Per API call, server action |