---
title: "How to add web analytics to a Next.js app"
description: "Set up Amplitude web analytics in the Next.js App Router: page views, sessions, and UTMs through autocapture, kept through redirects, read by channel."
canonical: "https://amplitude.com/en-us/build/nextjs-web-analytics"
---

# How to add web analytics to a Next.js app

Set up Amplitude web analytics in the Next.js App Router: page views, sessions, and UTMs through autocapture, kept through redirects, read by channel.

Source: https://amplitude.com/en-us/build/nextjs-web-analytics

---

# How to add web analytics to a Next.js app

Configure the Amplitude Unified SDK so every App Router navigation becomes a page view, every visit carries its UTM and referrer data, and sessions feed bounce rate and session length. Then open the Marketing Analytics views that read those events. About 15 minutes, all copy-paste.

- Next.js 13+
- App Router
- Unified SDK 1.x
- 15 min

Chris Tam · Last updated 25 September 2026

Next.js is one of the most popular frameworks for building web apps. [Web analytics](https://amplitude.com/web-analytics) tells you where its visitors come from, which campaigns bring them, and whether they convert.

In this tutorial, you'll add Amplitude to a Next.js App Router app, keep campaign data intact through redirects, and set up the traffic and campaign views.

## What you'll build

A Next.js app that sends `[Amplitude] Page Viewed` on first load and on every path change, starts and ends sessions automatically, and stores `utm_source`, `utm_medium`, `utm_campaign`, `referrer`, and click IDs as both user properties and event properties, including on visits that pass through a redirect. By the end you'll see a test campaign in your event stream and a Marketing Analytics view configured to read it.

## Prerequisites

- A Next.js 13 or later project using the App Router and TypeScript, with the default `@/` import alias. `npx create-next-app@latest` is fine. The Next.js Installation Guide lists Node.js 16.8 or higher.
- An Amplitude project API key. In Amplitude, go to Settings > Projects > your project > General.
- An Admin or Manager role in the Amplitude project for step 5. The Member role can't edit Marketing Analytics settings.

No Amplitude account yet? A free account is enough for this tutorial; nothing here needs a paid plan. [Sign up for free](https://app.amplitude.com/signup) to get your project API key.

## 1. Install the SDK

The Next.js Installation Guide recommends `@amplitude/unified` for new projects, and every Next.js page on Build uses it, so this `lib/amplitude.ts` works with the others. Unified also includes session replay, but it records nothing until you set `sessionReplay.sampleRate` above 0, and this page doesn't. If bundle size matters and you only want analytics, `@amplitude/analytics-browser` takes the same `autocapture` options, passed to `init()` instead of inside `initAll()`.

Terminal

```sh
npm install @amplitude/unified
```

Next.js only exposes env vars prefixed with `NEXT_PUBLIC_` to browser code. A project API key is public and its scope is limited to the minimum needed to ingest data, so shipping it to the browser is expected, but keep it out of source anyway.

.env.local

```sh
NEXT_PUBLIC_AMPLITUDE_API_KEY=your-project-api-key
```

## 2. Configure autocapture for traffic data

Every option web analytics depends on is already on by default. Name them anyway, so nobody turns one off later without knowing what it feeds, and change one: `trackHistoryChanges`.

lib/amplitude.ts

```typescript
import * as amplitude from '@amplitude/unified';

let started = false;

export function initAmplitude() {
  if (started || typeof window === 'undefined') return;

  const apiKey = process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY;
  if (!apiKey) {
    console.warn('NEXT_PUBLIC_AMPLITUDE_API_KEY is not set; analytics is off.');
    return;
  }

  amplitude.initAll(apiKey, {
    analytics: {
      autocapture: {
        // UTM, referrer, and click ID capture. Feeds Traffic by Channel and Campaign.
        attribution: true,
        // [Amplitude] Page Viewed on load and on App Router navigations.
        // 'pathOnly' ignores query-string-only changes such as ?sort=price.
        pageViews: { trackHistoryChanges: 'pathOnly' },
        // [Amplitude] Start Session and End Session. Feeds bounce rate and session length.
        sessions: true,
        // Adds current and previous page properties to every event.
        pageUrlEnrichment: true,
        formInteractions: true,
        fileDownloads: true,
      },
    // No sessionReplay option, so no sessions are recorded.
  });

  started = true;
}

// Runs once, in the browser, as soon as the client bundle loads.
initAmplitude();

export { amplitude };
```

**Why.&#x20;**&#x41; `'use client'` component still renders on the server, so anything it imports runs in Node too. The `typeof window` check keeps `init` out of that pass. The `started` flag stops a second init when Fast Refresh re-evaluates the module in development. Init runs at module scope rather than in a `useEffect` because attribution reads the landing URL at init; an effect runs after hydration, by which point client code may already have called `router.replace` and stripped the query string.

`trackHistoryChanges: 'pathOnly'` sends a page view when the path changes, not when only the query string or hash changes. Leave it out (the default is `'all'`) if your query strings mark genuinely different pages, such as `/blog?page=2`. With `'all'`, a product list that writes `?sort=` and `?color=` to the URL records a page view per filter click, which inflates page views per session and skews entry and exit pages.

Attribution needs no config. As of Browser SDK 2.45.0, the SDK stores campaign parameters two ways by default: as user properties (including first-touch `initial_utm_source` and friends, set once) and as event properties on every event, re-parsed on each client-side navigation. On versions before 2.45.0, event property tracking is opt-in. For referrer exclusions and the rest of the attribution options, see [Track marketing attribution](https://amplitude.com/docs/sdks/analytics/browser/browser-sdk-2#track-marketing-attribution).

Clicks (`elementInteractions`) stay off here because no web analytics view reads them. Turn them on if you also want click events in charts.

## 3. Mount the provider in the root layout

`app/layout.tsx` is a Server Component, so it can't run browser code itself. It renders a small client component that imports `lib/amplitude.ts`, which pulls the init into the client bundle for every route.

components/AnalyticsProvider.tsx

```typescript
'use client';

import type { ReactNode } from 'react';
import '@/lib/amplitude';

export function AnalyticsProvider({ children }: { children: ReactNode }) {
  return <>{children}</>;
}
```

app/layout.tsx

```typescript
import type { Metadata } from 'next';
import type { ReactNode } from 'react';
import { AnalyticsProvider } from '@/components/AnalyticsProvider';

export const metadata: Metadata = {
  title: 'My app',
};

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

That's all the page view code there is. The SDK sends `[Amplitude] Page Viewed` on init and again whenever the URL changes inside the app, which covers `<Link>` clicks and `router.push`. Each event carries `[Amplitude] Page Path`, `[Amplitude] Page Title`, `[Amplitude] Page URL`, and `[Amplitude] Page Counter`.

Don't add a `useEffect` on `usePathname()` that calls `track('Page Viewed')`. It produces a second, differently named page view per navigation, and Marketing Analytics only counts the one it's configured to read.

## 4. Keep UTM parameters through redirects

The SDK can only record campaign data that's in the URL when the page loads. Next.js middleware often rebuilds URLs for redirects, locale prefixes, or auth, and `new URL('/pricing', request.url)` keeps the host but discards the query string. The visitor arrives with `?utm_source=newsletter`, lands on `/pricing` with nothing, and Amplitude files the visit under Direct.

Clone `request.nextUrl` instead, change only the path, and the query string rides along:

middleware.ts

```typescript
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname === '/plans') {
    // Wrong: new URL('/pricing', request.url) drops ?utm_source=...
    const url = request.nextUrl.clone();
    url.pathname = '/pricing';
    return NextResponse.redirect(url);
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/plans'],
};
```

Apply the same rule anywhere you redirect: route handlers, server actions that call `redirect()`, and OAuth callbacks. Check each one with a `?utm_source=test` URL before you trust your channel numbers.

## 5. Set up the Marketing Analytics views

In Amplitude, web analytics lives under **Marketing Analytics** in the navigation bar. It uses Browser SDK events by default, so this step is configuration, not code.

1. **Create the channel classifier.&#x20;**&#x47;o to Data > Properties > Channels. Opening that page creates `[Amplitude] Default Channels`, which maps UTM parameters and referrer data into GA4-aligned channels such as Paid Search and Organic Social. If your org predates it, click **Build with the new logic** when prompted.
2. **Confirm the page view event.&#x20;**&#x4F;pen Marketing Analytics, click Customize, and select Page View and Filter. Choose `[Amplitude] Page Viewed` as the event. Set a domain filter if the project also receives events from another site.
3. **Define a conversion funnel.&#x20;**&#x55;nder Conversion Funnel, choose the events that make up your funnel. If you followed the [Next.js analytics page](/build/nextjs-product-analytics), `[Amplitude] Page Viewed` followed by `Plan Upgraded` works as a first funnel.
4. **Read the four views.&#x20;**&#x54;raffic by Channel, Traffic by Campaign, Page Engagement, and Conversion each show a time series and a table. Ad Performance needs an ad network source such as Google Ads connected first.

For every setting, see [Out-of-the-box Marketing Analytics](https://amplitude.com/docs/analytics/ootb-marketing-analytics).

**MCP.&#x20;**&#x57;orking in Cursor or Claude Code? Amplitude's MCP server lets your agent read your project's existing event taxonomy before it writes `track` calls, so a conversion event matches a name you already use instead of inventing `planUpgraded` next to `Plan Upgraded`. [Set up the MCP server](https://amplitude.com/docs/amplitude-ai/amplitude-mcp).

## Verify it worked

Run `npm run dev`, open `http://localhost:3000/plans?utm_source=build-test&utm_medium=email&utm_campaign=nextjs-web-analytics` in a fresh private window, click through two routes, then check in this order:

1. **Address bar.&#x20;**&#x41;fter the redirect you should be on `/pricing` with all three `utm_` parameters still in the URL. If they're gone, the fix is step 4, and nothing downstream will work until it is.
2. **Network tab.&#x20;**&#x46;ilter for `api2.amplitude.com`. You should see a `POST` returning `200`. The SDK compresses request bodies of 2 KB or more, so read the events in the Instrumentation Explorer Chrome extension instead: the `[Amplitude] Page Viewed` event should carry `utm_source: build-test` in `event_properties`, and an identify event should set `utm_source` and `initial_utm_source` as user properties.
3. **Ingestion Debugger.&#x20;**&#x49;n Amplitude, click Data, select Sources, then open the Ingestion Debugger tab. The successful requests chart should show your uploads. It shows counts, not individual events.
4. **User lookup.&#x20;**&#x47;o to Users > User Profiles and search your device ID (the Instrumentation Explorer Chrome extension shows it as device\_id). Within ten seconds to a minute, the event stream shows `[Amplitude] Start Session`, then one `[Amplitude] Page Viewed` per route, and the profile shows `initial_utm_source: build-test`.
5. **Marketing Analytics.&#x20;**&#x4F;pen Traffic by Campaign for today. `nextjs-web-analytics` should appear as a campaign.

To watch payloads in the console, add `logLevel: amplitude.Types.LogLevel.Debug` to the `init` options in development.

## Troubleshooting

### Two page view events per navigation

A hand-written page view hook is running alongside autocapture. Delete it. If both events are `[Amplitude] Page Viewed`, `init` is running twice; confirm it's only called from `lib/amplitude.ts` and the `started` flag is in place.

### A page view every time a filter or sort changes

The page writes state to the query string and `trackHistoryChanges` is `'all'`. Set `pageViews: { trackHistoryChanges: 'pathOnly' }` as in step 2.

### Campaign traffic shows up as Direct

The UTM parameters were gone by the time the SDK initialized. Load a `?utm_source=test` URL and watch the address bar: a middleware redirect built with `new URL()` or a client-side `router.replace` that strips the query are the usual causes. The Channels docs also note that inconsistent `set()` and `unset()` calls on UTM user properties can classify traffic as Other.

### Page views stopped even though the code enables them

Remote configuration overrides local autocapture settings at the feature level, and Browser SDK 2.16.1 and later fetch it by default. Check Data > Settings > Autocapture and make sure Page Views and Sessions are on.

### No requests in the Network tab

The key is undefined, so the warning from step 2 is in the console. The variable must start with `NEXT_PUBLIC_`, and Next.js reads `.env.local` when the dev server starts, so restart it. In production, the value is inlined at build time; set it in your host's environment and rebuild.

### Requests fail with ERR\_BLOCKED\_BY\_CLIENT

An ad blocker is dropping the call. Route events through a [proxy on your own domain](https://amplitude.com/docs/analytics/domain-proxy) and point `serverUrl` at it. Point `remoteConfig.serverUrl` at it too, so remote configuration requests aren't blocked.

## Full code reference

Every file from this tutorial in one block, the version to hand to an agent. 5 files, 81 lines.

5 files, 81 lines

```typescript
// -- .env.local --
NEXT_PUBLIC_AMPLITUDE_API_KEY=your-project-api-key

// -- lib/amplitude.ts --
import * as amplitude from '@amplitude/unified';

let started = false;

export function initAmplitude() {
  if (started || typeof window === 'undefined') return;

  const apiKey = process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY;
  if (!apiKey) {
    console.warn('NEXT_PUBLIC_AMPLITUDE_API_KEY is not set; analytics is off.');
    return;
  }

  amplitude.initAll(apiKey, {
    analytics: {
      autocapture: {
        attribution: true,
        pageViews: { trackHistoryChanges: 'pathOnly' },
        sessions: true,
        pageUrlEnrichment: true,
        formInteractions: true,
        fileDownloads: true,
      },
  });

  started = true;
}

initAmplitude();

export { amplitude };

// -- components/AnalyticsProvider.tsx --
'use client';

import type { ReactNode } from 'react';
import '@/lib/amplitude';

export function AnalyticsProvider({ children }: { children: ReactNode }) {
  return <>{children}</>;
}

// -- app/layout.tsx --
import type { Metadata } from 'next';
import type { ReactNode } from 'react';
import { AnalyticsProvider } from '@/components/AnalyticsProvider';

export const metadata: Metadata = {
  title: 'My app',
};

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

// -- middleware.ts --
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname === '/plans') {
    const url = request.nextUrl.clone();
    url.pathname = '/pricing';
    return NextResponse.redirect(url);
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/plans'],
};
```

## Hand it to your agent

Prefer to have Cursor or Claude Code do the wiring? This prompt points the agent at this page and pins the decisions that matter.

Using Cursor or Claude Code? Paste this prompt

```text
Set up Amplitude Web Analytics in this Next.js App Router project by
following https://amplitude.com/build/nextjs-web-analytics exactly.
Install @amplitude/unified (or keep @amplitude/analytics-browser if it's
already installed). Initialize once at module scope in lib/amplitude.ts
behind a started flag and a typeof window check, reading
NEXT_PUBLIC_AMPLITUDE_API_KEY. Inside analytics: { autocapture } keep
attribution, sessions, and pageUrlEnrichment on, and set pageViews to
{ trackHistoryChanges: 'pathOnly' }. Don't set a sessionReplay sample
rate. Import lib/amplitude.ts from a 'use client' AnalyticsProvider
mounted in app/layout.tsx. Do not initialize inside useEffect and do not
add a manual page view hook. Audit every redirect in middleware, route
handlers, and server actions and make sure each one preserves the query
string. Don't hardcode the key and don't invent event names. Report every
file you changed and every redirect you checked.
```

## What this unlocks

- [Session replay](https://amplitude.com/session-replay) lets you watch what visitors with `utm_campaign: nextjs-web-analytics` did after they landed.
- [Heatmaps](https://amplitude.com/heatmaps) show where visitors click on `/pricing`, and a segment on `initial_utm_source` splits paid from organic.
- [Web Experiment](https://amplitude.com/web-experimentation) can test a new `/pricing` layout, launched from the Page Engagement table, with your conversion funnel as the metric.
- [Guides and surveys](https://amplitude.com/guides-and-surveys) can ask visitors whose `utm_medium` is `email` what brought them back.

One integration; everything else reads from the same events.

## Next steps

[Web frameworksHow to add product analytics to a Next.js appIdentify signed-in users and track Plan Upgraded from a server action.](/build/nextjs-product-analytics)[Web frameworksHow to add heatmaps to a Next.js appTurn on session replay and see where visitors click on each route.](/build/nextjs-heatmaps)[Web frameworksHow to add Web Experiment to a Next.js appTest a page change without a deploy, measured against the same traffic data.](/build/nextjs-web-experiment)

Reference docs

[Out-of-the-box Marketing Analytics →](https://amplitude.com/docs/analytics/ootb-marketing-analytics)[Next.js Installation Guide →](https://amplitude.com/docs/sdks/frameworks/nextjs-installation-guide)[Browser SDK 2: Track marketing attribution →](https://amplitude.com/docs/sdks/analytics/browser/browser-sdk-2#track-marketing-attribution)[Channels →](https://amplitude.com/docs/data/channels)[Autocapture Remote Configuration →](https://amplitude.com/docs/data/autocapture-remote-configuration)[Debug Analytics →](https://amplitude.com/docs/analytics/debug-analytics)[Amplitude MCP server →](https://amplitude.com/docs/amplitude-ai/amplitude-mcp)

Amplitude is the leading [all-in-one analytics platform](https://amplitude.com/) for building better products. Product and engineering teams use Amplitude to connect [product analytics](https://amplitude.com/amplitude-analytics) and [web analytics](https://amplitude.com/web-analytics) with [session replay](https://amplitude.com/session-replay), [heatmaps](https://amplitude.com/heatmaps), [experimentation](https://amplitude.com/amplitude-experiment), [feature flags](https://amplitude.com/feature-management), and [guides and surveys](https://amplitude.com/guides-and-surveys) in one unified workflow, with [AI agents](https://amplitude.com/ai-agents) working across all of it. Learn how Amplitude is building [self-improving products](https://amplitude.com/self-improving-products) with [Wave](https://amplitude.com/wave), helping teams turn product data into PRs.

For agents

[View raw .md](/build/nextjs-web-analytics.md)[llms.txt](/llms.txt)

## Ship the instrumentation, then read the data

Once events are flowing, Amplitude turns them into funnels, retention curves, and session replays without any further setup.

[Create a free account](https://app.amplitude.com/signup)[Browse all Build tutorials](https://amplitude.com/build)
