StackSee Analytics

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 checkout

With 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 plan

This 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

app/api/users/route.ts
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)

app/actions.ts
'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:

lib/analytics.ts
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

contexts/AuthContext.tsx
'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

middleware.ts
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

src/routes/+layout.server.ts
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

FeatureClientServer
Methodidentify() oncePass user with each track()
StatePersistentPer-request
ResetCall reset()No reset needed
Use CaseSingle user per sessionMultiple users per instance
WhenOn login, page loadPer API call, server action

Next Steps