---
title: "How to add heatmaps to an Angular app"
description: "Turn on session replay in an Angular app so Amplitude heatmaps map clicks per route, give each tab its own child route, and create your first heatmap."
canonical: "https://amplitude.com/en-us/build/angular-heatmaps"
---

# How to add heatmaps to an Angular app

Turn on session replay in an Angular app so Amplitude heatmaps map clicks per route, give each tab its own child route, and create your first heatmap.

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

---

# How to add heatmaps to an Angular app

Turn on session replay in an Angular app from \`main.ts\`, switch on heatmap capture for the project, give each settings tab its own child route so heatmaps don't merge them, and create a click map for a dynamic route. About 15 minutes of setup, then heatmaps fill in as traffic arrives.

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

Chris Tam · Last updated 14 October 2026

Angular Router maps each URL in your app to a component. [Heatmaps](https://amplitude.com/heatmaps) show where people click and how far they scroll on each of those pages, so you see which parts of a screen get used and which get skipped.

In this tutorial, you'll turn on session replay, which heatmaps are built from, give each settings tab its own child route, and create your first heatmap. It builds on the [Angular product analytics page](/build/angular-product-analytics).

## What you'll build

An Angular app that records session replay on every route, sends `[Amplitude] Page Viewed` with the same `device_id` as the replay, keeps the masking rules from the session replay page, and gives the Profile and Billing tabs of `/settings` their own child routes, plus a click map in Amplitude for every `/projects/:projectId` page. It works with and without Angular SSR. By the end you'll see a replay of your test session and a heatmap set up to fill in once traffic crosses the data threshold.

## Prerequisites

- An Angular 19 or later app with standalone components. Angular SSR (`@angular/ssr`) is optional.
- The setup from [How to add product analytics to an Angular app](/build/angular-product-analytics): the environment files, `initAmplitude()` in `src/app/amplitude.ts` called from `src/main.ts`, `AuthService`, and `AmplitudeIdentityService` injected in the root `App` component. Code uses the Angular 20 file names (`app.ts`, `App`); on Angular 19 use `app.component.ts` and `AppComponent`.
- Path-based routing, the `provideRouter` default. The heatmap URL filters in step 5 assume routes in the path, such as `/settings/billing`, not after a `#` from `withHashLocation()`.
- An Amplitude project API key. In Amplitude, go to Settings > Projects > your project > General.
- Access to heatmaps in Amplitude. Any user with View permissions can create a heatmap; turning on heatmap capture for a project takes an Admin or Manager role.

Heatmaps are available on the Plus, Growth, and Enterprise plans, for customers with session replay; [compare plans](https://amplitude.com/pricing). New to Amplitude? [Sign up for free](https://app.amplitude.com/signup) to get your project API key.

Amplitude recommends a separate development project for testing heatmaps, so your test clicks stay out of production data. Heatmap and session replay events don't count toward your event volume.

## 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 Amplitude keeps the `device_id` on replay and analytics events aligned for you, which heatmap segment and cohort filters depend on. If you followed the [Angular product analytics page](/build/angular-product-analytics), it's already installed and your environment files already hold the key; skip to step 2.

Terminal

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

Already on `@amplitude/analytics-browser` to keep bundle size down? Add the [Session Replay Browser SDK Plugin](https://amplitude.com/docs/sdks/session-replay/session-replay-plugin) to your existing init instead of installing a second SDK. It takes the same options as the `sessionReplay` block in step 2.

Heatmaps need Session Replay Browser SDK Plugin 1.7.0 or later. The Verify section shows how to read the version your page actually runs.

## 2. Initialize with session replay on

Session replay's default sample rate is `0`, which records nothing. Setting `sessionReplay.sampleRate` above `0` turns recording on, and recording is what heatmaps are built from.

Other Angular pages add lines to `src/app/amplitude.ts`, and the comment at the top of the file below names each one. Keep those lines in your file and change only the `sessionReplay` block, which is all heatmaps need. A blind paste removes them with no error in the console: flags never fetch, the guides router never registers, and Web Experiment's On Event Tracked triggers stop firing. The block keeps the masking rules from [the session replay page](/build/angular-session-replay), since heatmap backgrounds are replay snapshots and unmasked text would show up in them.

src/app/amplitude.ts

```typescript
// Keep lines other Angular pages added here: pageViews: { trackHistoryChanges: 'pathOnly' }
// (/build/angular-web-analytics); debugMode and privacyConfig (/build/angular-session-replay,
// already below); 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,
        sessions: true,
        formInteractions: true,
        fileDownloads: true,
        elementInteractions: true,
      },
    sessionReplay: {
      // Every session while you build; about 1% in production.
      sampleRate: environment.production ? 0.01 : 1,
      // Ignores the focus check so replays record with DevTools open. Setup only.
      debugMode: !environment.production,
      // From /build/angular-session-replay. Keep your masking rules when you paste this file.
      privacyConfig: {
        maskSelector: ['[data-amp-mask]'],
        blockSelector: ['[data-amp-block]'],
        unmaskSelector: ['[data-amp-unmask]'],
      },
  });
}
```

`src/main.ts`, the environment files, `AuthService`, and `AmplitudeIdentityService` from the base page don't change. `main.ts` still calls `initAmplitude()` once, before `bootstrapApplication`.

**Why.** Initializing more than once can leave replay and analytics events with different device or session IDs. Heatmaps still draw an unsegmented map in that case, but segment and cohort filters come back empty, because Amplitude can't join the clicks to your users. `main.ts` runs once per page load, in the browser only; with Angular SSR the server renders from `main.server.ts` and never starts the recorder. Never add a second `initAll` in a component, a service, `provideAppInitializer`, or `app.config.ts`. Those run inside the Angular zone, which the base page explains breaks autocapture, and `app.config.ts` is shared with the server build.

You don't need `elementInteractions` for heatmaps. The heatmap prerequisites list session replay, not click autocapture; the clicks on the map come from the replay. The base page turns it on for click events in product analytics, so it stays. `pageUrlEnrichment` is on by default with autocapture, so the file doesn't list it; leave it on. It adds `[Amplitude] Page URL` to your own `track` events, such as `Project Created`, so you can chart them per route next to the heatmap.

Be deliberate about the sample rate. Every heatmap reflects only the sessions you captured, so at `0.01` a map is built from 1% of traffic and takes longer to cross the data threshold. Amplitude recommends starting low and raising the rate over a few days; capture stops for the month once your org's replay quota runs out. Session Replay Settings in Amplitude can also set the project's rate remotely, so check there when the rate you see doesn't match your code. Heatmap backgrounds show only what your masking lets through; read the [Angular session replay page](/build/angular-session-replay) before you raise the rate in production.

Unified also initializes guides and surveys by default. If you won't use them, add `engagement: { skip: true }` to the `initAll` options, as the [Unified SDK docs](https://amplitude.com/docs/sdks/analytics/browser/browser-unified-sdk) describe.

## 3. Turn on heatmap capture in Amplitude

The SDK records replays, but the project decides whether heatmap data gets collected. In Amplitude, go to Settings > Organizational Settings > Session Replay Settings. The project list shows a session capture toggle and a heatmap capture toggle for each project. Turn both on for this project.

Select the project to open its Sampling control. A rate set there applies to the project without a deploy, which is the easier place to raise capture once heatmaps need more data.

## 4. Give each tab its own route

Heatmaps compute data per URL string, not per view. A tab bar that swaps content with a signal and `@switch`, or a tab group component, never changes the URL, so the Profile and Billing tabs of `/settings` end up in one heatmap, drawn over whichever tab the background happened to catch. Angular Router fixes this with child routes: each tab becomes a child of `settings`, and the tab bar lives in a layout component with its own `<router-outlet />`.

src/app/settings/settings-layout.ts

```typescript
import { Component } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-settings-layout',
  imports: [RouterLink, RouterLinkActive, RouterOutlet],
  template: `
    <section>
      <h1>Settings</h1>
      <nav>
        <a routerLink="profile" routerLinkActive="active">Profile</a>
        <a routerLink="billing" routerLinkActive="active">Billing</a>
      </nav>
      <!-- The active tab renders here. -->
      <router-outlet />
    </section>
  `,
})
export class SettingsLayout {}
```

src/app/settings/profile-tab.ts

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

@Component({
  selector: 'app-profile-tab',
  template: `<h2>Profile</h2>`,
})
export class ProfileTab {}
```

src/app/settings/billing-tab.ts

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

@Component({
  selector: 'app-billing-tab',
  template: `<h2>Billing</h2>`,
})
export class BillingTab {}
```

The heatmap in step 5 targets a dynamic project route. If your app doesn't have a project screen yet, add a minimal one:

src/app/projects/project-detail.ts

```typescript
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute } from '@angular/router';
import { map } from 'rxjs';

@Component({
  selector: 'app-project-detail',
  template: `<h1>Project {{ projectId() }}</h1>`,
})
export class ProjectDetail {
  // The router reuses this component when you move between projects,
  // so read the ID as a signal instead of from the snapshot.
  protected readonly projectId = toSignal(
    inject(ActivatedRoute).paramMap.pipe(map((params) => params.get('projectId'))),
  );
}
```

Register the new routes next to the base page's routes. Merge them 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. The empty child path sends `/settings` on its own to the first tab, so it never becomes a page of its own:

src/app/app.routes.ts

```typescript
// Keep lines other Angular pages added here: their routes, such as team and billing
// (/build/angular-session-replay) 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 { ProjectDetail } from './projects/project-detail';
import { SettingsLayout } from './settings/settings-layout';
import { ProfileTab } from './settings/profile-tab';
import { BillingTab } from './settings/billing-tab';

export const routes: Routes = [
  { path: '', component: Home },
  // Keep this above projects/:projectId, or "new" is read as a project ID.
  { path: 'projects/new', component: NewProject },
  { path: 'projects/:projectId', component: ProjectDetail },
  {
    path: 'settings',
    component: SettingsLayout,
    children: [
      { path: '', redirectTo: 'profile', pathMatch: 'full' },
      { path: 'profile', component: ProfileTab },
      { path: 'billing', component: BillingTab },
    ],
  },
];
```

With Angular SSR, `ng new --ssr` prerenders every route, and a route with a parameter can't be prerendered without a list of IDs, so `ng build` fails on `projects/:projectId` with a missing `getPrerenderParams` error. Add the project route above the `'**'` entry in `src/app/app.routes.server.ts`, and keep any entries other Angular pages added there. Angular 19 apps without that file (CommonEngine) skip parameterized routes during prerendering and need no change.

src/app/app.routes.server.ts (Angular SSR only)

```typescript
// Keep lines other Angular pages added here: plans, offer/:code and account
// (/build/angular-web-analytics) and projects/new (/build/angular-feature-flags), all above '**'.
import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  // Project IDs aren't known at build time, so render these per request.
  { path: 'projects/:projectId', renderMode: RenderMode.Server },
  { path: '**', renderMode: RenderMode.Prerender },
];
```

Then add links to both screens in the root component's nav. The base page's `App` already imports `RouterLink`, so add only these two 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 Team, Billing and Invoices links
     (/build/angular-session-replay); EngagementBridgeService, the FeedbackButton import and
     <app-feedback-button /> (/build/angular-guides-surveys); ServerFlagsService
     (/build/angular-feature-flags). -->
<a routerLink="/projects/demo-1">Sample project</a>
<a routerLink="/settings">Settings</a>
```

Now `/settings/profile` and `/settings/billing` get separate heatmaps, and each tab switch also sends its own `[Amplitude] Page Viewed`. The router applies `redirectTo` before it updates the URL, so the Settings link lands on `/settings/profile` without stopping on `/settings`. On a direct load or refresh of `/settings`, the redirect replaces the URL rather than adding a history entry, and autocapture doesn't send a page view for a replaced URL. That visit's one page view can record `/settings` instead of `/settings/profile`, so link to `/settings/profile` directly in emails and docs. With Angular SSR, the default `'**'` Prerender entry resolves the `/settings` redirect at build time, so it always points to `/settings/profile`; linking `/settings/profile` directly still saves the extra hop. Heatmap clicks come from the replay, not from page views, so the heatmaps themselves aren't affected. None of these components call the SDK, so they render the same with Angular SSR. The settings layout renders inside the root `<router-outlet />`, so the identity effect from the base page keeps running on every tab.

**Why.** Put the tab in the path, not in a query string such as `/settings?tab=billing` set with `[queryParams]` and read from `ActivatedRoute`. Heatmaps need the `[Amplitude] Page URL` property to match URLs, and that property is the page URL with query parameters removed, so `/settings?tab=profile` and `/settings?tab=billing` can land in the same heatmap. A path segment is unambiguous. Dialogs and drawers that don't change the URL also merge into the page underneath; if one matters, give it a child route the same way.

`routerLink` and `redirectTo` resolve against the `<base href>` in `src/index.html`, so the routes work if you deploy under a sub-path. Your heatmap URL filters have to include that sub-path, and the URL scheme should stay put once heatmaps are collecting: heatmap data is keyed by the URL string, so changing `<base href>` or switching to hash routing mid-stream splits one page's data across two URLs.

## 5. Create your first heatmap

Heatmaps appear after Amplitude collects enough replay click data across your site: at least 10 separate pages, with about 500 tracked replay clicks on each page. These thresholds are approximate. Create the heatmap now and it fills in once traffic crosses them.

1. In Amplitude, click Heatmaps in the left navigation, then **+ New Heatmap**.
2. Choose **Click map**. You can switch to a selector map or scroll map at any time.
3. Select the URL. `projects/:projectId` gives every project its own URL, so use **Pattern match** with `https://your-domain.com/projects/*` to combine them into one map. That pattern also matches `/projects/new`, so give the create screen its own **Exact match** heatmap if you want to see it separately. Use **Exact match** for a single page, such as `https://your-domain.com/settings/billing`, and **Starts with** for a section, such as `https://your-domain.com/settings`.
4. Optionally pick a segment of users by user property or cohort.
5. Pick a device type. Heatmaps group data into four screen-width buckets: greater than 1440px, 900 to 1440px, 500 to 899px, and less than 500px. A responsive layout that collapses its nav into a menu below a CSS breakpoint looks different in each bucket, and the background and the click data must fall in the same one.
6. Choose a background. Each heatmap generates eight, taken from the page states with the most actions. Skip any that caught an open dialog or a loading state.

Once your data meets the thresholds, Amplitude also creates a few heatmaps automatically for your highest-traffic pages. You can delete them; they aren't recreated.

If your routes carry personal data in the path, such as an email address in `users/:email`, the [URL filter rules](https://amplitude.com/docs/session-replay/configure-ugc-filter-rules) replace those URLs before session replay and heatmaps record them.

**MCP.** Working in Cursor or Claude Code? Amplitude's MCP server lets your agent read your project's existing event taxonomy before it adds `track` calls near a heatmap hotspot, so new events match names 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

A development session can't reach the heatmap data threshold, so verification stops at confirming that replays arrive from a new enough SDK and that each tab has its own URL. Run `ng serve`, open `http://localhost:4200` in a fresh window, click **Sign in as test user**, then Settings, Billing, Profile, and Sample project. Check in this order:

1. **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 come from the server render; they start once the page loads in the browser.
2. **Ingestion Debugger.** In 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.
3. **User lookup.** Go to Users > User Profiles and search `user-42`. Within ten seconds to a minute, the event stream shows one `[Amplitude] Page Viewed` per route you visited, the `[Amplitude] Page URL` on the settings views ends in `/settings/profile` and `/settings/billing`, and no page view from the Settings link ends in `/settings` alone. A **Play Session** button appears for the session once the replay is processed, generally about five minutes after the session begins.
4. **Session Replay debug panel.** In the [Instrumentation Explorer Chrome extension](https://chrome.google.com/webstore/detail/amplitude-event-explorer/acehfjhnmhbmgkedjmjlobpgdicnhkbp), turn on the Session Replay debug panel. It shows the current replay ID, the session replay SDK and plugin versions, the sample rate, and the default and effective masking settings. The plugin version must be 1.7.0 or later, and the sample rate must not be `0`.
5. **Heatmaps.** Open the heatmap from step 5. In a development project it shows "No heatmap available" until the thresholds are met. After you deploy, confirm the live site serves the updated SDK, not only your local build.

## Troubleshooting

### The heatmap is blank or says "No heatmap available"

Work through the causes in order: Amplitude is still processing new replay data (a few minutes after install), the URL filter doesn't match URLs in your events, the deployed site runs a session replay plugin older than 1.7.0, or traffic hasn't reached about 500 replay clicks on each of 10 pages. If `[Amplitude] Page URL` was hidden, restore it in Data > Properties.

### Every tab on a screen shows up in one heatmap

The tabs share a URL. Move each one to a child route as in step 4. A tab kept in a query parameter can merge too, because `[Amplitude] Page URL` drops query parameters; move it into the path.

### `ng build` fails with "'projects/:projectId' route uses prerendering and includes parameters"

The default `'**'` Prerender entry in `src/app/app.routes.server.ts` covers the project route. Add `{ path: 'projects/:projectId', renderMode: RenderMode.Server }` above `'**'`, as in step 4.

### The Sample project link opens the New project screen, or /projects/new shows a project called "new"

The routes are out of order. Angular Router uses the first route that matches, so `projects/new` has to come before `projects/:projectId` in `src/app/app.routes.ts`.

### No replays at all, even with a sample rate of 1

Check the project's rate and capture toggles in Session Replay Settings (step 3). Then open the console. If a `Content-Security-Policy` comes from a `<meta>` tag in `src/index.html`, from `server.ts` with Angular SSR, or from your host, a blocked request appears there; add `https://api-sr.amplitude.com` and `https://sr-client-cfg.amplitude.com` to `connect-src`, next to `https://api2.amplitude.com` so the analytics events aren't blocked by the same policy. With `serverZone: 'EU'`, use `api-sr.eu.amplitude.com` and `sr-client-cfg.eu.amplitude.com` instead. If you turn on `useWebWorker`, also allow `worker-src 'self' blob:`. If remote configuration fails to load, session replay captures nothing, on purpose. An ad blocker also stops capture; test in a clean profile.

### A background shows no data

The background's viewport width falls in a different screen-width bucket from your recorded sessions. Use Change Screenshot > From a Replay and pick a screenshot taken at a width where your data exists.

### Segment or cohort filters return nothing

Replay and analytics events carry different `device_id` values, so Amplitude can't join clicks to users. Look for a second `initAll` or `init` call, for example in a component, a service, or `provideAppInitializer`, or a standalone session replay SDK running beside your analytics. Keep the single `initAmplitude()` call in `src/main.ts`.

### "Heatmaps isn't available for your organization"

Your organization predates Amplitude's current packaging and needs manual enablement. Contact [Amplitude Support](https://gethelp.amplitude.com).

## Full code reference

Every file from this tutorial in one block, the version to hand to an agent. 7 files, plus `src/app/app.routes.server.ts` if you use Angular SSR. Each shared file (`amplitude.ts`, `app.routes.ts`, `app.routes.server.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, 144 lines

```typescript
// -- src/app/amplitude.ts --
// Keep lines other Angular pages added here: pageViews: { trackHistoryChanges: 'pathOnly' }
// (/build/angular-web-analytics); debugMode and privacyConfig (/build/angular-session-replay,
// already below); 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,
        sessions: true,
        formInteractions: true,
        fileDownloads: true,
        elementInteractions: true,
      },
    sessionReplay: {
      // Every session while you build; about 1% in production.
      sampleRate: environment.production ? 0.01 : 1,
      // Ignores the focus check so replays record with DevTools open. Setup only.
      debugMode: !environment.production,
      // From /build/angular-session-replay. Keep your masking rules when you paste this file.
      privacyConfig: {
        maskSelector: ['[data-amp-mask]'],
        blockSelector: ['[data-amp-block]'],
        unmaskSelector: ['[data-amp-unmask]'],
      },
  });
}

// -- src/app/app.routes.ts --
// Keep lines other Angular pages added here: their routes, such as team and billing
// (/build/angular-session-replay) 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 { ProjectDetail } from './projects/project-detail';
import { SettingsLayout } from './settings/settings-layout';
import { ProfileTab } from './settings/profile-tab';
import { BillingTab } from './settings/billing-tab';

export const routes: Routes = [
  { path: '', component: Home },
  // Keep this above projects/:projectId, or "new" is read as a project ID.
  { path: 'projects/new', component: NewProject },
  { path: 'projects/:projectId', component: ProjectDetail },
  {
    path: 'settings',
    component: SettingsLayout,
    children: [
      { path: '', redirectTo: 'profile', pathMatch: 'full' },
      { path: 'profile', component: ProfileTab },
      { path: 'billing', component: BillingTab },
    ],
  },
];

// -- src/app/app.routes.server.ts (Angular SSR only) --
// Keep lines other Angular pages added here: plans, offer/:code and account
// (/build/angular-web-analytics) and projects/new (/build/angular-feature-flags), all above '**'.
import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  // Project IDs aren't known at build time, so render these per request.
  { path: 'projects/:projectId', renderMode: RenderMode.Server },
  { path: '**', renderMode: RenderMode.Prerender },
];

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

// -- src/app/settings/settings-layout.ts --
import { Component } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-settings-layout',
  imports: [RouterLink, RouterLinkActive, RouterOutlet],
  template: `
    <section>
      <h1>Settings</h1>
      <nav>
        <a routerLink="profile" routerLinkActive="active">Profile</a>
        <a routerLink="billing" routerLinkActive="active">Billing</a>
      </nav>
      <router-outlet />
    </section>
  `,
})
export class SettingsLayout {}

// -- src/app/settings/profile-tab.ts --
import { Component } from '@angular/core';

@Component({
  selector: 'app-profile-tab',
  template: `<h2>Profile</h2>`,
})
export class ProfileTab {}

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

@Component({
  selector: 'app-billing-tab',
  template: `<h2>Billing</h2>`,
})
export class BillingTab {}

// -- src/app/projects/project-detail.ts --
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute } from '@angular/router';
import { map } from 'rxjs';

@Component({
  selector: 'app-project-detail',
  template: `<h1>Project {{ projectId() }}</h1>`,
})
export class ProjectDetail {
  protected readonly projectId = toSignal(
    inject(ActivatedRoute).paramMap.pipe(map((params) => params.get('projectId'))),
  );
}
```

## 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
If src/app/amplitude.ts doesn't exist or src/main.ts doesn't call
initAmplitude() yet, first follow
https://amplitude.com/build/angular-product-analytics. Then set up
Amplitude heatmaps in this Angular app by following
https://amplitude.com/build/angular-heatmaps exactly. Merge into shared
files (src/app/amplitude.ts, src/app/app.routes.ts,
src/app/app.routes.server.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. If the app already uses
@amplitude/analytics-browser, add @amplitude/plugin-session-replay-browser
(1.7.0 or later) to the existing init instead of installing a second SDK.
Otherwise use @amplitude/unified. Keep the single initAll call inside
initAmplitude() in src/app/amplitude.ts, called once from src/main.ts
before bootstrapApplication. Never initialize in a component, service,
provideAppInitializer, app.config.ts, or main.server.ts. Change only the
sessionReplay block of amplitude.ts. Keep the setAmplitudeReady(...)
wrapper, its import and the setAmplitudeReady(null) line if they exist,
the experiment block and any analytics.deviceId handoff, any pageViews
options, and the WebExperimentGlobal type and
amplitude.add(webExperiment.plugin()) block. Set sessionReplay.sampleRate
to environment.production ? 0.01 : 1, debugMode: !environment.production,
and the privacyConfig maskSelector, blockSelector and unmaskSelector for
data-amp-mask, data-amp-block and data-amp-unmask, keeping any masking
rules already in the file. Keep pageViews on and leave pageUrlEnrichment
at its default (on). Leave AuthService, its sign-out reset(),
AmplitudeIdentityService, the environment files, home.ts and
new-project.ts as they are. If the app has no /settings or
/projects/:projectId routes, create the page's stubs so I can verify:
src/app/settings/settings-layout.ts (Profile and Billing routerLinks and a
router-outlet), settings/profile-tab.ts, settings/billing-tab.ts, and
projects/project-detail.ts (reads projectId from ActivatedRoute paramMap
with toSignal). In src/app/app.routes.ts add projects/:projectId after
projects/new, and a settings route with SettingsLayout and children: an
empty path with redirectTo 'profile' and pathMatch 'full', then profile
and billing. Keep every existing route and its title. If
src/app/app.routes.server.ts exists, add { path: 'projects/:projectId',
renderMode: RenderMode.Server } above the '**' entry and keep the other
entries. Add Sample project (/projects/demo-1) and Settings (/settings)
links to the root component's nav without removing anything. Use the root
component file the app already has (app.ts or app.component.ts); never
create a second one. Find every existing screen that switches tabs, steps,
or panels with a signal, @if or @switch blocks, a tab group component, or
a query parameter under one URL and list them for me, with a proposal to
move each into child routes; don't restructure existing routes without
asking. Don't change <base href> or the location strategy. Tell me that
session replay now records sessions and how to lower or disable the rate.
Report every file you changed.
```

## What this unlocks

- [Session replay](https://amplitude.com/session-replay), already on from step 2, lets you select an area of a click map and watch the sessions behind those clicks.
- [Product analytics](https://amplitude.com/amplitude-analytics) can chart `[Amplitude] Page Viewed` for `/settings/billing` against your upgrade event to see whether the tab you just mapped converts.
- [Feature Experiment](https://amplitude.com/amplitude-experiment), bundled in `@amplitude/unified`, can test a new billing layout, and a heatmap per variant shows how the clicks moved.
- [Guides and surveys](https://amplitude.com/guides-and-surveys), also bundled, can point users to a button the click map shows they miss.

One integration; everything else reads from the same events.

## Next steps

[Web frameworksHow to add session replay to an Angular appMask sensitive components and whole routes before you raise the sample rate.](/build/angular-session-replay)[Web frameworksHow to add web analytics to an Angular appSee which channels send the visitors whose clicks you're mapping.](/build/angular-web-analytics)[Web frameworksHow to add guides and surveys to an Angular appPoint people to the parts of the page your heatmap shows they skip.](/build/angular-guides-surveys)

Reference docs

[Heatmaps →](https://amplitude.com/docs/heatmaps)[Create a heatmap →](https://amplitude.com/docs/heatmaps/create-a-heatmap)[Troubleshoot heatmaps →](https://amplitude.com/docs/heatmaps/troubleshooting)[Session Replay Browser SDK Plugin →](https://amplitude.com/docs/sdks/session-replay/session-replay-plugin)[Manage Session Replay settings →](https://amplitude.com/docs/session-replay/session-replay-settings)[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-heatmaps.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)
