How to add product analytics to an Angular app

Install the Amplitude Unified SDK in an Angular app from `main.ts`, capture page views on every Angular Router navigation, identify signed-in users from a signal, and track one custom event. About 10 minutes, all copy-paste.

  • Angular 19+
  • Unified SDK 1.x
  • 10 min

Chris Tam · Last updated 14 October 2026

Angular is a TypeScript framework for building web apps, with its own router, dependency injection, and signals built in. Product analytics shows you what people do once your app loads: which routes they visit, what they use, and where they drop off.

In this tutorial, you'll add Amplitude to an Angular app from main.ts, capture page views, identify signed-in users, and track your first custom event.

What you'll build

An Angular app that initializes Amplitude once in the browser, sends [Amplitude] Page Viewed on the first load and on every Angular Router navigation, attaches the signed-in user's ID and plan to their events, clears identity on sign-out, and tracks a Project Created event with a template property. It works with and without Angular SSR. By the end you'll see live events in Amplitude's event stream.

Prerequisites

  • An Angular 19 or later app with standalone components, for example one created with ng new. Angular SSR (@angular/ssr) is optional; every file here works with and without it. Angular 19 apps created with --ssr use CommonEngine unless you chose server routing. Apps with or without Zone.js both work.
  • Code uses the Angular 20 file names (app.ts, App); on Angular 19 use app.component.ts and AppComponent.
  • An auth setup that exposes the signed-in user as a signal with an id and a plan. Step 4 includes a small stand-in AuthService you can replace with your own.
  • An Amplitude project API key. In Amplitude, go to Settings > Projects > your project > General.

No Amplitude account yet? A free account is enough for this tutorial; nothing here needs a paid plan. Sign up for free to get your project API key.

1. Install the SDK and add the key

The Quickstart recommends @amplitude/unified for browser apps installed from npm. It bundles analytics, session replay, Feature Experiment, and guides and surveys behind one init call.

Terminal
npm install @amplitude/unified
ng generate environments

ng generate environments creates src/environments/environment.ts and src/environments/environment.development.ts, and adds a fileReplacements entry to the development configuration in angular.json. ng serve uses the development configuration, so it reads environment.development.ts; ng build uses the production configuration, so it reads environment.ts.

The project API key is public and its scope is limited to the minimum needed to ingest data, so shipping it in the browser bundle is expected. Angular has no runtime environment variables in the browser by default, so the key goes into the environment files and gets compiled into the build. Committing it is fine. If you'd rather keep it out of git, have your CI write the key into the environment file before ng build.

src/environments/environment.ts
// Keep lines other Angular pages added here: amplitudeDeploymentKey (/build/angular-feature-flags).
export const environment = {
  production: true,
  amplitudeApiKey: 'your-project-api-key',
};
src/environments/environment.development.ts
// Keep lines other Angular pages added here: amplitudeDeploymentKey (/build/angular-feature-flags).
export const environment = {
  production: false,
  amplitudeApiKey: 'your-project-api-key',
};

Use a separate Amplitude project's key in environment.development.ts if you want to keep local testing out of your production data.

2. Initialize in main.ts

Put the SDK setup in one small module that exports initAmplitude(), then call it in src/main.ts before bootstrapApplication.

src/app/amplitude.ts
// Keep lines other Angular pages added here: debugMode and privacyConfig
// (/build/angular-session-replay); 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;
  }

  // Nothing on this page needs to wait for initAll to finish.
  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,
    },
  });
}

Later Angular pages add lines to this file and to the other files on this page; the comment at the top of each block names them. Already followed another Angular page? Keep what it added and merge this page's code in instead of replacing the file.

src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
// Angular 19 and earlier: src/app/app.component.ts, class AppComponent.
import { App } from './app/app';
import { initAmplitude } from './app/amplitude';

// Browser only, once per page load, before Angular starts.
initAmplitude();

bootstrapApplication(App, appConfig).catch((err) => console.error(err));

Keep your existing main.ts imports; on Angular 19 the root component import is AppComponent from ./app/app.component.

Why. main.ts is the browser entry point. It runs once per page load, and with Angular SSR the server renders from main.server.ts instead, so the SDK never starts on the server. Calling initAmplitude() before bootstrapApplication also keeps it outside the Angular zone, which bootstrap hasn't created yet. The Browser SDK docs ask apps that use Zone.js to initialize outside the Angular zone, because the zone's patched DOM functions can break user interactions when autocapture calls them. Initializing from a component's constructor or ngOnInit would run inside the zone, after the first render, and on the server too. Zoneless apps don't have the zone problem, and the same main.ts works for them.

Clicks (elementInteractions) default to off in autocapture, which is why the config names it explicitly. @amplitude/unified turns on session replay when sessionReplay.sampleRate is above 0. This config records every session under ng serve and about 1% in a production build, which is where Amplitude recommends starting; raise it over a few days. Your session replay capture quota depends on your plan; compare plans. Set sampleRate to 0 to turn session replay off. To mask sensitive elements before you record real users, see the Angular session replay page. For the full option list, see the Browser Unified SDK reference.

3. Let autocapture handle navigation

There's no code in this step. Angular Router changes the URL with the browser's History API on every navigation. With pageViews: true, the SDK sends [Amplitude] Page Viewed on init and again on every URL change, which covers routerLink clicks and router.navigate(). Each event carries the page path, title, and URL.

Don't subscribe to Router events to call track('Page Viewed'). It produces a second, differently named page view on every navigation.

If your app uses hash routing (provideRouter(routes, withHashLocation())), keep pageViews: true. The route lives after the #, and the default setting tracks hash changes. Don't set trackHistoryChanges: 'pathOnly', which ignores the hash and stops route page views. How to add web analytics to an Angular app covers routing modes and attribution in detail.

4. Identify the signed-in user

Until you set a user ID, events belong to an anonymous device ID. Set it as soon as your auth state resolves, and reset only when someone signs out, so the next person on the same browser isn't merged into the last one.

If you don't have auth yet, use this stand-in. If you do, keep your own service and make sure it exposes the user as a signal. An auth library that gives you an observable converts with toSignal(user$, { initialValue: null }).

src/app/auth.service.ts
// Keep lines other Angular pages added here: closeAll() before reset()
// (/build/angular-guides-surveys), experiment()?.clear() and refetch() after
// reset() (/build/angular-feature-flags).
import { Injectable, signal } from '@angular/core';
import * as amplitude from '@amplitude/unified';

export interface User {
  id: string;
  plan?: string;
}

// Stand-in for your auth. Replace the bodies with your own logic.
@Injectable({ providedIn: 'root' })
export class AuthService {
  readonly user = signal<User | null>(null);

  signIn(user: User): void {
    // ... your sign-in logic
    this.user.set(user);
  }

  async signOut(): Promise<void> {
    // ... your sign-out logic
    this.user.set(null);
    // Clears the user ID and issues a new device ID for the next person.
    amplitude.reset();
  }
}

If you keep your own auth, add the amplitude.reset() line to your sign-out method, after sign-out succeeds.

Then identify from a service that reacts to the user signal:

src/app/amplitude-identity.service.ts
import { Injectable, PLATFORM_ID, computed, effect, inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import * as amplitude from '@amplitude/unified';
import { AuthService } from './auth.service';

@Injectable({ providedIn: 'root' })
export class AmplitudeIdentityService {
  private readonly auth = inject(AuthService);
  private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));

  // Only the ID and plan matter. A new user object with the same
  // values doesn't trigger another Identify.
  private readonly identity = computed(
    () => {
      const user = this.auth.user();
      return user ? { id: user.id, plan: user.plan ?? 'unknown' } : null;
    },
    { equal: (a, b) => a?.id === b?.id && a?.plan === b?.plan },
  );

  constructor() {
    // Angular runs effects during server rendering too.
    if (!this.isBrowser) return;

    effect(() => {
      const identity = this.identity();

      // Signed out or not signed in yet: keep the same device ID.
      // Reset only from your sign-out method.
      if (!identity) return;

      // Amplitude drops IDs shorter than 5 characters. Use the same
      // string your backend sends as user_id.
      amplitude.setUserId(`user-${identity.id}`);

      const identify = new amplitude.Identify();
      identify.set('plan', identity.plan);
      amplitude.identify(identify);
    });
  }
}

Create the service once from the root component, which also gets the sign-in and sign-out buttons and links to the two routes from step 5:

src/app/app.ts
// Angular 19 and earlier: src/app/app.component.ts, class AppComponent.
// Keep lines other Angular pages added here: nav links (/build/angular-session-replay,
// /build/angular-heatmaps), EngagementBridgeService and <app-feedback-button />
// (/build/angular-guides-surveys), ServerFlagsService (/build/angular-feature-flags).
import { Component, inject } from '@angular/core';
import { RouterLink, RouterOutlet } from '@angular/router';
import { AmplitudeIdentityService } from './amplitude-identity.service';
import { AuthService } from './auth.service';

@Component({
  selector: 'app-root',
  imports: [RouterOutlet, RouterLink],
  template: `
    <nav>
      <a routerLink="/">Home</a>
      <a routerLink="/projects/new">New project</a>
      @if (auth.user()) {
        <button type="button" (click)="auth.signOut()">Sign out</button>
      } @else {
        <button type="button" (click)="auth.signIn({ id: '42', plan: 'pro' })">
          Sign in as test user
        </button>
      }
    </nav>
    <router-outlet />
  `,
})
export class App {
  protected readonly auth = inject(AuthService);

  constructor() {
    // Creating the service starts its identity effect.
    inject(AmplitudeIdentityService);
  }
}

This inline template replaces the starter page that ng new puts in app.html. If your root component already has markup, keep it and add the inject(AmplitudeIdentityService) constructor and your own sign-out button. Replace the test sign-in button with your real sign-in flow. Delete or update the generated app.spec.ts (app.component.spec.ts on Angular 19), which tests the starter template.

Why. Angular runs effects during server rendering as well as in the browser, so the isPlatformBrowser check keeps every SDK call out of the server render. The computed with a custom equal passes the effect only the ID and plan, so a refreshed user object with the same values doesn't send an extra Identify. A null user also describes every anonymous visitor on every page load, and reset() issues a new device ID each time it runs, so resetting there splits one visitor into many and breaks anything bucketed by device ID, such as feature flags and Web Experiment. The plan value updates the profile right away, but only shows in charts from the user's next event.

5. Track one custom event

Autocapture covers page views, sessions, forms, and clicks. Track the events that mean something to your product yourself, from a click handler, with properties you'll want to break down by.

src/app/projects/new-project.ts
// Keep lines other Angular pages added here: /build/angular-feature-flags replaces this
// file with a flag-driven version; if you followed it, keep that version.
import { Component, signal } from '@angular/core';
import * as amplitude from '@amplitude/unified';

@Component({
  selector: 'app-new-project',
  template: `
    <select #picker (change)="template.set(picker.value)">
      <option value="blank">Blank</option>
      <option value="kanban">Kanban</option>
    </select>
    <button type="button" (click)="create()">Create project</button>
  `,
})
export class NewProject {
  protected readonly template = signal('blank');

  protected create(): void {
    // ... your create logic

    amplitude.track('Project Created', { template: this.template() });
  }
}

Add a home page and both routes, so you have something to navigate between:

src/app/home.ts
// Keep lines other Angular pages added here: data-exp attributes
// (/build/angular-web-experiment), data-guide attributes (/build/angular-guides-surveys).
import { Component } from '@angular/core';

@Component({
  selector: 'app-home',
  template: `<h1>Home</h1>`,
})
export class Home {}
src/app/app.routes.ts
// Keep lines other Angular pages added here: their routes, such as team and billing
// (/build/angular-session-replay), 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';

export const routes: Routes = [
  { path: '', component: Home },
  { path: 'projects/new', component: NewProject },
];

If app.routes.ts already has routes, including ones other Angular pages added, merge these two into the existing array instead of replacing it.

Click handlers only run after a click in the browser, so this is safe on a server-rendered page. Don't call track from a constructor or ngOnInit, which also run during server rendering. For events your server should send, such as a payment confirmed by your API, use the Node.js SDK there; the Node.js product analytics page covers identity and flushing.

MCP. Working in Cursor or Claude Code? Amplitude's MCP server lets your agent read your project's existing event taxonomy before it writes track calls, so new events match names you already use instead of inventing projectCreated next to Project Created. Set up the MCP server.

Verify it worked

Run ng serve, open http://localhost:4200, click Sign in as test user, go to New project, create a project, then go back to Home. Check in this order:

  1. Network tab. Filter for api2.amplitude.com. You should see a POST to /2/httpapi returning 200 within about a second of each navigation. With Angular SSR, Amplitude requests start only after the page loads in the browser; the server render sends none.
  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. The event stream shows [Amplitude] Page Viewed for each route and Project Created with its template property within ten seconds to a minute, and the profile shows plan. Events from before you signed in belong to the same device ID.

To watch payloads in the console, add logLevel: amplitude.Types.LogLevel.Debug to the analytics block in development.

Troubleshooting

No requests in the Network tab

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 file exists, run ng generate environments again and check fileReplacements in angular.json.

Clicks or form inputs stop working after you add Amplitude

initAll ran inside the Angular zone, for example from a component, a service, or provideAppInitializer. Call initAmplitude() from src/main.ts before bootstrapApplication. If it has to run inside Angular, wrap it: inject(NgZone).runOutsideAngular(() => initAmplitude()).

"window is not defined" or "document is not defined" during server rendering

Something calls the SDK on the server: initAmplitude() imported into app.config.ts or a component, a track call in a constructor or ngOnInit, or an identity effect without the isPlatformBrowser check. Keep initAmplitude() in main.ts, identity in AmplitudeIdentityService, and track in click handlers.

Two page view events per navigation

A Router events subscription or a page view helper is running alongside autocapture. Delete it. If both events are [Amplitude] Page Viewed, check that initAmplitude() is only called from src/main.ts.

Signed-in events still show as anonymous

AmplitudeIdentityService never gets created, or it can't see the user. Check that the App constructor injects it and that your auth exposes the user as a signal; an observable needs toSignal() first.

Production records every session

environment.ts is missing production: true, so environment.production is undefined and the sample rate falls through to 1. Add the field to both environment files.

Events send but never appear in the project

EU projects need serverZone: 'EU' at the top level of the initAll options and an API key from the EU project.

Autocapture does something different from the code

Remote configuration is on by default, so autocapture settings saved in Amplitude under Data > Settings > Autocapture take precedence over the autocapture block in src/app/amplitude.ts. To keep autocapture under code control, add remoteConfig: { fetchRemoteConfig: false } to the analytics block.

Full code reference

Every file from this tutorial in one block, the version to hand to an agent. 10 files. Merge the routes into your existing array, and on Angular 19 put the App code in app.component.ts with the class AppComponent.

10 files, 207 lines
// -- src/environments/environment.ts --
// Keep lines other Angular pages added here: amplitudeDeploymentKey (/build/angular-feature-flags).
export const environment = {
  production: true,
  amplitudeApiKey: 'your-project-api-key',
};

// -- src/environments/environment.development.ts --
// Keep lines other Angular pages added here: amplitudeDeploymentKey (/build/angular-feature-flags).
export const environment = {
  production: false,
  amplitudeApiKey: 'your-project-api-key',
};

// -- src/app/amplitude.ts --
// Keep lines other Angular pages added here: debugMode and privacyConfig
// (/build/angular-session-replay); 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;
  }

  void amplitude.initAll(environment.amplitudeApiKey, {
    analytics: {
      autocapture: {
        attribution: true,
        pageViews: true,
        sessions: true,
        formInteractions: true,
        fileDownloads: true,
        elementInteractions: true,
      },
    },
    sessionReplay: {
      sampleRate: environment.production ? 0.01 : 1,
    },
  });
}

// -- src/main.ts --
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
// Angular 19 and earlier: src/app/app.component.ts, class AppComponent.
import { App } from './app/app';
import { initAmplitude } from './app/amplitude';

initAmplitude();

bootstrapApplication(App, appConfig).catch((err) => console.error(err));

// -- src/app/auth.service.ts --
// Keep lines other Angular pages added here: closeAll() before reset()
// (/build/angular-guides-surveys), experiment()?.clear() and refetch() after
// reset() (/build/angular-feature-flags).
import { Injectable, signal } from '@angular/core';
import * as amplitude from '@amplitude/unified';

export interface User {
  id: string;
  plan?: string;
}

@Injectable({ providedIn: 'root' })
export class AuthService {
  readonly user = signal<User | null>(null);

  signIn(user: User): void {
    // ... your sign-in logic
    this.user.set(user);
  }

  async signOut(): Promise<void> {
    // ... your sign-out logic
    this.user.set(null);
    amplitude.reset();
  }
}

// -- src/app/amplitude-identity.service.ts --
import { Injectable, PLATFORM_ID, computed, effect, inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import * as amplitude from '@amplitude/unified';
import { AuthService } from './auth.service';

@Injectable({ providedIn: 'root' })
export class AmplitudeIdentityService {
  private readonly auth = inject(AuthService);
  private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));

  private readonly identity = computed(
    () => {
      const user = this.auth.user();
      return user ? { id: user.id, plan: user.plan ?? 'unknown' } : null;
    },
    { equal: (a, b) => a?.id === b?.id && a?.plan === b?.plan },
  );

  constructor() {
    if (!this.isBrowser) return;

    effect(() => {
      const identity = this.identity();
      if (!identity) return;
      // Amplitude drops IDs shorter than 5 characters.
      amplitude.setUserId(`user-${identity.id}`);
      const identify = new amplitude.Identify();
      identify.set('plan', identity.plan);
      amplitude.identify(identify);
    });
  }
}

// -- src/app/app.ts --
// Angular 19 and earlier: src/app/app.component.ts, class AppComponent.
// Keep lines other Angular pages added here: nav links (/build/angular-session-replay,
// /build/angular-heatmaps), EngagementBridgeService and <app-feedback-button />
// (/build/angular-guides-surveys), ServerFlagsService (/build/angular-feature-flags).
import { Component, inject } from '@angular/core';
import { RouterLink, RouterOutlet } from '@angular/router';
import { AmplitudeIdentityService } from './amplitude-identity.service';
import { AuthService } from './auth.service';

@Component({
  selector: 'app-root',
  imports: [RouterOutlet, RouterLink],
  template: `
    <nav>
      <a routerLink="/">Home</a>
      <a routerLink="/projects/new">New project</a>
      @if (auth.user()) {
        <button type="button" (click)="auth.signOut()">Sign out</button>
      } @else {
        <button type="button" (click)="auth.signIn({ id: '42', plan: 'pro' })">
          Sign in as test user
        </button>
      }
    </nav>
    <router-outlet />
  `,
})
export class App {
  protected readonly auth = inject(AuthService);

  constructor() {
    inject(AmplitudeIdentityService);
  }
}

// -- src/app/home.ts --
// Keep lines other Angular pages added here: data-exp attributes
// (/build/angular-web-experiment), data-guide attributes (/build/angular-guides-surveys).
import { Component } from '@angular/core';

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

// -- src/app/projects/new-project.ts --
// Keep lines other Angular pages added here: /build/angular-feature-flags replaces this
// file with a flag-driven version; if you followed it, keep that version.
import { Component, signal } from '@angular/core';
import * as amplitude from '@amplitude/unified';

@Component({
  selector: 'app-new-project',
  template: `
    <select #picker (change)="template.set(picker.value)">
      <option value="blank">Blank</option>
      <option value="kanban">Kanban</option>
    </select>
    <button type="button" (click)="create()">Create project</button>
  `,
})
export class NewProject {
  protected readonly template = signal('blank');

  protected create(): void {
    // ... your create logic

    amplitude.track('Project Created', { template: this.template() });
  }
}

// -- src/app/app.routes.ts --
// Keep lines other Angular pages added here: their routes, such as team and billing
// (/build/angular-session-replay), 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';

export const routes: Routes = [
  { path: '', component: Home },
  { path: 'projects/new', component: NewProject },
];

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
Add Amplitude analytics to this Angular app by following
https://amplitude.com/build/angular-product-analytics exactly. Install
@amplitude/unified and run ng generate environments. Put production and
amplitudeApiKey in both src/environments/environment.ts (production: true)
and environment.development.ts (production: false); leave the key as
your-project-api-key and tell me where to paste mine. Create
src/app/amplitude.ts exporting initAmplitude(), which calls initAll once
with autocapture elementInteractions on and sessionReplay.sampleRate set
to environment.production ? 0.01 : 1. Call initAmplitude() in
src/main.ts before bootstrapApplication, never in a component, service,
provideAppInitializer, app.config.ts, or main.server.ts. Don't subscribe
to Router events for page views; autocapture tracks navigations. If the
app has auth, expose the user as a signal (toSignal for observables) and
add amplitude.reset() to its sign-out method after sign-out succeeds; if
not, create the page's stand-in src/app/auth.service.ts. Create
src/app/amplitude-identity.service.ts as on the page: return early when
not in the browser (isPlatformBrowser), set the user ID with a "user-"
prefix and plan with Identify in an effect, and never call reset() when
the user is null. Use the root component file the app already has
(app.ts or app.component.ts); never create a second one. Inject the
service in its constructor and merge the nav, sign-in and sign-out
buttons into the existing root template. Add the page's Home and
NewProject components and merge both routes into the existing array in
src/app/app.routes.ts. Only call track from click handlers, and invent no
event names other than Project Created.

Other Amplitude Build pages for Angular add lines to these same files
(the environment files, amplitude.ts, auth.service.ts, the root
component, home.ts, new-project.ts, app.routes.ts). If any of these files
already exist, merge into them and never delete lines another Amplitude
Build page added, such as the experiment block, a setAmplitudeReady(...)
wrapper, debugMode and privacyConfig, or a webExperiment.plugin()
registration in amplitude.ts.

Tell me session replay records every session under ng serve and about 1%
in production, and how to turn it off. Report every file you changed.

What this unlocks

  • Session replay, already on from step 2, lets you watch the session where Project Created fired with template: kanban.
  • Heatmaps show where people click on /projects/new before they create one, built from those same session replays.
  • Feature Experiment, bundled in the same package, can test a new template picker with Project Created as the metric.
  • Guides and surveys, also bundled, can prompt users who viewed /projects/new but never fired Project Created.

One integration; everything else reads from the same events.

Next steps

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

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.