Skip to main content
Technology & EngineeringAnalytics Services377 lines

Amplitude Analytics

"Amplitude: product analytics, behavioral cohorts, funnels, retention, experiment platform, user journeys, JavaScript SDK"

Quick Summary21 lines
Amplitude is a product analytics platform focused on understanding user behavior at scale.
Its core model revolves around events, users, and properties, enabling teams to build funnels,
retention analyses, behavioral cohorts, and user journey maps. Amplitude encourages an
"instrument once, analyze forever" approach where a well-designed taxonomy of events and

## Key Points

- **Define a typed event taxonomy** using TypeScript interfaces. A type-safe tracking function
- **Use `Identify.setOnce()` for immutable properties** like `first_login_date` or
- **Use `Identify.add()` for counters** like `login_count` or `feature_usage_count` instead
- **Call `amplitude.reset()` on logout** to clear the user ID, device ID, and all user
- **Flush events in serverless environments** by calling `client.flush()` after tracking to
- **Enable `automaticExposureTracking`** in the Experiment SDK so that variant assignments
- **Use the Revenue class** for all monetary tracking instead of custom events with amount
- **Sample session replays** to control costs. A 5-10% sample rate is usually sufficient to
- **Creating unique event names per page or feature.** Events like `home_page_button_click` and
- **Sending high-cardinality string properties.** Properties like `search_query` or `user_agent`
- **Calling `amplitude.init()` on the server.** The browser SDK must only run client-side. Use
- **Tracking events before identifying the user.** Events tracked before `setUserId` are
skilldb get analytics-services-skills/Amplitude AnalyticsFull skill: 377 lines
Paste into your CLAUDE.md or agent config

Amplitude Analytics

Core Philosophy

Amplitude is a product analytics platform focused on understanding user behavior at scale. Its core model revolves around events, users, and properties, enabling teams to build funnels, retention analyses, behavioral cohorts, and user journey maps. Amplitude encourages an "instrument once, analyze forever" approach where a well-designed taxonomy of events and properties powers unlimited ad-hoc queries without re-instrumentation. The platform also includes an experimentation layer for A/B testing and a customer data platform (CDP) for syncing behavioral data across tools. The key principle is that analytics should answer "why" users convert or churn, not just report "how many."

Setup

Browser SDK Installation

// lib/amplitude.ts
import * as amplitude from "@amplitude/analytics-browser";
import { sessionReplayPlugin } from "@amplitude/plugin-session-replay-browser";

export function initAmplitude(): void {
  amplitude.init(process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY!, {
    autocapture: {
      elementInteractions: true,
      pageViews: true,
      sessions: true,
    },
    defaultTracking: {
      pageViews: true,
      sessions: true,
      formInteractions: true,
      fileDownloads: true,
    },
    minIdLength: 1,
    flushIntervalMillis: 1000,
    flushQueueSize: 30,
    logLevel: amplitude.Types.LogLevel.Warn,
  });

  // Add session replay plugin
  const sessionReplay = sessionReplayPlugin({
    sampleRate: 0.1, // Record 10% of sessions
  });
  amplitude.add(sessionReplay);
}

export { amplitude };

Next.js Provider

// app/providers.tsx
"use client";

import { useEffect } from "react";
import { initAmplitude } from "@/lib/amplitude";

export function AmplitudeProvider({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    initAmplitude();
  }, []);

  return <>{children}</>;
}

Node.js Server SDK

// lib/amplitude-server.ts
import { Amplitude } from "@amplitude/analytics-node";

let serverClient: Amplitude | null = null;

export function getAmplitudeServer(): Amplitude {
  if (!serverClient) {
    serverClient = new Amplitude(process.env.AMPLITUDE_API_KEY!);
  }
  return serverClient;
}

Key Techniques

Event Tracking with Typed Events

// lib/analytics-events.ts
import { amplitude } from "@/lib/amplitude";
import { Identify } from "@amplitude/analytics-browser";

// Type-safe event definitions
interface AnalyticsEvents {
  signup_started: { method: string; source: string };
  signup_completed: { method: string; time_to_complete_ms: number };
  feature_used: { feature: string; context: string };
  upgrade_clicked: { current_plan: string; target_plan: string; source: string };
  search_performed: { query_length: number; result_count: number; filters: string[] };
}

export function trackEvent<K extends keyof AnalyticsEvents>(
  event: K,
  properties: AnalyticsEvents[K]
): void {
  amplitude.track(event, properties);
}

// Usage
function onSignupComplete(method: string, startTime: number): void {
  trackEvent("signup_completed", {
    method,
    time_to_complete_ms: Date.now() - startTime,
  });
}

User Identity and Properties

import { amplitude } from "@/lib/amplitude";
import { Identify } from "@amplitude/analytics-browser";

// Identify user after authentication
function identifyUser(user: {
  id: string;
  email: string;
  plan: string;
  companyId: string;
  role: string;
}): void {
  amplitude.setUserId(user.id);

  const identify = new Identify();
  identify.set("email", user.email);
  identify.set("plan", user.plan);
  identify.set("company_id", user.companyId);
  identify.set("role", user.role);
  identify.setOnce("first_login_date", new Date().toISOString());
  identify.add("login_count", 1);

  amplitude.identify(identify);
}

// Set group for B2B analytics
function setUserGroup(companyId: string, companyName: string): void {
  amplitude.setGroup("company", companyId);

  const groupIdentify = new Identify();
  groupIdentify.set("name", companyName);
  groupIdentify.set("updated_at", new Date().toISOString());
  amplitude.groupIdentify("company", companyId, groupIdentify);
}

// Reset on logout
function handleLogout(): void {
  amplitude.track("user_logged_out");
  amplitude.reset();
}

Revenue Tracking

import { amplitude } from "@/lib/amplitude";
import { Revenue } from "@amplitude/analytics-browser";

function trackPurchase(order: {
  productId: string;
  productName: string;
  price: number;
  quantity: number;
}): void {
  const revenue = new Revenue();
  revenue.setProductId(order.productId);
  revenue.setPrice(order.price);
  revenue.setQuantity(order.quantity);
  revenue.setRevenueType("purchase");
  revenue.setEventProperties({
    product_name: order.productName,
    currency: "USD",
  });

  amplitude.revenue(revenue);
}

function trackSubscription(plan: string, mrr: number, interval: string): void {
  const revenue = new Revenue();
  revenue.setProductId(plan);
  revenue.setPrice(mrr);
  revenue.setQuantity(1);
  revenue.setRevenueType("subscription");
  revenue.setEventProperties({ interval, plan_name: plan });

  amplitude.revenue(revenue);
}

Experiment Integration

// lib/experiment.ts
import { Experiment, ExperimentClient } from "@amplitude/experiment-js-client";

let experimentClient: ExperimentClient | null = null;

export async function initExperiment(userId?: string): Promise<ExperimentClient> {
  if (!experimentClient) {
    experimentClient = Experiment.initializeWithAmplitudeAnalytics(
      process.env.NEXT_PUBLIC_AMPLITUDE_DEPLOYMENT_KEY!,
      {
        automaticExposureTracking: true,
        automaticFetchOnAmplitudeIdentityChange: true,
      }
    );
  }

  await experimentClient.fetch({ user_id: userId });
  return experimentClient;
}

export function getVariant(flagKey: string): string | undefined {
  if (!experimentClient) return undefined;
  const variant = experimentClient.variant(flagKey);
  return variant.value as string | undefined;
}
// hooks/useExperiment.ts
"use client";

import { useEffect, useState } from "react";
import { getVariant, initExperiment } from "@/lib/experiment";

export function useExperiment(flagKey: string, fallback: string = "control"): string {
  const [variant, setVariant] = useState<string>(fallback);

  useEffect(() => {
    initExperiment().then(() => {
      const value = getVariant(flagKey);
      if (value) setVariant(value);
    });
  }, [flagKey]);

  return variant;
}

// Usage in a component
function OnboardingFlow(): JSX.Element {
  const variant = useExperiment("onboarding-v2", "control");

  if (variant === "treatment") {
    return <NewOnboarding />;
  }
  return <ClassicOnboarding />;
}

Server-Side Event Tracking

// Server-side tracking for webhooks and background jobs
import { getAmplitudeServer } from "@/lib/amplitude-server";

async function trackServerEvent(
  userId: string,
  event: string,
  properties: Record<string, unknown>
): Promise<void> {
  const client = getAmplitudeServer();

  client.track({
    user_id: userId,
    event_type: event,
    event_properties: properties,
    time: Date.now(),
    platform: "server",
  });

  await client.flush();
}

// Example: track subscription renewal from a webhook
async function handleRenewalWebhook(event: {
  userId: string;
  plan: string;
  amount: number;
  nextRenewal: string;
}): Promise<void> {
  await trackServerEvent(event.userId, "subscription_renewed", {
    plan: event.plan,
    amount: event.amount,
    next_renewal: event.nextRenewal,
    source: "stripe_webhook",
  });
}

Middleware for Page-Level Properties

// middleware.ts — Enrich analytics with request context
import { NextResponse, type NextRequest } from "next/server";

export function middleware(request: NextRequest): NextResponse {
  const response = NextResponse.next();

  // Pass UTM parameters to client for attribution tracking
  const utm = {
    source: request.nextUrl.searchParams.get("utm_source"),
    medium: request.nextUrl.searchParams.get("utm_medium"),
    campaign: request.nextUrl.searchParams.get("utm_campaign"),
  };

  if (utm.source) {
    response.cookies.set("amp_utm", JSON.stringify(utm), {
      maxAge: 60 * 60, // 1 hour
      path: "/",
    });
  }

  return response;
}

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

Best Practices

  • Define a typed event taxonomy using TypeScript interfaces. A type-safe tracking function prevents event name typos and ensures required properties are always present.
  • Use Identify.setOnce() for immutable properties like first_login_date or signup_source that should never be overwritten by subsequent identify calls.
  • Use Identify.add() for counters like login_count or feature_usage_count instead of manually incrementing and setting values.
  • Call amplitude.reset() on logout to clear the user ID, device ID, and all user properties, preventing cross-user data contamination.
  • Flush events in serverless environments by calling client.flush() after tracking to ensure events are sent before the function terminates.
  • Enable automaticExposureTracking in the Experiment SDK so that variant assignments are tracked as analytics events automatically without manual instrumentation.
  • Use the Revenue class for all monetary tracking instead of custom events with amount properties. This integrates with Amplitude's built-in revenue analysis tools.
  • Sample session replays to control costs. A 5-10% sample rate is usually sufficient to find usability issues while keeping data volume manageable.

Anti-Patterns

  • Creating unique event names per page or feature. Events like home_page_button_click and pricing_page_button_click should be a single button_click event with a page property.
  • Sending high-cardinality string properties. Properties like search_query or user_agent as free text create millions of unique values that slow down queries. Use bucketed or categorical properties instead (e.g., query_length_bucket: "10-20").
  • Calling amplitude.init() on the server. The browser SDK must only run client-side. Use the @amplitude/analytics-node package for server-side tracking.
  • Tracking events before identifying the user. Events tracked before setUserId are associated with an anonymous device ID. While Amplitude merges them on identify, the merge is not retroactive in all analysis views. Identify as early as possible.
  • Using Identify.set() for every property update. Using set() overwrites the previous value. For properties that should reflect the first observed value, use setOnce().
  • Ignoring the experiment exposure event. If you check a variant but do not track an exposure, the experiment results will be inaccurate. Always use the built-in automatic exposure tracking or manually call experimentClient.exposure().
  • Not batching events. Sending each event individually with flushQueueSize: 1 creates excessive network requests. Use the default batching configuration and only reduce it in serverless contexts where function lifetime is short.

Install this skill directly: skilldb add analytics-services-skills

Get CLI access →