---
title: "How to add product analytics to a React Router app (formerly Remix)"
description: "Add Amplitude to a React Router (formerly Remix) app from the client entry, capture page views, identify signed-in users, and confirm events land."
canonical: "https://amplitude.com/en-us/build/react-router-product-analytics"
---

# How to add product analytics to a React Router app (formerly Remix)

Add Amplitude to a React Router (formerly Remix) app from the client entry, capture page views, identify signed-in users, and confirm events land.

Source: https://amplitude.com/en-us/build/react-router-product-analytics

---

# How to add product analytics to a React Router app (formerly Remix)

Install the Amplitude Unified SDK in a React Router app through the client entry, capture page views on every client-side navigation, identify signed-in users from your root loader, and track one custom event. About 10 minutes, all copy-paste.

- React Router 7+
- Framework mode
- Unified SDK 1.x
- 10 min

Chris Tam · Last updated 14 October 2026

Remix now ships as React Router's framework mode: loaders, actions, and server rendering, with the browser taking over navigation after the first page. [Product analytics](https://amplitude.com/amplitude-analytics) shows you what people do once that page is interactive: which routes they visit, what they use, and where they drop off.

In this tutorial, you'll add Amplitude to a React Router app through the client entry, capture page views, identify signed-in users, and track your first custom event.

## What you'll build

A React Router app that sends `[Amplitude] Page Viewed` on every navigation, attaches a `user-` prefixed user ID and a `plan` user property once someone signs in, and tracks `Project Created` with a `template` property from a `/projects/new` page. By the end you'll see those events on your test user's profile in Amplitude.

## Prerequisites

- A React Router 7 or later app in framework mode with TypeScript, for example one created with `npx create-react-router@latest`. This is the current form of Remix. Still on Remix v2? It stopped receiving security fixes on 17 June 2026; move to React Router first with the [Remix upgrade guide](https://reactrouter.com/upgrading/remix), then follow this page.
- Server rendering on, which is the template default (no `ssr: false` in `react-router.config.ts`). Step 4 reads the user in a server `loader`; for an `ssr: false` app, see Troubleshooting.
- An auth helper that reads the signed-in user from the request, for example `getUser(request)` in `app/auth.server.ts`, returning an object with an `id` and a `plan`, or `null`. No auth yet? Step 4 gives you a stub.
- An Amplitude project API key. In Amplitude, go to Settings > Projects > your project > General.

No Amplitude account yet? A free account covers the tracking on this page. [Sign up for free](https://app.amplitude.com/signup) to get your project API key. Your session replay capture quota depends on your plan; [compare plans](https://amplitude.com/pricing).

## 1. Install the SDK and add the key

The [Quickstart](https://amplitude.com/docs/get-started/amplitude-quickstart) installs `@amplitude/unified` for browser apps that use npm. It bundles analytics, session replay, Feature Experiment, and guides and surveys behind one init call.

Terminal

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

React Router builds with Vite, which only exposes env vars prefixed with `VITE_` to browser code. The project API key is public and its scope is limited to the minimum needed to ingest data, so shipping it to the browser is expected, but keep it out of source anyway.

.env

```sh
# Keep lines other React Router pages added here: VITE_AMPLITUDE_DEPLOYMENT_KEY and
# AMPLITUDE_SERVER_DEPLOYMENT_KEY (/build/react-router-feature-flags).
VITE_AMPLITUDE_API_KEY=your-project-api-key
```

## 2. Initialize in the client entry

`app/entry.client.tsx` is the first code that runs in the browser, and it runs once per page load, before React hydrates the server-rendered HTML. That's where `initAll` belongs. React Router uses a built-in default until you reveal the file, so reveal it first. This writes `app/entry.client.tsx` and `app/entry.server.tsx`; leave the server file as generated.

Terminal

```sh
npx react-router reveal
```

Then add the Amplitude import and the init block above the existing hydrate call. Other React Router pages add lines to this file later; the first comment names them, so you can keep them when you come back here.

app/entry.client.tsx

```typescript
// Keep lines other React Router pages added here: pageViews: { trackHistoryChanges:
// "pathOnly" } (/build/react-router-web-analytics); debugMode and privacyConfig
// (/build/react-router-session-replay); the amplitude-cookies import and handoff
// lines, the deviceId spread, and the experiment block
// (/build/react-router-feature-flags); the setAmplitudeReady import, the
// setAmplitudeReady(...) wrapper, and setAmplitudeReady(null)
// (/build/react-router-feature-flags, /build/react-router-guides-surveys); the
// amplitude.add(webExperiment.plugin()) lines (/build/react-router-web-experiment).
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
import * as amplitude from "@amplitude/unified";

const apiKey = import.meta.env.VITE_AMPLITUDE_API_KEY;

if (apiKey) {
  // initAll returns a promise; nothing on this page needs to wait for it.
  void amplitude.initAll(apiKey, {
    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: import.meta.env.DEV ? 1 : 0.01,
    },
  });
} else {
  console.warn("VITE_AMPLITUDE_API_KEY is not set; analytics is off.");
}

startTransition(() => {
  hydrateRoot(
    document,
    <StrictMode>
      <HydratedRouter />
    </StrictMode>
  );
});
```

**Why.** `root.tsx` and every route module also run on the server, where there's no `window`, and code in a component body runs again on every render. The client entry runs only in the browser and only once, so you get one SDK instance without a guard.

`@amplitude/unified` turns on session replay when `sessionReplay.sampleRate` is above 0. This config records every session in development and about 1% in production, which is where Amplitude recommends starting; raise it over a few days. Set it to `0` to turn session replay off. For the full option list, see the [Browser Unified SDK reference](https://amplitude.com/docs/sdks/analytics/browser/browser-unified-sdk).

## 3. Let autocapture handle navigation

There's no code in this step. After the first server-rendered page, React Router handles navigation in the browser. With `pageViews: true`, the SDK sends `[Amplitude] Page Viewed` on init and again on every URL change, which covers `<Link>`, `<Form>` redirects, and `navigate()`. Each event carries the page path, title, and URL.

Don't add a `useLocation` effect that calls `track('Page Viewed')`. It produces a second, differently named page view on every navigation.

## 4. Identify the signed-in user

Until you set a user ID, events belong to an anonymous device ID. Set it as soon as the page knows who's signed in, and reset only when someone signs out, so the next person on the same browser isn't merged into the last one.

No auth yet? Add this stub so the root loader compiles; every visitor stays anonymous until you replace it with your auth library's lookup.

app/auth.server.ts (stub until you add auth)

```typescript
export type User = { id: string | number; plan?: string | null };

export async function getUser(_request: Request): Promise<User | null> {
  return null;
}
```

Return the user from the root loader, so every page has it, then identify from an effect in the root `App` component. In your existing `app/root.tsx`, add the loader and replace the default `App` export. The template already imports `Outlet` and `Route`; add only the missing imports, and leave its `links`, `Layout`, and `ErrorBoundary` exports as they are.

app/root.tsx (loader and App export; keep the template's imports, links, Layout and ErrorBoundary)

```typescript
// Keep lines other React Router pages added here: the refetchBrowserFlags import and
// calls (/build/react-router-feature-flags); useEngagementBridge() and
// <FeedbackButton /> (/build/react-router-guides-surveys); the experiment <script>
// in Layout's head (/build/react-router-web-experiment).

// Add these imports. The template already imports Outlet and the Route types.
import { useEffect, useRef } from "react";
import * as amplitude from "@amplitude/unified";
import { getUser } from "./auth.server";

export async function loader({ request }: Route.LoaderArgs) {
  const user = await getUser(request);
  return {
    user: user ? { id: String(user.id), plan: user.plan ?? "unknown" } : null,
  };
}

export default function App({ loaderData }: Route.ComponentProps) {
  const id = loaderData.user?.id;
  const plan = loaderData.user?.plan ?? "unknown";

  // The root loader reruns after every action. Only act when the
  // identity actually changes.
  const lastIdentity = useRef<string | undefined>(undefined);

  useEffect(() => {
    const identity = id ? `user-${id}|${plan}` : "anonymous";
    if (identity === lastIdentity.current) return;
    lastIdentity.current = identity;

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

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

    const identify = new amplitude.Identify();
    identify.set("plan", plan);
    amplitude.identify(identify);
  }, [id, plan]);

  return <Outlet />;
}
```

`getUser` only runs inside the loader, so React Router keeps `auth.server.ts` out of the browser bundle. Every page now carries the signed-in user in its loader data, so don't let a shared cache store signed-in pages.

Reset from your sign-out form, not from the root component. If you don't have a sign-out route yet, skip SignOutButton.tsx until you do.

app/components/SignOutButton.tsx

```typescript
// Keep lines other React Router pages added here: the getEngagement import and
// getEngagement()?.gs.closeAll() before reset() (/build/react-router-guides-surveys);
// amplitude.experiment()?.clear() after reset() (/build/react-router-feature-flags).
// Order: closeAll, reset, clear.
import { Form } from "react-router";
import * as amplitude from "@amplitude/unified";

export function SignOutButton() {
  return (
    <Form
      method="post"
      action="/logout"
      onSubmit={() => {
        // Clears the user ID and issues a new device ID for the next person.
        amplitude.reset();
      }}
    >
      <button type="submit">Sign out</button>
    </Form>
  );
}
```

Point `action` at your own sign-out route. Render `<SignOutButton />` wherever your signed-in navigation lives.

**Why.** A `null` user also means every anonymous visitor and every page where auth hasn't resolved. Calling `reset()` there issues a new device ID on each of those renders, which splits one visitor into many and breaks device-based flag bucketing later.

**MCP.** Working in Cursor or Claude Code? [Amplitude's MCP server](https://amplitude.com/docs/amplitude-ai/amplitude-mcp) 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`.

## 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 handler that only runs in the browser.

app/components/CreateProjectForm.tsx

```typescript
import { useState } from "react";
import * as amplitude from "@amplitude/unified";

export function CreateProjectForm() {
  const [template, setTemplate] = useState("blank");

  async function handleCreate() {
    // ... your create logic

    amplitude.track("Project Created", { template });
  }

  return (
    <>
      <select value={template} onChange={(e) => setTemplate(e.target.value)}>
        <option value="blank">Blank</option>
        <option value="kanban">Kanban</option>
      </select>
      <button type="button" onClick={handleCreate}>
        Create project
      </button>
    </>
  );
}
```

Event handlers only run after a click in the browser, so this is safe in a server-rendered route. Don't call `track` in a component body, a `loader`, or an `action`, which run on the server. If creation happens in an `action`, track after it succeeds, for example when `useActionData()` or `fetcher.data` returns the new project, still in the browser. For events your server should send, such as a payment confirmed in an `action`, use the Node.js SDK there; the [Node.js product analytics page](/build/nodejs-product-analytics) covers identity and flushing on the server.

Give the form a page at `/projects/new`. Later React Router pages link to this route, and the feature flags page replaces its contents with a flag-driven version.

app/routes/new-project.tsx

```typescript
// Keep lines other React Router pages added here: /build/react-router-feature-flags replaces
// this file with a flag-driven version; if you have that version, keep it.
import { CreateProjectForm } from "../components/CreateProjectForm";

export default function NewProject() {
  return (
    <main>
      <h1>New project</h1>
      <CreateProjectForm />
    </main>
  );
}
```

Register it in `app/routes.ts`, next to the template's index route:

app/routes.ts

```typescript
// Keep lines other React Router pages added here: their routes
// (/build/react-router-web-analytics, /build/react-router-session-replay,
// /build/react-router-heatmaps). Merge new entries into the array.
import { type RouteConfig, index, route } from "@react-router/dev/routes";

export default [
  index("routes/home.tsx"),
  route("projects/new", "routes/new-project.tsx"),
] satisfies RouteConfig;
```

Using `flatRoutes()` from `@react-router/fs-routes` instead? Name the file `app/routes/projects.new.tsx` and skip the `routes.ts` entry.

## Verify it worked

Run `npm run dev`, open `/`, then `/projects/new`, create a project, then 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. Amplitude requests start only once the client entry runs 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 your `user-` prefixed test ID. The event stream shows `[Amplitude] Page Viewed` for each page and `Project Created` with its `template` property within ten seconds to a minute, and the profile shows `plan`. Using the auth stub? Search the device ID instead; the [Instrumentation Explorer](https://amplitude.com/docs/analytics/debug-analytics#instrumentation-explorer) Chrome extension shows it as `device_id` on each event; `plan` appears once a real user signs in.

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

## Troubleshooting

### The console says "VITE\_AMPLITUDE\_API\_KEY is not set"

Vite reads `.env` when the dev server starts and inlines the value at build time. Restart the dev server after editing `.env`, and set the variable in your CI or host's build environment, not only at runtime. The name must start with `VITE_`.

### No requests in the Network tab and no warning in the console

`app/entry.client.tsx` isn't the file React Router is using, or the template's `Layout` lost its `<Scripts />`. The client entry has to sit directly in your app directory (`app/` by default); run `npx react-router reveal` again if you created it by hand somewhere else. Check that `app/root.tsx` still exports the template's `Layout`.

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

Something calls the SDK on the server: an `initAll` or `track` at the top level of `root.tsx` or a route module, or a call inside a `loader` or `action`. Keep `initAll` in `app/entry.client.tsx`, identity in the `useEffect`, and `track` in event handlers.

### The build fails with "Could not resolve ./auth.server"

The root loader imports `getUser`, and the app has no `app/auth.server.ts`. Add the stub from step 4, or point the import at your auth library's helper.

### Two page view events per navigation

A `useLocation` effect or a page view helper is running alongside autocapture. Delete it.

### Signed-in events still show as anonymous

The root component never sees the user. Check that the root `loader` returns `user` and that your sign-in `action` redirects or returns, so React Router revalidates the root loader and the effect reruns.

### Signed-in users never get a user ID in an `ssr: false` app

SPA mode has no request-time server loader: the root `loader` runs at build time, if at all, with no cookies, so `user` is always `null`. Return the user from a root `clientLoader` that calls your auth client's session endpoint, and read it the same way in `App`.

### 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 `app/entry.client.tsx`. 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. 8 files, 179 lines. Shared files start with a comment naming the lines other React Router pages add there; merge into your file and keep those lines.

8 files, 179 lines

```typescript
// -- .env --
# Keep lines other React Router pages added here: VITE_AMPLITUDE_DEPLOYMENT_KEY and
# AMPLITUDE_SERVER_DEPLOYMENT_KEY (/build/react-router-feature-flags).
VITE_AMPLITUDE_API_KEY=your-project-api-key

// -- app/entry.client.tsx --
// Keep lines other React Router pages added here: pageViews: { trackHistoryChanges:
// "pathOnly" } (/build/react-router-web-analytics); debugMode and privacyConfig
// (/build/react-router-session-replay); the amplitude-cookies import and handoff
// lines, the deviceId spread, and the experiment block
// (/build/react-router-feature-flags); the setAmplitudeReady import, the
// setAmplitudeReady(...) wrapper, and setAmplitudeReady(null)
// (/build/react-router-feature-flags, /build/react-router-guides-surveys); the
// amplitude.add(webExperiment.plugin()) lines (/build/react-router-web-experiment).
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
import * as amplitude from "@amplitude/unified";

const apiKey = import.meta.env.VITE_AMPLITUDE_API_KEY;

if (apiKey) {
  // initAll returns a promise; nothing on this page needs to wait for it.
  void amplitude.initAll(apiKey, {
    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: import.meta.env.DEV ? 1 : 0.01,
    },
  });
} else {
  console.warn("VITE_AMPLITUDE_API_KEY is not set; analytics is off.");
}

startTransition(() => {
  hydrateRoot(
    document,
    <StrictMode>
      <HydratedRouter />
    </StrictMode>
  );
});

// -- app/auth.server.ts (stub until you add auth) --
export type User = { id: string | number; plan?: string | null };

export async function getUser(_request: Request): Promise<User | null> {
  return null;
}

// -- app/root.tsx (loader and App export; keep the template's imports, links, Layout and ErrorBoundary) --
// Keep lines other React Router pages added here: the refetchBrowserFlags import and
// calls (/build/react-router-feature-flags); useEngagementBridge() and
// <FeedbackButton /> (/build/react-router-guides-surveys); the experiment <script>
// in Layout's head (/build/react-router-web-experiment).

// Add these imports. The template already imports Outlet and the Route types.
import { useEffect, useRef } from "react";
import * as amplitude from "@amplitude/unified";
import { getUser } from "./auth.server";

export async function loader({ request }: Route.LoaderArgs) {
  const user = await getUser(request);
  return {
    user: user ? { id: String(user.id), plan: user.plan ?? "unknown" } : null,
  };
}

export default function App({ loaderData }: Route.ComponentProps) {
  const id = loaderData.user?.id;
  const plan = loaderData.user?.plan ?? "unknown";

  // The root loader reruns after every action. Only act when the
  // identity actually changes.
  const lastIdentity = useRef<string | undefined>(undefined);

  useEffect(() => {
    const identity = id ? `user-${id}|${plan}` : "anonymous";
    if (identity === lastIdentity.current) return;
    lastIdentity.current = identity;

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

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

    const identify = new amplitude.Identify();
    identify.set("plan", plan);
    amplitude.identify(identify);
  }, [id, plan]);

  return <Outlet />;
}

// -- app/components/SignOutButton.tsx --
// Keep lines other React Router pages added here: the getEngagement import and
// getEngagement()?.gs.closeAll() before reset() (/build/react-router-guides-surveys);
// amplitude.experiment()?.clear() after reset() (/build/react-router-feature-flags).
// Order: closeAll, reset, clear.
import { Form } from "react-router";
import * as amplitude from "@amplitude/unified";

export function SignOutButton() {
  return (
    <Form
      method="post"
      action="/logout"
      onSubmit={() => {
        // Clears the user ID and issues a new device ID for the next person.
        amplitude.reset();
      }}
    >
      <button type="submit">Sign out</button>
    </Form>
  );
}

// -- app/components/CreateProjectForm.tsx --
import { useState } from "react";
import * as amplitude from "@amplitude/unified";

export function CreateProjectForm() {
  const [template, setTemplate] = useState("blank");

  async function handleCreate() {
    // ... your create logic

    amplitude.track("Project Created", { template });
  }

  return (
    <>
      <select value={template} onChange={(e) => setTemplate(e.target.value)}>
        <option value="blank">Blank</option>
        <option value="kanban">Kanban</option>
      </select>
      <button type="button" onClick={handleCreate}>
        Create project
      </button>
    </>
  );
}

// -- app/routes/new-project.tsx --
// Keep lines other React Router pages added here: /build/react-router-feature-flags replaces
// this file with a flag-driven version; if you have that version, keep it.
import { CreateProjectForm } from "../components/CreateProjectForm";

export default function NewProject() {
  return (
    <main>
      <h1>New project</h1>
      <CreateProjectForm />
    </main>
  );
}

// -- app/routes.ts --
// Keep lines other React Router pages added here: their routes
// (/build/react-router-web-analytics, /build/react-router-session-replay,
// /build/react-router-heatmaps). Merge new entries into the array.
import { type RouteConfig, index, route } from "@react-router/dev/routes";

export default [
  index("routes/home.tsx"),
  route("projects/new", "routes/new-project.tsx"),
] satisfies RouteConfig;
```

## 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
Add Amplitude analytics to this React Router (formerly Remix) app by
following https://amplitude.com/build/react-router-product-analytics exactly.
Install @amplitude/unified. Read VITE_AMPLITUDE_API_KEY from
import.meta.env. Reveal app/entry.client.tsx with
"npx react-router reveal" if it doesn't exist, and call initAll once
there, before hydrateRoot, with autocapture elementInteractions on. Never
call initAll in root.tsx, a route module, a loader, or a useEffect. Don't
add a useLocation page view effect; autocapture tracks navigations.
Merge into existing shared files (app/entry.client.tsx, app/root.tsx,
app/components/SignOutButton.tsx, app/routes.ts) and never delete lines
another Amplitude Build page added; each file's first comment names them.
In app/root.tsx, add the loader and replace only the default App export;
keep the template's imports, links, Layout (with <Scripts />) and
ErrorBoundary. Return the user from the root loader using my existing
auth helper, or create the page's app/auth.server.ts stub (getUser
returns null) if none exists. Set the user ID with a "user-" prefix in a
useEffect in App, skipping reruns where the user ID and plan haven't
changed. Call reset() only from the sign-out form's onSubmit, never when
the user is null. If there's no sign-out route, skip SignOutButton.tsx
and tell me. Set sessionReplay.sampleRate to
import.meta.env.DEV ? 1 : 0.01 and tell me how to turn it off. Only call
track from event handlers. Add the page's Project Created example in
app/components/CreateProjectForm.tsx, create app/routes/new-project.tsx
at projects/new rendering it if it doesn't exist, and register it in
app/routes.ts. Invent no other event names. If the app sets ssr: false,
tell me, and read the user from a root clientLoader instead. Don't
hardcode the key. Report every file you changed.
```

## What this unlocks

- [Session replay](https://amplitude.com/session-replay), already on from step 2, lets you watch the session where `Project Created` fired with `template: kanban`.
- [Heatmaps](https://amplitude.com/heatmaps) show where people click on `/projects/new` before they create a project, built from those same session replays.
- [Feature Experiment](https://amplitude.com/amplitude-experiment), bundled in the same package, can test a new template picker with `Project Created` as the metric.
- [Guides and surveys](https://amplitude.com/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

[Web frameworksHow to add session replay to a React Router appTune the sample rate and mask sensitive components before you record real users.](/build/react-router-session-replay)[Web frameworksHow to add feature flags and experiments to a React Router appRender the right variant in the first HTML and use \`Project Created\` as the experiment's metric.](/build/react-router-feature-flags)[Use caseHow to stitch client and server events to one userKeep browser events and your actions' server events on one profile through sign-up.](/build/stitch-client-server-events)

Reference docs

[Amplitude Quickstart →](https://amplitude.com/docs/get-started/amplitude-quickstart)[Browser Unified SDK →](https://amplitude.com/docs/sdks/analytics/browser/browser-unified-sdk)[Browser SDK 2 →](https://amplitude.com/docs/sdks/analytics/browser/browser-sdk-2)[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/react-router-product-analytics.md)[llms.txt](/llms.txt)

## Ship the instrumentation, then read the data

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

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