Skip to content
trakoo
Esc
navigateopen⌘Jpreview
On this page

Next.js

Integrate trakoo with the Next.js App Router — typed events on the client, and guaranteed delivery from server actions and route handlers.

This guide wires trakoo into a Next.js App Router project (Next.js 13+). You’ll set up one shared set of event definitions, track browser interactions from client components, and send critical events from the server with guaranteed delivery.

Install

Add the packages

npm install trakoo

This guide uses PostHog, which needs its own SDKs — one for the browser, one for Node:

npm install posthog-js posthog-node

Other providers install different packages (or none). See Installation for the full matrix.

Set environment variables

Client keys must be prefixed with NEXT_PUBLIC_ so Next.js exposes them to the browser. Server keys stay unprefixed and private.

# Client-side (exposed to the browser)
NEXT_PUBLIC_POSTHOG_KEY=your-posthog-api-key
NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com

# Server-side (never sent to the browser)
POSTHOG_API_KEY=your-posthog-api-key

Define your events

Describe every event once, in a file both the client and server import. This is the single source of truth that gives you autocomplete and type checking on every track() call.

import type { CreateEventDefinition, EventCollection } from 'trakoo';

export const appEvents = {
  buttonClicked: {
    name: 'button_clicked',
    category: 'engagement',
    properties: {} as {
      buttonId: string;
      location: 'hero' | 'nav' | 'pricing';
    }
  },
  userSignedUp: {
    name: 'user_signed_up',
    category: 'user',
    properties: {} as {
      email: string;
      plan: 'free' | 'pro' | 'enterprise';
    }
  }
} as const satisfies EventCollection<Record<string, CreateEventDefinition<string>>>;

export type AppEvents = typeof appEvents;

Client analytics

Create the browser instance from trakoo/client. It’s stateful: it initializes in the background, and later events carry the current user until you reset.

import { createClientAnalytics } from 'trakoo/client';
import { PostHogClientProvider } from 'trakoo/providers/client';
import type { AppEvents } from './events';

export const analytics = createClientAnalytics<AppEvents>({
  providers: [
    new PostHogClientProvider({
      token: process.env.NEXT_PUBLIC_POSTHOG_KEY!,
      api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST
    })
  ],
  debug: process.env.NODE_ENV === 'development'
});

Initialize and track page views

Put initialization and route-change page views in a client component, then mount it once in the root layout. usePathname fires on every App Router navigation, so this captures client-side route changes that a plain page load would miss.

'use client';

import { useEffect } from 'react';
import { usePathname, useSearchParams } from 'next/navigation';
import { analytics } from '@/lib/analytics';

export function AnalyticsProvider({ children }: { children: React.ReactNode }) {
  const pathname = usePathname();
  const searchParams = useSearchParams();

  useEffect(() => {
    analytics.initialize();
  }, []);

  useEffect(() => {
    if (pathname) {
      analytics.pageView({ path: pathname, title: document.title });
    }
  }, [pathname, searchParams]);

  return <>{children}</>;
}
import { AnalyticsProvider } from '@/components/analytics-provider';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <AnalyticsProvider>{children}</AnalyticsProvider>
      </body>
    </html>
  );
}

Track from client components

Call track() where the interaction happens. Client tracking is fire-and-forget — don’t await it, so the UI stays responsive.

'use client';

import { analytics } from '@/lib/analytics';

export function SignupButton() {
  return (
    <button
      onClick={() =>
        analytics.track('button_clicked', {
          buttonId: 'signup-cta',
          location: 'hero'
        })
      }
    >
      Sign up
    </button>
  );
}

Identify users on login

The client instance is stateful, so call identify() once after login and later events carry that user automatically. Call reset() on logout.

'use client';

import { useEffect } from 'react';
import { analytics } from '@/lib/analytics';
import { useUser } from '@/hooks/use-user';

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const user = useUser();

  useEffect(() => {
    if (user) {
      analytics.identify(user.id, { email: user.email, plan: user.plan });
    } else {
      analytics.reset();
    }
  }, [user]);

  return <>{children}</>;
}

See Identifying Users for traits, typing, and logout.

Server analytics

Use trakoo/server for events you cannot afford to lose to an ad-blocker — signups, payments, anything that must be recorded. It’s stateless and edge-ready: pass user context with each call, and flush before the function returns.

import { createServerAnalytics } from 'trakoo/server';
import { PostHogServerProvider } from 'trakoo/providers/server';
import type { AppEvents } from './events';

export const serverAnalytics = createServerAnalytics<AppEvents>({
  providers: [
    new PostHogServerProvider({
      apiKey: process.env.POSTHOG_API_KEY!,
      host: process.env.NEXT_PUBLIC_POSTHOG_HOST
    })
  ],
  debug: process.env.NODE_ENV === 'development'
});

Track in server actions

'use server';

import { serverAnalytics } from '@/lib/server-analytics';
import { revalidatePath } from 'next/cache';

export async function createUser(formData: FormData) {
  const email = formData.get('email') as string;
  const plan = formData.get('plan') as 'free' | 'pro' | 'enterprise';

  const user = await db.user.create({ data: { email, plan } });

  await serverAnalytics.track('user_signed_up', { email, plan }, {
    userId: user.id,
    user: { email: user.email, traits: { plan } }
  });

  await serverAnalytics.shutdown();

  revalidatePath('/dashboard');
}

Track in route handlers

import { NextResponse } from 'next/server';
import { serverAnalytics } from '@/lib/server-analytics';

export async function POST(request: Request) {
  const body = await request.json();
  const user = await db.user.create({ data: body });

  await serverAnalytics.track('user_signed_up', {
    email: body.email,
    plan: body.plan
  }, {
    userId: user.id,
    user: { email: user.email, traits: { plan: user.plan } }
  });

  await serverAnalytics.shutdown();

  return NextResponse.json({ user });
}

For events that shouldn’t block the response, hand the work to Vercel’s waitUntil and shut down when it settles:

import { NextResponse } from 'next/server';
import { waitUntil } from '@vercel/functions';
import { serverAnalytics } from '@/lib/server-analytics';

export async function GET() {
  const data = await fetchData();

  waitUntil(
    serverAnalytics
      .track('data_fetched', { count: data.length })
      .then(() => serverAnalytics.shutdown())
  );

  return NextResponse.json(data);
}

Next steps

Was this page helpful?