---
title: "How to add product analytics to a Vue app"
description: "Install Amplitude in a Vue and Vite app, capture page views on every Vue Router navigation, identify signed-in users, and confirm events land."
canonical: "https://amplitude.com/en-us/build/vue-product-analytics"
---

# How to add product analytics to a Vue app

Install Amplitude in a Vue and Vite app, capture page views on every Vue Router navigation, identify signed-in users, and confirm events land.

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

---

# How to add product analytics to a Vue app

Install the Amplitude Unified SDK in a Vue app built with Vite, capture page views on every Vue Router navigation, identify signed-in users, and track one custom event. About 10 minutes, all copy-paste.

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

Chris Tam · Last updated 9 October 2026

Vue is a progressive framework for building interfaces, usually paired with Vite and Vue Router. [Product analytics](https://amplitude.com/amplitude-analytics) shows you what people do inside your Vue app: which routes they visit, what they use, and where they drop off.

In this tutorial, you'll add Amplitude to a Vue and Vite app, capture page views, identify signed-in users, and track your first custom event.

## What you'll build

A Vue app that sends `[Amplitude] Page Viewed` on the first load and on every Vue 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. By the end you'll see live events in Amplitude's event stream.

## Prerequisites

- A Vue 3 app with Vue Router, for example one created with `npm create vue@latest`.
- Using Nuxt? It renders on the server first, so initialization works differently. Follow [How to add product analytics to a Nuxt app](/build/nuxt-product-analytics) instead.
- 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

[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 init call.

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
```

## 2. Initialize before the app mounts

Create one module that initializes the SDK at import time, then import it first in your entry file, before `createApp`.

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,
        sessions: true,
        formInteractions: true,
        fileDownloads: true,
        elementInteractions: true,
      },
    sessionReplay: {
      // Every session while you build; about 1% in production.
      sampleRate: import.meta.env.PROD ? 0.01 : 1,
    },
  });
} else {
  console.warn('VITE_AMPLITUDE_API_KEY is not set; analytics is off.');
}

export { amplitude };
```

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.** An ES module body runs once per page load no matter how many components import it, so `initAll` runs exactly once and before the first route renders. Putting it in a component's `onMounted` runs it after the first paint, and again every time that component mounts. 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 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).

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. The sign-out and custom event code from steps 4 and 5 carries over. Use that page's identity composable, which skips the server render.

## 3. Let autocapture handle route changes

There's no code in this step. With `pageViews: true`, the SDK sends `[Amplitude] Page Viewed` on init and again on every URL change inside the single-page app, which covers `router.push()` and `<RouterLink>` navigation. Each event carries the page path, title, and URL.

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

If your router uses hash history (`createWebHashHistory`), 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 a Vue app](/build/vue-web-analytics) covers history modes 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.

src/composables/useAmplitudeIdentity.ts

```typescript
import { watch, type Ref } from 'vue';
import * as amplitude from '@amplitude/unified';

type User = { id: string; plan?: string } | null | undefined;

export function useAmplitudeIdentity(user: Ref<User>) {
  watch(
    () => [user.value?.id, user.value?.plan] as const,
    ([id, plan]) => {
      // 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 backend sends as user_id.
      amplitude.setUserId(`user-${id}`);

      const identify = new amplitude.Identify();
      identify.set('plan', plan ?? 'unknown');
      amplitude.identify(identify);
    },
    { immediate: true },
  );
}
```

Call it once, wherever your auth state lives:

src/App.vue

```html
<script setup lang="ts">
import { useAuth } from './auth/useAuth'; // your auth composable
import { useAmplitudeIdentity } from './composables/useAmplitudeIdentity';

const { user } = useAuth();
useAmplitudeIdentity(user);
</script>

<template>
  <RouterView />
</template>
```

Reset from your sign-out handler, not from the composable:

src/auth/signOut.ts

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

export async function signOut() {
  // ... your sign-out logic
  // Clears the user ID and issues a new device ID for the next person.
  amplitude.reset();
}
```

**Why.** A `null` user also describes every anonymous visitor on every page load, and `reset()` issues a new device ID each time it runs. Resetting there splits one visitor into many and breaks anything bucketed by device ID, such as feature flags and Web Experiment. The watcher reads the ID and plan rather than the whole `user` object, so a fresh object with the same values doesn't identify again. 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, with properties you'll want to break down by.

src/views/NewProject.vue

```html
<script setup lang="ts">
import { ref } from 'vue';
import * as amplitude from '@amplitude/unified';

const template = ref('blank');

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

  amplitude.track('Project Created', { template: template.value });
}
</script>

<template>
  <select v-model="template">
    <option value="blank">Blank</option>
    <option value="kanban">Kanban</option>
  </select>
  <button @click="handleCreate">Create project</button>
</template>
```

Call `track` from the event handler, not from a `watch` on the form state. A handler runs once per click; a watcher runs whenever its source changes.

**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](https://amplitude.com/docs/amplitude-ai/amplitude-mcp).

## Verify it worked

Run `npm run dev`, click through two or three routes, 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.
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 route and `Project Created` with its `template` property within ten seconds to a minute, and the profile shows `plan`.

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 undefined, so the warning from step 2 is in the console. The env var must start with `VITE_`, and Vite only reads `.env.local` at startup, so restart the dev server.

### Two page view events per navigation

A `router.afterEach` hook or a page view composable is running alongside autocapture. Delete it. If both events are `[Amplitude] Page Viewed`, check that `initAll` is only called from `src/lib/amplitude.ts`.

### Signed-in events still show as anonymous

The composable never sees the user. Pass it the reactive `user` ref from your auth composable, not `user.value`, so the watcher reruns when auth resolves. If your auth state is a Pinia store, use `const { user } = storeToRefs(useAuthStore())`; destructuring the store directly gives a plain value that never updates.

### 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.

## Full code reference

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

7 files, 102 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,
    },
  });
} 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/composables/useAmplitudeIdentity.ts --
import { watch, type Ref } from 'vue';
import * as amplitude from '@amplitude/unified';

type User = { id: string; plan?: string } | null | undefined;

export function useAmplitudeIdentity(user: Ref<User>) {
  watch(
    () => [user.value?.id, user.value?.plan] as const,
    ([id, plan]) => {
      if (!id) return;
      // Amplitude drops IDs shorter than 5 characters. Use the same
      // string your backend sends as user_id.
      amplitude.setUserId(`user-${id}`);
      const identify = new amplitude.Identify();
      identify.set('plan', plan ?? 'unknown');
      amplitude.identify(identify);
    },
    { immediate: true },
  );
}

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

export async function signOut() {
  // ... your sign-out logic
  amplitude.reset();
}

// -- src/App.vue --
<script setup lang="ts">
import { useAuth } from './auth/useAuth';
import { useAmplitudeIdentity } from './composables/useAmplitudeIdentity';

const { user } = useAuth();
useAmplitudeIdentity(user);
</script>

<template>
  <RouterView />
</template>

// -- src/views/NewProject.vue --
<script setup lang="ts">
import { ref } from 'vue';
import * as amplitude from '@amplitude/unified';

const template = ref('blank');

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

  amplitude.track('Project Created', { template: template.value });
}
</script>

<template>
  <select v-model="template">
    <option value="blank">Blank</option>
    <option value="kanban">Kanban</option>
  </select>
  <button @click="handleCreate">Create project</button>
</template>
```

## 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 Vue and Vite app by following
https://amplitude.com/build/vue-product-analytics exactly. Install
@amplitude/unified and call initAll once at module scope in
src/lib/amplitude.ts, reading VITE_AMPLITUDE_API_KEY from import.meta.env,
with autocapture elementInteractions on. Import that module first in
src/main.ts, before createApp. Don't initialize in onMounted. Don't add a
router.afterEach page view hook; autocapture tracks route changes. Set the
user ID with a "user-" prefix in a composable that watches the auth user,
and call reset() only from the sign-out handler. Tell me session replay
records every session in development and about 1% in production, and how to
turn it off. Don't hardcode the key and don't invent event names. 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 one, 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 web analytics to a Vue appKeep campaign attribution through Vue Router redirects and read traffic in the web analytics dashboards.](/build/vue-web-analytics)[Web frameworksHow to add session replay to a Vue appTune the sample rate and mask sensitive components before you record real users.](/build/vue-session-replay)[Web frameworksHow to add feature flags and experiments to a Vue appRead a flag in a composable and use \`Project Created\` as the experiment's metric.](/build/vue-feature-flags)

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/vue-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)
