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

# How to add session replay to a Vue app

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

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

---

# How to add session replay to a Vue app

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

- Vue 3
- Vite
- Vue Router
- Unified SDK 1.x
- 15 min

Chris Tam · Last updated 9 October 2026

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

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

## What you'll build

A Vue app that records every session in development and 1% of sessions in production, masks text inputs by default, masks anything wrapped in an `AmpMask` component and every screen under `/billing`, replaces blocked elements with a placeholder, and tags the session's events with `[Amplitude] Session Replay ID`. By the end you'll play back your own test session from your event stream in Amplitude.

## Prerequisites

- A Vue 3 app with Vite, TypeScript, and Vue Router, for example one created with `npm create vue@latest`.
- An Amplitude project API key. In Amplitude, go to Settings > Projects > your project > General.
- To change sampling or masking in Amplitude's settings, an Admin or Manager role. The code on this page doesn't need it.

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

## 1. Install the SDK

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

Terminal

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

Vite 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.local

```sh
VITE_AMPLITUDE_API_KEY=your-project-api-key
```

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

## 2. Set a sample rate per environment

Initialize once, at module scope, and choose the sample rate from Vite's build mode. This file replaces `src/lib/amplitude.ts` from the Vue product analytics page; the `analytics` block is unchanged.

src/lib/amplitude.ts

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

const apiKey = import.meta.env.VITE_AMPLITUDE_API_KEY;

if (apiKey) {
  amplitude.initAll(apiKey, {
    analytics: {
      autocapture: {
        attribution: true,
        pageViews: true,
        // Tracks [Amplitude] Start Session and End Session events.
        sessions: true,
        formInteractions: true,
        fileDownloads: true,
        elementInteractions: true,
      },
    sessionReplay: {
      // Fraction of sessions to record: every session in `vite dev`, 1% in a production build.
      sampleRate: import.meta.env.PROD ? 0.01 : 1,
      // Ignores the focus check so replays record with DevTools open. Setup only.
      debugMode: !import.meta.env.PROD,
      // Selectors for the data attributes in steps 3 and 4.
      privacyConfig: {
        maskSelector: ['[data-amp-mask]'],
        blockSelector: ['[data-amp-block]'],
        unmaskSelector: ['[data-amp-unmask]'],
      },
  });
} else {
  console.warn('VITE_AMPLITUDE_API_KEY is not set; analytics is off.');
}

export { amplitude };
```

Import it first in your entry file, before `createApp`. If you followed the base page, this file is already in place:

src/main.ts

```typescript
import './lib/amplitude';
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';

createApp(App).use(router).mount('#app');
```

**Why.** Session replay takes a full snapshot of the page when it initializes, and nothing that happens before that point appears in the replay. A module body runs once per page load, before `createApp`, so recording starts as early as the app allows. It also matters for matching: initializing the SDK more than once can leave the replay and the session's events with different device or session IDs, and a replay that doesn't match its events is one you can't open from the event stream. An `initAll` inside a component's `onMounted` runs again every time that component mounts.

Later pages in this hub build on this file. If you copy a version of `src/lib/amplitude.ts` that lacks `privacyConfig`, keep this block.

The plugin's own default sample rate is `0`, so nothing records until you set one. Amplitude recommends starting production low, around `0.01`, and raising it over a few days; capture stops for the month once your org's quota runs out. To stop recording, set the rate to `0`. Session Replay Settings in Amplitude can also set a project's rate remotely, so check Settings > Organizational Settings > Session Replay Settings when the rate you see doesn't match your code. For every option, see the [session replay plugin configuration](https://amplitude.com/docs/sdks/session-replay/session-replay-plugin#configuration); the Unified SDK accepts all of them inside `sessionReplay` except `serverZone` and `instanceName`, which go at the top level of the `initAll` options.

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

On Nuxt, initialize in `app/plugins/amplitude.client.ts` (`plugins/` on Nuxt 3) as on [How to add product analytics to a Nuxt app](/build/nuxt-product-analytics), and keep SDK calls out of the server render. Pass the same `sessionReplay` block with Nuxt's dev flag; the `AmpMask` component and data attributes from step 3 work unchanged. For step 4, wrap `<NuxtPage />` in `<AmpMask>` in `app/pages/billing.vue` (`pages/billing.vue` on Nuxt 3) and put the child screens in `app/pages/billing/`.

app/plugins/amplitude.client.ts

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

export default defineNuxtPlugin(() => {
  const apiKey = useRuntimeConfig().public.amplitudeApiKey;

  if (!apiKey) {
    console.warn('NUXT_PUBLIC_AMPLITUDE_API_KEY is not set; analytics is off.');
    return;
  }

  amplitude.initAll(apiKey, {
    analytics: {
      autocapture: {
        attribution: true,
        pageViews: true,
        sessions: true,
        formInteractions: true,
        fileDownloads: true,
        elementInteractions: true,
      },
    sessionReplay: {
      sampleRate: import.meta.dev ? 1 : 0.01,
      debugMode: import.meta.dev,
      privacyConfig: {
        maskSelector: ['[data-amp-mask]'],
        blockSelector: ['[data-amp-block]'],
        unmaskSelector: ['[data-amp-unmask]'],
      },
  });
```

## 3. Mask sensitive elements in Vue templates

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

The SDK recognizes three classes out of the box, `amp-mask`, `amp-block`, and `amp-unmask`, and Vue merges a static `class` with a `:class` binding, so they work in templates. The data attributes registered in step 2 keep privacy separate from styling, and they're what the wrapper component below sets. Put one small component in charge of masking, so every template says it the same way:

src/components/AmpMask.vue

```html
<script setup lang="ts">
const props = withDefaults(
  defineProps<{
    // Replace the content with a same-size placeholder instead of asterisks.
    block?: boolean;
    // Use 'span' inside inline content.
    tag?: string;
  }>(),
  { block: false, tag: 'div' },
);
</script>

<template>
  <component
    :is="props.tag"
    :data-amp-mask="props.block ? undefined : ''"
    :data-amp-block="props.block ? '' : undefined"
  >
    <slot />
  </component>
</template>
```

Use it in any template, next to plain data attributes where a wrapper would be overkill:

src/views/TeamView\.vue

```html
<script setup lang="ts">
import { ref } from 'vue';
import AmpMask from '../components/AmpMask.vue';

type Member = { id: string; name: string; email: string; avatarUrl: string };

// Replace with data from your API.
const members = ref<Member[]>([
  { id: '1', name: 'Test User', email: 'test@example.com', avatarUrl: 'https://placehold.co/32' },
]);
</script>

<template>
  <main>
    <h1>Team</h1>

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

    <ul>
      <li v-for="member in members" :key="member.id" :class="$style.row">
        <!-- Replaced by a same-size placeholder -->
        <AmpMask block tag="span">
          <img :src="member.avatarUrl" alt="" />
        </AmpMask>
        <span>{{ member.name }}</span>
        <!-- Captured as asterisks -->
        <AmpMask tag="span" :class="$style.email">{{ member.email }}</AmpMask>
      </li>
    </ul>
  </main>
</template>

<style module>
.row { display: flex; gap: 0.5rem; }
.email { color: #555; }
</style>
```

Vue renders a bare `data-amp-unmask` and the component's `''` value as `data-amp-unmask=""` and `data-amp-mask=""`, which the attribute selectors match. Binding `undefined` leaves the attribute off.

**Why.** The wrapper puts the attribute on an element you can see in the template. Writing `data-amp-mask` on a child component's tag, such as `<MemberEmail data-amp-mask />`, relies on Vue passing it through to that component's root element, which doesn't happen when the component has several root nodes or sets `inheritAttrs: false`, and nothing in the replay tells you. Masking also follows the DOM, not the component tree: content a component moves with `<Teleport to="body">`, such as a modal, leaves its masked parent, so wrap the teleported content itself. And masking covers text and form inputs, not HTML attributes. `alt`, `title`, `placeholder`, `aria-label`, `value`, and `data-*` attributes stay visible in replays even inside a masked element, so a tooltip bound as `:title="member.email"` leaks the address no matter what you mask.

Don't target `<style module>` or scoped style class names as masking selectors, in code or in Session Replay Settings. They're generated at build time and can change between builds, at which point the rule stops matching without any error.

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

## 4. Mask whole routes with a nested route

Some screens shouldn't show any text in a replay, such as billing, invoices, or a patient record. Masking each element there is how one gets missed. A Vue Router parent route with a layout component gives you one wrapper for a whole section:

src/layouts/MaskedLayout.vue

```html
<script setup lang="ts">
import AmpMask from '../components/AmpMask.vue';
</script>

<template>
  <!-- Everything rendered under this route is masked, including screens added later. -->
  <AmpMask>
    <RouterView />
  </AmpMask>
</template>
```

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

src/views/BillingView\.vue

```html
<template>
  <h1>Billing</h1>
  <p>Card ending 4242, billed to Test User</p>
</template>
```

src/views/InvoicesView\.vue

```html
<template>
  <h1>Invoices</h1>
</template>
```

Then register the layout as a parent route:

src/router/index.ts

```typescript
import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '../views/HomeView.vue';
import TeamView from '../views/TeamView.vue';
import MaskedLayout from '../layouts/MaskedLayout.vue';
import BillingView from '../views/BillingView.vue';
import InvoicesView from '../views/InvoicesView.vue';

export default createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    { path: '/', component: HomeView },
    { path: '/team', component: TeamView },
    {
      path: '/billing',
      component: MaskedLayout,
      children: [
        { path: '', component: BillingView },
        { path: 'invoices', component: InvoicesView },
      ],
    },
  ],
});
```

Keep the rest of your routes as they are. If your `App.vue` already calls `useAmplitudeIdentity` from the Vue product analytics page, leave it there.

**Why.** Masking an element through a selector masks all of its children, and a child inside it can't be unmasked again. That's what you want here: a developer who adds `/billing/payment-methods` next year gets masking without knowing this file exists. It also means `data-amp-unmask` does nothing under `/billing`, so put content you want visible outside the nested route.

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

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

## 5. Allow Amplitude in your CSP

A Vite app sends no `Content-Security-Policy` by default. Skip this step unless you added one, in a `<meta>` tag in `index.html` or in your host's response headers. If you did, add Amplitude's hosts to the `connect-src` directive you already have:

Add to your existing Content-Security-Policy (US data center)

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

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

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

## Verify it worked

Run `npm run dev`, open `http://localhost:5173` in a fresh private window, visit `/team`, type into the filter, open `/billing`, then check in this order:

1. **Console and Network tab.** No `Content Security Policy` errors in the console. In the Network tab, filter for `amplitude`: you should see event uploads to `api2.amplitude.com` returning `200`, a request to `sr-client-cfg.amplitude.com`, and replay uploads to `api-sr.amplitude.com`.
2. **Ingestion Debugger.** In Amplitude, click Data, select Sources, then open the Ingestion Debugger tab. The successful requests chart should show your event uploads. It shows counts, not individual events.
3. **User lookup.** Go to Users > User Profiles and search your device ID (the Instrumentation Explorer Chrome extension shows it as device\_id) or your user ID if you're signed in. Within ten seconds to a minute, the session's events, such as `[Amplitude] Start Session`, carry `[Amplitude] Session Replay ID`, and the event stream shows a **Play Session** button next to that session. Replays are generally ready about five minutes after the session begins.
4. **Session Replay debug panel.** Install the [Instrumentation Explorer](https://chrome.google.com/webstore/detail/amplitude-event-explorer/acehfjhnmhbmgkedjmjlobpgdicnhkbp) Chrome extension and turn on its Session Replay debug panel on your page. It floats over the page and shows the current replay ID, the session replay SDK and plugin versions, the sample rate, and the default and effective masking settings. A sample rate of `1` in development confirms step 2 took effect, and the effective masking settings show whether Session Replay Settings overrode your code.
5. **The replay itself.** Play it. On `/team`, the filter text is visible, emails are asterisks, and avatars are grey placeholders. On `/billing`, every line of text is masked while clicks and scrolling still show.

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

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

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

## Troubleshooting

### No session replay requests at all

The effective sample rate is `0`. Check `sessionReplay.sampleRate`, then the rate in Session Replay Settings; the debug panel shows the rate the SDK is using. A production build (`vite build`, then `vite preview`) records only about one session in a hundred at `0.01`, so test in `vite dev` or raise the rate temporarily.

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

Session replay only captures the tab that has focus, and an open DevTools panel can take it. Keep `debugMode: !import.meta.env.PROD` from step 2, which ignores the focus check in development.

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

No event in that session carries `[Amplitude] Session Replay ID`. The usual cause is a second `initAll` or `init` call, for example inside a component's `onMounted` or a Vue plugin's `install`, so the replay and the events disagree on device or session ID. Search the codebase and keep the one in `src/lib/amplitude.ts`.

### Masked text shows up in a replay

Check four things in order. The text sits in an attribute such as `title` or `aria-label`, which masking never covers. The `data-amp-mask` sits on a child component's tag and never reached the DOM; inspect the element, then wrap the content in `AmpMask`. The text renders through `<Teleport>` outside the masked wrapper. Or the element was unmasked in Session Replay Settings, which overrides the SDK; the debug panel's effective masking settings show it.

### Old replays lose their styling after a deploy

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

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

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

### No requests to Amplitude at all

The key is undefined, so the warning from step 2 is in the console. The variable must start with `VITE_`, and Vite only reads `.env.local` at startup, so restart the dev server. In a production build the value is inlined at build time; set it in your build environment and rebuild.

## Full code reference

Every file from this tutorial in one block, the version to hand to an agent. 9 files.

10 files, 144 lines

```typescript
// -- .env.local --
VITE_AMPLITUDE_API_KEY=your-project-api-key

// -- src/lib/amplitude.ts --
import * as amplitude from '@amplitude/unified';

const apiKey = import.meta.env.VITE_AMPLITUDE_API_KEY;

if (apiKey) {
  amplitude.initAll(apiKey, {
    analytics: {
      autocapture: {
        attribution: true,
        pageViews: true,
        sessions: true,
        formInteractions: true,
        fileDownloads: true,
        elementInteractions: true,
      },
    sessionReplay: {
      sampleRate: import.meta.env.PROD ? 0.01 : 1,
      debugMode: !import.meta.env.PROD,
      privacyConfig: {
        maskSelector: ['[data-amp-mask]'],
        blockSelector: ['[data-amp-block]'],
        unmaskSelector: ['[data-amp-unmask]'],
      },
  });
} else {
  console.warn('VITE_AMPLITUDE_API_KEY is not set; analytics is off.');
}

export { amplitude };

// -- src/main.ts --
import './lib/amplitude';
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';

createApp(App).use(router).mount('#app');

// -- src/components/AmpMask.vue --
<script setup lang="ts">
const props = withDefaults(
  defineProps<{ block?: boolean; tag?: string }>(),
  { block: false, tag: 'div' },
);
</script>

<template>
  <component
    :is="props.tag"
    :data-amp-mask="props.block ? undefined : ''"
    :data-amp-block="props.block ? '' : undefined"
  >
    <slot />
  </component>
</template>

// -- src/layouts/MaskedLayout.vue --
<script setup lang="ts">
import AmpMask from '../components/AmpMask.vue';
</script>

<template>
  <AmpMask>
    <RouterView />
  </AmpMask>
</template>

// -- src/router/index.ts --
import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '../views/HomeView.vue';
import TeamView from '../views/TeamView.vue';
import MaskedLayout from '../layouts/MaskedLayout.vue';
import BillingView from '../views/BillingView.vue';
import InvoicesView from '../views/InvoicesView.vue';

export default createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    { path: '/', component: HomeView },
    { path: '/team', component: TeamView },
    {
      path: '/billing',
      component: MaskedLayout,
      children: [
        { path: '', component: BillingView },
        { path: 'invoices', component: InvoicesView },
      ],
    },
  ],
});

// -- src/views/BillingView.vue --
<template>
  <h1>Billing</h1>
  <p>Card ending 4242, billed to Test User</p>
</template>

// -- src/views/InvoicesView.vue --
<template>
  <h1>Invoices</h1>
</template>

// -- src/views/TeamView.vue --
<script setup lang="ts">
import { ref } from 'vue';
import AmpMask from '../components/AmpMask.vue';

type Member = { id: string; name: string; email: string; avatarUrl: string };

// Replace with data from your API.
const members = ref<Member[]>([
  { id: '1', name: 'Test User', email: 'test@example.com', avatarUrl: 'https://placehold.co/32' },
]);
</script>

<template>
  <main>
    <h1>Team</h1>
    <input data-amp-unmask name="filter" placeholder="Filter by name" />
    <ul>
      <li v-for="member in members" :key="member.id" :class="$style.row">
        <AmpMask block tag="span">
          <img :src="member.avatarUrl" alt="" />
        </AmpMask>
        <span>{{ member.name }}</span>
        <AmpMask tag="span" :class="$style.email">{{ member.email }}</AmpMask>
      </li>
    </ul>
  </main>
</template>

<style module>
.row { display: flex; gap: 0.5rem; }
.email { color: #555; }
</style>

// -- src/App.vue, src/views/HomeView.vue --
// Your existing files. Nothing in them changes.
```

## 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 session replay to this Vue + Vite + Vue Router app by
following https://amplitude.com/build/vue-session-replay exactly. If the
app already uses @amplitude/analytics-browser, add
@amplitude/plugin-session-replay-browser to the existing init instead of
installing a second SDK, pass the same options to
sessionReplayPlugin({...}), and call amplitude.add() with it before
amplitude.init(). Otherwise use
@amplitude/unified. Call initAll once at module scope in
src/lib/amplitude.ts, reading VITE_AMPLITUDE_API_KEY from import.meta.env,
and import that module first in src/main.ts, before createApp. Never
initialize in onMounted or a component. Set sessionReplay.sampleRate to
import.meta.env.PROD ? 0.01 : 1, with debugMode only in development, and
register [data-amp-mask], [data-amp-block], and [data-amp-unmask] in
sessionReplay.privacyConfig. Create src/components/AmpMask.vue, a wrapper
that renders data-amp-mask (or data-amp-block with a block prop) around a
slot, and wrap elements that render emails, billing names, or account data
in it. Wrap images of people or documents in <AmpMask block>. Put billing
and any other sensitive route group under a parent route whose component
renders <AmpMask><RouterView /></AmpMask>, and keep any other routes
you already have. Don't put masking attributes on
child component tags, wrap teleported content separately, and never use
CSS Modules or scoped class names as masking selectors. Flag any title,
aria-label, alt, or placeholder binding that contains personal data. If
this is a Nuxt app, initialize in app/plugins/amplitude.client.ts instead.
Don't hardcode the key. Report every file you changed and any CSP you
found.
```

## What this unlocks

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

One integration; everything else reads from the same events.

## Next steps

[Web frameworksHow to add heatmaps to a Vue appTurn the replay capture you just added into click and scroll maps.](/build/vue-heatmaps)[Web frameworksHow to add product analytics to a Vue appIdentify signed-in users, so you can find a replay by the user ID from a support ticket.](/build/vue-product-analytics)[Web frameworksHow to add web analytics to a Vue appAttribute sessions to channels and campaigns, then watch replays from one campaign.](/build/vue-web-analytics)

Reference docs

[Browser Unified SDK →](https://amplitude.com/docs/sdks/analytics/browser/browser-unified-sdk)[Session Replay Browser SDK Plugin →](https://amplitude.com/docs/sdks/session-replay/session-replay-plugin)[Manage privacy settings for session replay →](https://amplitude.com/docs/session-replay/manage-privacy-settings-for-session-replay)[Configure session replay sampling →](https://amplitude.com/docs/session-replay/configure-sampling)[Debug analytics →](https://amplitude.com/docs/analytics/debug-analytics)[Amplitude MCP server →](https://amplitude.com/docs/amplitude-ai/amplitude-mcp)

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

For agents

[View raw .md](/build/vue-session-replay.md)[llms.txt](/llms.txt)

## Ship the instrumentation, then read the data

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

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