---
title: "How to add session replay to an Angular app"
description: "Record sessions in an Angular app with Amplitude, set a sample rate per environment, mask components and whole routes, and confirm a replay lands."
canonical: "https://amplitude.com/en-us/build/angular-session-replay"
---

# How to add session replay to an Angular app

Record sessions in an Angular app with Amplitude, set a sample rate per environment, mask components and whole routes, and confirm a replay lands.

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

---

# How to add session replay to an Angular app

Turn on Amplitude session replay in an Angular app, record every session in development and a small share in production, mask sensitive elements and whole routes, and open your own test replay. About 15 minutes, all copy-paste.

- Angular 19+
- Unified SDK 1.x
- 15 min

Chris Tam · Last updated 14 October 2026

Bug reports rarely include the state that caused them. [Session replay](https://amplitude.com/session-replay) records what each user of your Angular app saw and did, so "the button didn't work" becomes a session you can watch.

In this tutorial, you'll turn on session replay in an Angular app, set a sample rate per environment, mask sensitive elements and routes, and confirm a replay lands. It builds on [How to add product analytics to an Angular app](/build/angular-product-analytics).

## What you'll build

An Angular app that records every session under `ng serve` and 1% of sessions in a production build, masks text inputs by default, masks any element marked with an `ampMask` directive and every page under `/billing`, replaces blocked elements with a placeholder, and tags the session's events with `[Amplitude] Session Replay ID`. It works with and without Angular SSR. By the end you'll play back your own test session from your event stream in Amplitude.

## Prerequisites

- An Angular 19 or later app with standalone components, set up as on [How to add product analytics to an Angular app](/build/angular-product-analytics): `@amplitude/unified` installed, the key in `src/environments/environment.ts` and `environment.development.ts`, and `initAmplitude()` called from `src/main.ts`. Angular SSR (`@angular/ssr`) is optional.
- Code uses the Angular 20 file names (`app.ts`, `App`); on Angular 19 use `app.component.ts` and `AppComponent`.
- An Amplitude project API key. In Amplitude, go to Settings > Projects > your project > General.
- To change sampling or masking in Amplitude's settings, an Admin or Manager role. The code on this page doesn't need it.

No Amplitude account yet? Session replay for web is available on all plans, so a free account covers this tutorial. [Sign up for free](https://app.amplitude.com/signup) to get your project API key.

## 1. Install the SDK

[The Quickstart](https://amplitude.com/docs/get-started/amplitude-quickstart) recommends `@amplitude/unified` for browser apps installed from npm. It bundles analytics, session replay, Feature Experiment, and guides and surveys behind one `initAll` call, and session replay records once `sessionReplay.sampleRate` is above `0`. If you followed the [Angular product analytics page](/build/angular-product-analytics), it's already installed; skip to step 2.

Terminal

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

The project API key lives in `src/environments/environment.ts` and `environment.development.ts`, which the [Angular product analytics page](/build/angular-product-analytics) creates and this page doesn't change. The key is public and its scope is limited to the minimum needed to ingest data, so shipping it in the browser bundle is expected.

Already on `@amplitude/analytics-browser` to keep bundle size down? The [session replay plugin](https://amplitude.com/docs/sdks/session-replay/session-replay-plugin) takes the same options as the `sessionReplay` block below. Add it to your existing init rather than installing a second SDK.

## 2. Set a sample rate per environment

Initialize once, from `src/main.ts`, and choose the sample rate from `environment.production`. Update `src/app/amplitude.ts` from the Angular product analytics page to match this version; `src/main.ts` doesn't change and still calls `initAmplitude()` once, before `bootstrapApplication`. Other Angular pages add lines to this file, and the comment at the top names each one. Keep those lines and change only the `sessionReplay` block. A blind paste removes them with no error: flags never fetch, the guides router never registers, and Web Experiment's On Event Tracked triggers stop firing.

src/app/amplitude.ts

```typescript
// Keep lines other Angular pages added here: pageViews: { trackHistoryChanges: 'pathOnly' }
// (/build/angular-web-analytics); the setAmplitudeReady(...) wrapper, its import and the
// setAmplitudeReady(null) line (/build/angular-feature-flags, /build/angular-guides-surveys);
// the experiment block and any analytics.deviceId handoff (/build/angular-feature-flags);
// the WebExperimentGlobal type and the amplitude.add(webExperiment.plugin()) block
// (/build/angular-web-experiment).
import * as amplitude from '@amplitude/unified';
import { environment } from '../environments/environment';

export function initAmplitude(): void {
  if (!environment.amplitudeApiKey) {
    console.warn('amplitudeApiKey is not set; analytics is off.');
    return;
  }

  // This page doesn't wait for initAll. If setAmplitudeReady(...) wraps this call, keep it.
  void amplitude.initAll(environment.amplitudeApiKey, {
    analytics: {
      autocapture: {
        attribution: true,
        pageViews: true,
        // Tracks [Amplitude] Start Session and End Session events.
        sessions: true,
        formInteractions: true,
        fileDownloads: true,
        elementInteractions: true,
      },
    sessionReplay: {
      // Fraction of sessions to record: every session under ng serve, 1% in a production build.
      sampleRate: environment.production ? 0.01 : 1,
      // Ignores the focus check so replays record with DevTools open. Setup only.
      debugMode: !environment.production,
      // Selectors for the data attributes in steps 3 and 4.
      privacyConfig: {
        maskSelector: ['[data-amp-mask]'],
        blockSelector: ['[data-amp-block]'],
        unmaskSelector: ['[data-amp-unmask]'],
      },
  });
}
```

**Why.** Session replay takes a full snapshot of the page when it initializes, then records every change to the page after it. `main.ts` runs once per page load, in the browser only; with Angular SSR the server renders from `main.server.ts`, which never calls `initAmplitude()`. With SSR, the masking attributes from steps 3 and 4 are already in the server HTML when the first snapshot is taken. Without SSR, Angular sets the attribute in the same render pass that creates the element, before the recorder flushes its next batch of changes. Initializing once also matters for matching: initializing the SDK more than once can leave the replay and the session's events with different device or session IDs, and a replay that doesn't match its events is one you can't open from the event stream. An `initAll` in a component constructor, `ngOnInit`, or `provideAppInitializer` runs inside the Angular zone, which the Browser SDK docs ask Zone.js apps to avoid, and an `initAll` in a routed component runs again on every visit to that route.

Later pages in this hub show `src/app/amplitude.ts` again. If you copy a version that lacks `privacyConfig`, keep this block.

The plugin's own default sample rate is `0`, so nothing records until you set one. Amplitude recommends starting production low, around `0.01`, and raising it over a few days; capture stops for the month once your org's quota runs out. To stop recording, set the rate to `0`. Session Replay Settings in Amplitude can also set a project's rate remotely, so check Settings > Organizational Settings > Session Replay Settings when the rate you see doesn't match your code. 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` except `serverZone` and `instanceName`, which go at the top level of the `initAll` options.

Unified also initializes guides and surveys by default. If you won't use them, the [Unified SDK docs](https://amplitude.com/docs/sdks/analytics/browser/browser-unified-sdk) describe an `engagement: { skip: true }` option that stops that SDK from loading.

## 3. Mask sensitive elements with a directive

Session replay masks every text input by default: typed characters arrive as asterisks. The project's default masking level, Medium, masks all form fields and text inputs and captures other text as-is. Everything else is your call, element by element.

The SDK recognizes three classes out of the box, `amp-mask`, `amp-block`, and `amp-unmask` (for inputs), and they work on any element in an Angular template. The data attributes registered in step 2 keep privacy separate from styling, and they're what the directive below sets. Put one small attribute directive in charge of masking, so every template says it the same way:

src/app/privacy/amp-mask.ts

```typescript
import { Directive, input } from '@angular/core';

// <span ampMask> masks text as asterisks.
// <img ampMask="block"> replaces the element with a same-size placeholder.
@Directive({
  selector: '[ampMask]',
  host: {
    '[attr.data-amp-mask]': "ampMask() === 'block' ? null : ''",
    '[attr.data-amp-block]': "ampMask() === 'block' ? '' : null",
  },
})
export class AmpMask {
  readonly ampMask = input<'' | 'block'>('');
}
```

Use it in any component, next to plain data attributes where a directive would be overkill. Add `AmpMask` to the component's `imports`:

src/app/team/team.ts

```typescript
import { Component, signal } from '@angular/core';
import { AmpMask } from '../privacy/amp-mask';

interface Member {
  id: string;
  name: string;
  email: string;
  avatarUrl: string;
}

@Component({
  selector: 'app-team',
  imports: [AmpMask],
  template: `
    <h1>Team</h1>

    <!-- Inputs are masked by default. This one is safe to watch. -->
    <input data-amp-unmask name="filter" placeholder="Filter by name" />

    <ul>
      @for (member of members(); track member.id) {
        <li class="row">
          <!-- Replaced by a same-size placeholder -->
          <img ampMask="block" [src]="member.avatarUrl" alt="" width="32" height="32" />
          <span>{{ member.name }}</span>
          <!-- Captured as asterisks -->
          <span ampMask>{{ member.email }}</span>
        </li>
      }
    </ul>
  `,
  styles: `
    .row {
      display: flex;
      gap: 0.5rem;
    }
  `,
})
export class Team {
  // Replace with data from your API.
  protected readonly members = signal<Member[]>([
    { id: '1', name: 'Test User', email: 'test@example.com', avatarUrl: 'https://placehold.co/32' },
  ]);
}
```

A bare `ampMask` passes `''` and renders `data-amp-mask=""`; `ampMask="block"` renders `data-amp-block=""` instead. A `null` binding leaves the attribute off. A bare `data-amp-unmask` renders as `data-amp-unmask=""`. The attribute selectors match all three. With Angular SSR, the attributes are in the server HTML too.

**Why.** In Angular, a component's host element is a real element in the DOM, so `<app-member-card ampMask>` or `<app-member-card data-amp-mask>` masks everything that component renders. `<ng-container>` and `<ng-template>` are the exceptions: they render no element, so an attribute or directive on them masks nothing, and nothing in the replay tells you. Put it on a real element instead. Masking also follows the DOM, not the component tree. Angular CDK overlays, which back Angular Material's dialogs, menus, snack bars, and tooltips, render into a container at the end of `document.body`, outside any masked parent. Put `ampMask` inside the dialog component's own template, and don't feed personal data to `matTooltip`. And masking covers text and form inputs, not HTML attributes. `alt`, `title`, `placeholder`, `aria-label`, `value`, and `data-*` attributes stay visible in replays even inside a masked element, so `[title]="member.email"` leaks the address no matter what you mask.

Don't target the attributes Angular generates for component styles, such as `_ngcontent-ng-c123456` or `_nghost-ng-c123456`, as masking selectors, in code or in Session Replay Settings. Angular generates them at build time and they can change between builds, at which point the rule stops matching without any error.

When the masking rules in Session Replay Settings and the SDK disagree about an element, the settings page wins. You can change the project's level (Light, Medium, Conservative) or add selector overrides there, per project; see [Manage privacy settings for session replay](https://amplitude.com/docs/session-replay/manage-privacy-settings-for-session-replay).

## 4. Mask whole routes with a parent route

Some pages shouldn't show any text in a replay, such as billing, invoices, or a patient record. Masking each element there is how one gets missed. In Angular Router, a parent route's component wraps every child route in its own `<router-outlet>`, so one component covers a whole section:

src/app/billing/billing-layout.ts

```typescript
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { AmpMask } from '../privacy/amp-mask';

@Component({
  selector: 'app-billing-layout',
  imports: [RouterOutlet, AmpMask],
  template: `
    <!-- Everything routed under /billing renders inside this div, including pages added later. -->
    <div ampMask>
      <router-outlet />
    </div>
  `,
})
export class BillingLayout {}
```

Add two pages for the section, so there's text to check in the replay. If you already have billing pages, use those instead:

src/app/billing/billing-home.ts

```typescript
import { Component } from '@angular/core';

@Component({
  selector: 'app-billing-home',
  template: `
    <h1>Billing</h1>
    <p>Card ending 4242, billed to Test User</p>
  `,
})
export class BillingHome {}
```

src/app/billing/invoices.ts

```typescript
import { Component } from '@angular/core';

@Component({
  selector: 'app-invoices',
  template: `<h1>Invoices</h1>`,
})
export class Invoices {}
```

Register the layout as the parent of both pages, next to the routes from the product analytics page. Merge these routes into your existing array; the comment at the top names the routes other Angular pages add, so keep those, and keep any `title` already on a route:

src/app/app.routes.ts

```typescript
// Keep lines other Angular pages added here: their routes, such as projects/:projectId and
// settings (/build/angular-heatmaps) and pricing, welcome, login, account, plans and offer/:code
// (/build/angular-web-analytics), and the title on each route if you added titles there.
import { Routes } from '@angular/router';
import { Home } from './home';
import { NewProject } from './projects/new-project';
import { Team } from './team/team';
import { BillingLayout } from './billing/billing-layout';
import { BillingHome } from './billing/billing-home';
import { Invoices } from './billing/invoices';

export const routes: Routes = [
  { path: '', component: Home },
  { path: 'projects/new', component: NewProject },
  { path: 'team', component: Team },
  {
    path: 'billing',
    component: BillingLayout,
    children: [
      { path: '', component: BillingHome },
      { path: 'invoices', component: Invoices },
    ],
  },
];
```

Then add links to the new routes in the root component, so you can move between them without a reload. The base page's `App` already imports `RouterLink`, so add only these three links inside the template's `<nav>`, after the New project link, and change nothing else in the file:

src/app/app.ts (Angular 19 and earlier: app.component.ts): add inside the template's \<nav>

```html
<!-- Keep lines other Angular pages added here: the Sample project and Settings links
     (/build/angular-heatmaps); EngagementBridgeService, the FeedbackButton import and
     <app-feedback-button /> (/build/angular-guides-surveys); ServerFlagsService
     (/build/angular-feature-flags). -->
<a routerLink="/team">Team</a>
<a routerLink="/billing">Billing</a>
<a routerLink="/billing/invoices">Invoices</a>
```

To mask several sections that don't share a URL prefix, such as `/billing` and `/account`, give them one parent route with `path: ''` and a masking layout as its `component`, and list both sections under its `children`. A parent with an empty path doesn't add anything to the URL. The same layout works when the children come from `loadChildren`.

**Why.** `<router-outlet>` doesn't render routed components inside itself. Angular inserts each one as the outlet's next sibling, so `<router-outlet ampMask />` masks an empty element and leaves the page visible. Wrapping the outlet in a `<div ampMask>` puts every routed page inside the masked element. Masking an element through a selector masks all of its children, and a child inside it can't be unmasked again. That's what you want here: a developer who adds `/billing/payment-methods` next year as another child route gets masking without knowing this component exists. It also means `data-amp-unmask` does nothing under `/billing`, so put content you want visible outside the section. Angular Router keeps `BillingLayout` in place while someone moves between `/billing` and `/billing/invoices`, so the wrapper never disappears mid-session.

The layout masks text, not structure. Clicks, scrolling, and layout still record, so you can follow what the user did on `/billing` without reading it. To hide the region entirely, use `<div ampMask="block">` in the layout instead.

The layout doesn't cover the page title. Keep personal data out of route `title` values and `Title.setTitle()` calls under `/billing`; autocaptured page views send the title to Amplitude as an event property too.

To mask the title on every page, add `data-amp-mask` to the `<title>` in `src/index.html`: `<title data-amp-mask>MyApp</title>`.

**MCP.** Working in Cursor or Claude Code? Amplitude's MCP server includes `get_session_replays`, and the `debug-replay` skill in Amplitude's MCP marketplace plugin turns a bug report into numbered reproduction steps from the replay's interaction timeline, so you don't have to scrub through sessions by hand. [Set up the MCP server](https://amplitude.com/docs/amplitude-ai/amplitude-mcp).

## 5. Allow Amplitude in your CSP

The Angular CLI doesn't add a `Content-Security-Policy` header to your app. Skip this step unless your server, your host, or a `<meta http-equiv="Content-Security-Policy">` tag in `src/index.html` sets one. If one does, add Amplitude's hosts to the `connect-src` directive you already have:

Content-Security-Policy (connect-src only; keep your other directives)

```text
connect-src 'self' https://api2.amplitude.com https://api-sr.amplitude.com https://sr-client-cfg.amplitude.com;
```

`api-sr.amplitude.com` receives replay data and `sr-client-cfg.amplitude.com` serves the session replay remote configuration. `api2.amplitude.com` is where the analytics SDK sends events. EU projects use `api-sr.eu.amplitude.com` and `sr-client-cfg.eu.amplitude.com` and set `serverZone: 'EU'` at the top level of the `initAll` options. Add `worker-src blob:` only if you turn on `useWebWorker`. Unified also loads guides and surveys by default, which needs `https://*.amplitude.com` in `script-src`, `connect-src`, `img-src`, `media-src`, and `style-src`. Allow those, or turn guides and surveys off with `engagement: { skip: true }`.

**Why.** If remote configuration fails to load, session replay captures nothing, on purpose, so it never records without the privacy settings you defined in Amplitude. A blocked config request looks like "replay does nothing" rather than like an error, until you open the console and find a `Content Security Policy` message.

## Verify it worked

Run `ng serve`, open `http://localhost:4200` in a fresh private window, click Team, type into the filter, click Billing, then Invoices, 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` returning `200`, a request to `sr-client-cfg.amplitude.com`, and replay uploads to `api-sr.amplitude.com`. With Angular SSR, none of them start until the page loads in the browser; the server render sends nothing.
2. **Ingestion Debugger.** In Amplitude, click Data, select Sources, then open the Ingestion Debugger tab. The successful requests chart should show your event uploads. It shows counts, not individual events.
3. **User lookup.** Go to Users > User Profiles and search your device ID (the Instrumentation Explorer Chrome extension shows it as device\_id) or `user-42` if you clicked **Sign in as test user**. Within ten seconds to a minute, the session's events, such as `[Amplitude] Start Session`, carry `[Amplitude] Session Replay ID`, and the event stream shows a **Play Session** button next to that session. Replays are generally ready about five minutes after the session begins.
4. **Session Replay debug panel.** Install the [Instrumentation Explorer](https://chrome.google.com/webstore/detail/amplitude-event-explorer/acehfjhnmhbmgkedjmjlobpgdicnhkbp) Chrome extension and turn on its Session Replay debug panel on your page. It floats over the page and shows the current replay ID, the session replay SDK and plugin versions, the sample rate, and the default and effective masking settings. A sample rate of `1` under `ng serve` confirms step 2 took effect, and the effective masking settings show whether Session Replay Settings overrode your code.
5. **The replay itself.** Play it. On `/team`, the filter text is visible, emails are asterisks, and avatars are replaced by empty placeholders of the same size. The plugin's `applyBackgroundColorToBlockedElements` option shades those placeholders, but it's off by default, so expect a blank gap, not a gray box. On `/billing` and `/billing/invoices`, every line of text is masked while clicks and scrolling still show.

If the replay won't load, open the Ingestion Monitor from the top right of the Session Replay section of your project's Users & Sessions tab. It shows replay request statuses, including `429` throttling and `402` quota errors.

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

Replays recorded against `localhost` can play back unstyled, because Amplitude can't fetch files from your machine. Check styling against a deployed preview.

## Troubleshooting

### No session replay requests at all

The effective sample rate is `0`. Check `sessionReplay.sampleRate`, then the rate in Session Replay Settings; the debug panel shows the rate the SDK is using. A production build reads `environment.ts`, where `production` is `true`, and records only about one session in a hundred at `0.01`, so test with `ng serve` or raise the rate temporarily. If `environment.ts` lacks `production: true`, every build records every session instead.

### Replays record in production but not while you debug

Session replay only captures the page that's in focus, and an open DevTools panel can take it. Keep `debugMode: !environment.production` from step 2, which ignores the focus check in development.

### Events arrive but the session has no Play Session button

No event in that session carries `[Amplitude] Session Replay ID`. The usual cause is a second `initAll` or `init` call, for example in a component, a service, `provideAppInitializer`, or `app.config.ts`, so the replay and the events disagree on device or session ID. Search the codebase and keep the one call in `src/main.ts`.

### Text under /billing shows up in the replay

The masking attribute sits on `<router-outlet>` or an `<ng-container>`, which never wraps the routed page. Inspect the page in DevTools: the routed component should be inside the `<div data-amp-mask="">`. Wrap the outlet in an element as in step 4. Also check that the masking layout is the `component` of the `billing` route itself, not of some other parent route.

### Masked text shows up in a replay

Check four things in order. The text sits in an attribute such as `title` or `aria-label`, which masking never covers. The directive sits on an `<ng-container>` or `<ng-template>` and never reached the DOM; inspect the element. The text renders in a CDK overlay (a dialog, menu, snack bar, or tooltip) at the end of `document.body`, outside the masked parent. Or the element was unmasked in Session Replay Settings, which overrides the SDK; the debug panel's effective masking settings show it.

### Old replays lose their styling after a deploy

`ng build` writes your global stylesheet to a hashed file name such as `styles-5INURTSO.css`, and a deploy that replaces the build output removes the old one. Keep previous build assets available on your host, 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, so test in a clean profile. For production traffic, route requests through your own domain and point `trackServerUrl` and `configServerUrl` at it, and set `serverUrl` in the `analytics` block for events, plus `remoteConfig.serverUrl` for remote configuration, as on [the web analytics page](/build/angular-web-analytics).

### No requests to Amplitude at all

The key is empty, so the warning from step 2 is in the console. `ng serve` reads `environment.development.ts` and `ng build` reads `environment.ts`, so check the file your configuration uses. If neither exists, run `ng generate environments` and check `fileReplacements` in `angular.json`.

## Full code reference

Every file from this tutorial in one block, the version to hand to an agent. 8 files. Each shared file (`amplitude.ts`, `app.routes.ts`, `app.ts`) starts with a comment naming the lines other Angular pages add there; merge into your file and keep those lines. Unchanged from earlier pages; keep your versions: `src/environments/environment.ts`, `src/environments/environment.development.ts`, `src/main.ts`, `src/app/auth.service.ts`, `src/app/amplitude-identity.service.ts`, `src/app/home.ts`, `src/app/projects/new-project.ts`.

8 files, 170 lines

```typescript
// -- src/app/amplitude.ts --
// Keep lines other Angular pages added here: pageViews: { trackHistoryChanges: 'pathOnly' }
// (/build/angular-web-analytics); the setAmplitudeReady(...) wrapper, its import and the
// setAmplitudeReady(null) line (/build/angular-feature-flags, /build/angular-guides-surveys);
// the experiment block and any analytics.deviceId handoff (/build/angular-feature-flags);
// the WebExperimentGlobal type and the amplitude.add(webExperiment.plugin()) block
// (/build/angular-web-experiment).
import * as amplitude from '@amplitude/unified';
import { environment } from '../environments/environment';

export function initAmplitude(): void {
  if (!environment.amplitudeApiKey) {
    console.warn('amplitudeApiKey is not set; analytics is off.');
    return;
  }

  // This page doesn't wait for initAll. If setAmplitudeReady(...) wraps this call, keep it.
  void amplitude.initAll(environment.amplitudeApiKey, {
    analytics: {
      autocapture: {
        attribution: true,
        pageViews: true,
        // Tracks [Amplitude] Start Session and End Session events.
        sessions: true,
        formInteractions: true,
        fileDownloads: true,
        elementInteractions: true,
      },
    sessionReplay: {
      // Fraction of sessions to record: every session under ng serve, 1% in a production build.
      sampleRate: environment.production ? 0.01 : 1,
      // Ignores the focus check so replays record with DevTools open. Setup only.
      debugMode: !environment.production,
      // Selectors for the data attributes in steps 3 and 4.
      privacyConfig: {
        maskSelector: ['[data-amp-mask]'],
        blockSelector: ['[data-amp-block]'],
        unmaskSelector: ['[data-amp-unmask]'],
      },
  });
}

// -- src/app/privacy/amp-mask.ts --
import { Directive, input } from '@angular/core';

@Directive({
  selector: '[ampMask]',
  host: {
    '[attr.data-amp-mask]': "ampMask() === 'block' ? null : ''",
    '[attr.data-amp-block]': "ampMask() === 'block' ? '' : null",
  },
})
export class AmpMask {
  readonly ampMask = input<'' | 'block'>('');
}

// -- src/app/team/team.ts --
import { Component, signal } from '@angular/core';
import { AmpMask } from '../privacy/amp-mask';

interface Member {
  id: string;
  name: string;
  email: string;
  avatarUrl: string;
}

@Component({
  selector: 'app-team',
  imports: [AmpMask],
  template: `
    <h1>Team</h1>
    <input data-amp-unmask name="filter" placeholder="Filter by name" />
    <ul>
      @for (member of members(); track member.id) {
        <li class="row">
          <img ampMask="block" [src]="member.avatarUrl" alt="" width="32" height="32" />
          <span>{{ member.name }}</span>
          <span ampMask>{{ member.email }}</span>
        </li>
      }
    </ul>
  `,
  styles: `
    .row {
      display: flex;
      gap: 0.5rem;
    }
  `,
})
export class Team {
  // Replace with data from your API.
  protected readonly members = signal<Member[]>([
    { id: '1', name: 'Test User', email: 'test@example.com', avatarUrl: 'https://placehold.co/32' },
  ]);
}

// -- src/app/billing/billing-layout.ts --
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { AmpMask } from '../privacy/amp-mask';

@Component({
  selector: 'app-billing-layout',
  imports: [RouterOutlet, AmpMask],
  template: `
    <div ampMask>
      <router-outlet />
    </div>
  `,
})
export class BillingLayout {}

// -- src/app/billing/billing-home.ts --
import { Component } from '@angular/core';

@Component({
  selector: 'app-billing-home',
  template: `
    <h1>Billing</h1>
    <p>Card ending 4242, billed to Test User</p>
  `,
})
export class BillingHome {}

// -- src/app/billing/invoices.ts --
import { Component } from '@angular/core';

@Component({
  selector: 'app-invoices',
  template: `<h1>Invoices</h1>`,
})
export class Invoices {}

// -- src/app/app.routes.ts --
// Keep lines other Angular pages added here: their routes, such as projects/:projectId and
// settings (/build/angular-heatmaps) and pricing, welcome, login, account, plans and offer/:code
// (/build/angular-web-analytics), and the title on each route if you added titles there.
import { Routes } from '@angular/router';
import { Home } from './home';
import { NewProject } from './projects/new-project';
import { Team } from './team/team';
import { BillingLayout } from './billing/billing-layout';
import { BillingHome } from './billing/billing-home';
import { Invoices } from './billing/invoices';

export const routes: Routes = [
  { path: '', component: Home },
  { path: 'projects/new', component: NewProject },
  { path: 'team', component: Team },
  {
    path: 'billing',
    component: BillingLayout,
    children: [
      { path: '', component: BillingHome },
      { path: 'invoices', component: Invoices },
    ],
  },
];

// -- src/app/app.ts (add inside the template's <nav>; change nothing else) --
// Keep lines other Angular pages added here: the Sample project and Settings links
// (/build/angular-heatmaps); EngagementBridgeService, the FeedbackButton import and
// <app-feedback-button /> (/build/angular-guides-surveys); ServerFlagsService
// (/build/angular-feature-flags).
//   <a routerLink="/team">Team</a>
//   <a routerLink="/billing">Billing</a>
//   <a routerLink="/billing/invoices">Invoices</a>
```

## Hand it to your agent

Prefer to have Cursor, Claude Code, or Codex CLI 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
Complete https://amplitude.com/build/angular-product-analytics first, or
confirm this Angular 19+ standalone app already has its setup:
@amplitude/unified, both environment files with production and
amplitudeApiKey, src/app/amplitude.ts exporting initAmplitude(), and
initAmplitude() called in src/main.ts before bootstrapApplication. Then
add Amplitude session replay by following
https://amplitude.com/build/angular-session-replay exactly. Merge into
shared files (src/app/amplitude.ts, src/app/app.routes.ts and the root
component) and never delete a line another Amplitude Build page added;
each shared file on the page starts with a comment listing those lines.
Don't touch the environment files, main.ts, AuthService,
AmplitudeIdentityService, home.ts or new-project.ts. If the app already
uses @amplitude/analytics-browser, add
@amplitude/plugin-session-replay-browser to the existing init instead of
installing a second SDK, pass the same options to
sessionReplayPlugin({...}), and call amplitude.add() with it before
amplitude.init(). Otherwise use @amplitude/unified. Call initAll once,
inside initAmplitude() in src/app/amplitude.ts, and keep the guard that
warns and returns when the key is empty. Change only the sessionReplay
block. Keep the existing analytics block, including any pageViews options;
the setAmplitudeReady(...) wrapper, its import and the
setAmplitudeReady(null) line if they exist; the experiment block and any
analytics.deviceId handoff; and the WebExperimentGlobal type and
amplitude.add(webExperiment.plugin()) block. If there is no analytics
block yet, use the page's autocapture block, with elementInteractions on.
Never initialize in a component, a service, provideAppInitializer,
app.config.ts, or main.server.ts. Set sessionReplay.sampleRate to
environment.production ? 0.01 : 1, set debugMode to
!environment.production, and register [data-amp-mask], [data-amp-block],
and [data-amp-unmask] in sessionReplay.privacyConfig. Create
src/app/privacy/amp-mask.ts, a standalone attribute directive with
selector [ampMask] and a signal input ampMask typed '' | 'block' that
binds data-amp-mask (or data-amp-block when the value is 'block') through
host attribute bindings. Put ampMask on real elements that render emails,
billing names, or account data, and ampMask="block" on images of people or
documents; never on ng-container, ng-template, or router-outlet. Put
billing and any other sensitive section under a parent route whose
component wraps <router-outlet /> in <div ampMask>, using a parent route
with path '' if the sections don't share a URL prefix. If the app has no
/team or /billing pages, create the page's demo files so the Verify steps
run: src/app/team/team.ts (a filter input with data-amp-unmask, each
member's avatar img with ampMask="block" and email in <span ampMask>),
src/app/billing/billing-layout.ts, src/app/billing/billing-home.ts,
src/app/billing/invoices.ts, the team and billing routes merged into
src/app/app.routes.ts (keep every existing route and its title), and Team,
Billing, and Invoices links added to the root component's nav. Use the
root component file the app already has (app.ts or app.component.ts);
never create a second one. Put ampMask inside dialog, menu, and snack bar
component templates rather than around their triggers, and never use
Angular's generated _ngcontent or _nghost attributes as masking selectors.
Flag any title, aria-label, alt, placeholder, or matTooltip binding that
contains personal data. Don't change the key handling. Report every file
you changed and any Content-Security-Policy you found.
```

## What this unlocks

- [Session replay](https://amplitude.com/session-replay) sits on the same events as your charts: open a funnel step that ends at `/billing` and watch the sessions where users dropped, text masked.
- [Heatmaps](https://amplitude.com/heatmaps) are built from session replays, so once heatmap capture is on for the project in Session Replay Settings, clicks and scrolls on `/team` 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 a new version of `/team`.
- [Guides and surveys](https://amplitude.com/guides-and-surveys), also bundled, can ask the users whose replays show a stall on `/billing` what they were looking for.

One integration; everything else reads from the same events.

## Next steps

[Web frameworksHow to add heatmaps to an Angular appTurn the replay capture you just added into click and scroll maps.](/build/angular-heatmaps)[Web frameworksHow to add feature flags and experiments to an Angular appShip a new version of \`/team\` behind a flag, then filter replays by variant.](/build/angular-feature-flags)[Web frameworksHow to add web analytics to an Angular appAttribute sessions to channels and campaigns, then watch replays from one campaign.](/build/angular-web-analytics)

Reference docs

[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)[Configure session replay sampling →](https://amplitude.com/docs/session-replay/configure-sampling)[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/angular-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)
