---
title: "How to add session replay to a Next.js app"
description: "Record Next.js App Router sessions with Amplitude session replay: client-only init, a sane sample rate, masking that survives hashed class names, and CSP."
canonical: "https://amplitude.com/en-us/build/nextjs-session-replay"
---

# How to add session replay to a Next.js app

Record Next.js App Router sessions with Amplitude session replay: client-only init, a sane sample rate, masking that survives hashed class names, and CSP.

Source: https://amplitude.com/en-us/build/nextjs-session-replay

---

# How to add session replay to a Next.js app

Install the Amplitude Unified SDK in a Next.js App Router project, start session replay once on the client with a sample rate you choose, mask what shouldn't leave the browser, and confirm a replay lands. 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. [Session replay](https://amplitude.com/session-replay) shows you what users actually saw and did, so a vague bug report becomes something you can watch.

In this tutorial, you'll add session replay to a Next.js App Router app, mask sensitive content, and confirm replays are arriving.

## What you'll build

A Next.js app that records every session in development and 1% of sessions in production, masks form inputs by default plus any element you mark with `amp-mask`, blocks elements marked `amp-block`, and attaches `[Amplitude] Session Replay ID` to the session's events. By the end you'll open a replay of your own test session from the user's event stream in Amplitude.

## Prerequisites

- A Next.js 13+ project using the App Router. `npx create-next-app@latest` with TypeScript and the default `@/*` import alias is fine.
- Node.js 16.8 or later, the floor the Amplitude Next.js guide lists.
- An Amplitude project API key. In Amplitude, go to Settings > Projects > your project > General.
- Session replay available on your plan. Replay capture is included on all plans; the sessions you can capture each month come from your plan's quota.

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

[Amplitude's Next.js guide](https://amplitude.com/docs/sdks/frameworks/nextjs-installation-guide) recommends `@amplitude/unified` for new projects. It bundles Analytics, session replay, Feature Experiment, and guides and surveys behind one `initAll` call.

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
```

Already on `@amplitude/analytics-browser`, for example to keep bundle size down? Use the plugin variant at the end of step 2 instead of switching packages.

## 2. Initialize once, on the client only

Put initialization in one module, behind two guards: `typeof window` so server rendering never touches the SDK, and a `started` flag so a second call does nothing.

lib/amplitude.ts

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

let started = false;

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

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

  started = true;
  const isProduction = process.env.NODE_ENV === 'production';

  await amplitude.initAll(apiKey, {
    sessionReplay: {
      // Fraction of sessions to record: every session locally, 1% in production.
      sampleRate: isProduction ? 0.01 : 1,
      // Ignores the focus check so you get replays with DevTools open. Setup only.
      debugMode: !isProduction,
    },
  });
}

export { amplitude };
```

**Why.** A `'use client'` file still executes on the server during prerendering, where `window` doesn't exist, so the `typeof window` check is what keeps `next build` from failing. The `started` flag matters because initializing the SDK more than once can leave you with mismatched device IDs or session IDs, and a replay that doesn't match its session's events is a replay you can't find.

Session replay only records when `sessionReplay.sampleRate` is above zero; the plugin's own default is `0`. Amplitude recommends starting production low, around `0.01`, and raising it over a few days, since capture stops for the month once your org's quota runs out. To turn recording off, set the rate to `0`. A sample rate set in Settings > Organizational Settings > Session Replay Settings overrides the one in code, so check there too. For every option, see the [session replay plugin configuration](https://amplitude.com/docs/sdks/session-replay/session-replay-plugin#configuration); the Unified SDK accepts all of them inside `sessionReplay`.

Unified also initializes guides and surveys by default. If you won't use them, the Unified SDK docs describe an `engagement: { skip: true }` option that stops that SDK from loading.

**Plugin variant.** If your `lib/amplitude.ts` initializes `@amplitude/analytics-browser` instead of Unified, add the session replay plugin:

Terminal

```sh
npm install @amplitude/plugin-session-replay-browser --save
```

lib/amplitude.ts (plugin variant)

```typescript
import * as amplitude from '@amplitude/analytics-browser';
import { sessionReplayPlugin } from '@amplitude/plugin-session-replay-browser';

let started = false;

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

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

  started = true;
  const isProduction = process.env.NODE_ENV === 'production';

  amplitude.add(
    sessionReplayPlugin({
      sampleRate: isProduction ? 0.01 : 1,
      debugMode: !isProduction,
    }),
  );

  await amplitude.init(apiKey, undefined, {
    autocapture: true,
  }).promise;
}

export { amplitude };
```

## 3. Mount it in the root layout

Call `initAmplitude` from a client component's effect, and mount that component in the root layout. The root layout stays mounted across App Router navigations, so recording continues from page to page without reinitializing.

components/AnalyticsProvider.tsx

```typescript
'use client';

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

export function AnalyticsProvider({ children }: { children: ReactNode }) {
  useEffect(() => {
    void initAmplitude();
  }, []);

  return <>{children}</>;
}
```

app/layout.tsx

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

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

**Why.** React Strict Mode runs effects twice in development, and the `started` flag absorbs the second run. Don't move the provider into a `page.tsx` or a layout keyed on the pathname; that remounts it on navigation, which is the pattern that produces duplicate starts. The provider renders its children unchanged, so there's nothing for hydration to disagree about.

Session replay needs at least one Amplitude event in the session before a replay can play. Autocapture's session and page view events cover that with no extra code.

## 4. Mask what shouldn't leave the browser

Session replay masks every text input by default: typed characters arrive as asterisks. The project's masking level in Session Replay Settings defaults to Medium, which masks all form fields and text inputs and captures other text as-is. For anything else, add a class in your JSX:

app/account/page.tsx

```typescript
export default async function AccountPage() {
  const user = await getCurrentUser(); // your auth helper

  return (
    <main>
      <h1>Account</h1>

      {/* Rendered text: captured as asterisks */}
      <p className="amp-mask">{user.email}</p>

      {/* Whole element replaced by a same-size placeholder */}
      <div className="amp-block">
        <img src={user.idDocumentUrl} alt="" />
      </div>

      {/* Inputs are masked by default; opt this one back in */}
      <input className="amp-unmask" name="search" placeholder="Search settings" />
    </main>
  );
}
```

These classes work in Server Components because they're plain strings in the rendered HTML; no client code is involved. When you need a selector for masking overrides in Session Replay Settings or in `privacyConfig`, target a data attribute like `[data-amp-mask]` rather than a CSS Modules class. Next.js hashes module class names, so `.styles_email__x7f2a` matches today and silently stops matching after a rebuild.

**Why.** Masking covers text and inputs, not HTML attributes. `alt`, `title`, `placeholder`, `aria-label`, `value`, and `data-*` attributes stay visible in replays, so don't put an email address in a `title`. When the remote settings and the SDK disagree about an element, the remote setting wins. Change the level (Light, Medium, Conservative) at Settings > Organizational Settings > Session Replay Settings, per project.

**MCP.** Working in Cursor or Claude Code? Amplitude's MCP server includes `get_session_replays`, so once replays land your agent can pull the sessions where users hit an error or rage-clicked, instead of you scrubbing through them by hand. [Set up the MCP server](https://amplitude.com/docs/amplitude-ai/amplitude-mcp).

## 5. Allow Amplitude in your CSP

Skip this step if your app doesn't send a `Content-Security-Policy` header. If it does, and you set it in `next.config.js` `headers()` or in middleware, add Amplitude's hosts to the directives you already have:

```text
# Add to your existing Content-Security-Policy (US data center)
connect-src https://api2.amplitude.com https://api-secure.amplitude.com https://api-sr.amplitude.com https://sr-client-cfg.amplitude.com;
```

The session replay plugin docs list `https://api-secure.amplitude.com` in their CSP section and `https://api-sr.amplitude.com` (replay upload) plus `https://sr-client-cfg.amplitude.com` (remote config) in their endpoint list, so allow all three until that's settled. `api2.amplitude.com` is where the Browser SDK sends events. EU projects use the `.eu.` hosts instead and set `serverZone: 'EU'` at the top level of the `initAll` options. Add `worker-src blob:` only if you turn on `useWebWorker`.

**Why.** If remote configuration fails to load, session replay captures nothing, on purpose, so it can't record with privacy settings it never received. A blocked config request looks like "session replay does nothing" rather than like an error, until you open the console and find the `Content Security Policy` message.

## Verify it worked

Run `npm run dev`, click through two or three routes, type into a form, then check in this order:

1. **Console and Network tab.** No `Content Security Policy` errors in the console. In the Network tab, filter for `amplitude`: you should see event uploads to `api2.amplitude.com` and a session replay upload to a `/sessions/` endpoint.
2. **Instrumentation Explorer.** Turn on the Session Replay debug panel in the [Amplitude Instrumentation Explorer](https://chrome.google.com/webstore/detail/amplitude-event-explorer/acehfjhnmhbmgkedjmjlobpgdicnhkbp) Chrome extension. It shows the current replay ID, the SDK and plugin versions, the sample rate, and the default and effective masking settings, which is the fastest way to confirm a remote setting isn't overriding yours.
3. **User lookup.** Go to Users > User Profiles and search for your user ID or device ID. The event stream fills in ten seconds to one minute; look for a session event carrying `[Amplitude] Session Replay ID`, then the play button on that session. Replays are generally viewable about five minutes after the session starts.

To force a fresh session while testing, delete the cookies that start with `AMP_` in DevTools and reload.

Replays recorded against `localhost` often play back unstyled, because Amplitude can't fetch stylesheets from your machine. That's expected. Check styling against a deployed preview URL.

## Troubleshooting

### No session replay requests at all

The effective sample rate is `0`. Check `sessionReplay.sampleRate`, then the rate in Session Replay Settings, which overrides code. In production at `0.01`, only about one session in a hundred records, so test in development or on a preview build with a higher rate.

### Replays work in production but not while you're debugging

Session replay only captures the tab that has focus, and an open DevTools panel can steal it. Keep `debugMode: true` in development, as in step 2, which ignores the focus check.

### A replay has events but no play button

No event in that session carries `[Amplitude] Session Replay ID`. That happens when the session's only events come from your server (for example a server action tracking with `@amplitude/analytics-node`), or when the SDK initialized twice and the replay and events disagree on device or session ID. Search the codebase for a second `initAll` or `init` call.

### Old replays lose their styling after a deploy

Next.js writes CSS to hashed file names under `/_next/static/`, and a deploy removes the old ones. Keep previous build assets available on your host or CDN, allow `app.amplitude.com` in your asset server's CORS policy, and add `crossorigin="anonymous"` to any external `<link rel="stylesheet">`.

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

An ad blocker is dropping them. Session replay isn't compatible with ad blocking software; test in a clean profile. For production traffic, route requests through your own domain with `trackServerUrl` and `configServerUrl`.

### `ReferenceError: window is not defined` during build

Something calls the SDK at module scope outside `initAmplitude`, or imports it from a Server Component and calls a method. Keep every call behind the `typeof window` guard or inside an effect in a `'use client'` component.

## Full code reference

Every file from this tutorial in one block, the version to hand to an agent. It uses the Unified SDK; the plugin variant of `lib/amplitude.ts` is in step 2.

5 files, 73 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 async function initAmplitude() {
  if (typeof window === 'undefined' || started) return;

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

  started = true;
  const isProduction = process.env.NODE_ENV === 'production';

  await amplitude.initAll(apiKey, {
    sessionReplay: {
      sampleRate: isProduction ? 0.01 : 1,
      debugMode: !isProduction,
    },
  });
}

export { amplitude };

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

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

export function AnalyticsProvider({ children }: { children: ReactNode }) {
  useEffect(() => {
    void initAmplitude();
  }, []);

  return <>{children}</>;
}

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

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

// -- app/account/page.tsx --
export default async function AccountPage() {
  const user = await getCurrentUser(); // your auth helper

  return (
    <main>
      <h1>Account</h1>
      <p className="amp-mask">{user.email}</p>
      <div className="amp-block">
        <img src={user.idDocumentUrl} alt="" />
      </div>
      <input className="amp-unmask" name="search" placeholder="Search settings" />
    </main>
  );
}
```

## 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
Add Amplitude Session Replay to this Next.js App Router app by following
https://amplitude.com/build/nextjs-session-replay exactly. If the app
already uses @amplitude/analytics-browser, add
@amplitude/plugin-session-replay-browser to the existing init instead of
installing a second SDK. Otherwise install @amplitude/unified. Initialize
only inside initAmplitude() in lib/amplitude.ts, behind a typeof window
check and a started flag, called from a 'use client' AnalyticsProvider
mounted in app/layout.tsx. Never call init at module scope or from a page.
Set sessionReplay.sampleRate to 1 in development and 0.01 in production,
and tell me that a rate in Session Replay Settings overrides it. Add
amp-mask to elements that render emails or account data and amp-block to
images of documents; don't use CSS Modules class names as masking
selectors. Read NEXT_PUBLIC_AMPLITUDE_API_KEY from the environment, don't
hardcode it. Report every file you changed and any CSP header you found.
```

## What this unlocks

- [Session replay](https://amplitude.com/session-replay) now sits on the same events as your charts: open a funnel step and watch the sessions where users dropped.
- [Heatmaps](https://amplitude.com/heatmaps) build on session replay, so once heatmap capture is on for the project in Session Replay Settings, clicks and scrolls on `/account` aggregate without new code.
- [Feature Experiment](https://amplitude.com/amplitude-experiment), bundled in `@amplitude/unified`, lets you filter replays by variant and see how people use the new version.
- [Guides and surveys](https://amplitude.com/guides-and-surveys), also bundled, can ask the users whose replays show a stall what they were looking for.

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 server-side events, so replays show up under a real user.](/build/nextjs-product-analytics)[Web frameworksAdd heatmaps to a Next.js appTurn the replay capture you just added into click and scroll maps.](/build/nextjs-heatmaps)[Web frameworksAdd guides and surveys to a Next.js appReach the users whose sessions you just watched.](/build/nextjs-guides-surveys)

Reference docs

[Next.js Installation Guide →](https://amplitude.com/docs/sdks/frameworks/nextjs-installation-guide)[Browser Unified SDK →](https://amplitude.com/docs/sdks/analytics/browser/browser-unified-sdk)[Session Replay Browser SDK Plugin →](https://amplitude.com/docs/sdks/session-replay/session-replay-plugin)[Manage privacy settings for session replay →](https://amplitude.com/docs/session-replay/manage-privacy-settings-for-session-replay)[Session Replay Settings →](https://amplitude.com/docs/session-replay/session-replay-settings)[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-session-replay.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)
