Chris Tam · Last updated 9 October 2026
Nuxt is the full-stack framework for Vue, rendering pages on the server before the browser takes over. Product analytics shows you what people do after the page loads: which pages they visit, what they use, and where they drop off.
In this tutorial, you'll add Amplitude to a Nuxt app through a client-only plugin, capture page views, identify signed-in users, and track your first custom event.
What you'll build
A Nuxt app that initializes Amplitude only in the browser, sends [Amplitude] Page Viewed on the first load and on every client-side 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 Nuxt 3.7 or later app, for example one created with
npm create nuxt@latest. - This page uses the Nuxt 4 layout that
npm create nuxt@latestscaffolds, with app code underapp/:app/plugins/,app/composables/,app/utils/,app/pages/, andapp/app.vue. On a Nuxt 3 project without anapp/folder, drop theapp/prefix.nuxt.config.tsand.envstay at the project root either way. - An Amplitude project API key. In Amplitude, go to Settings > Projects > your project > General.
- Building a Vue app with Vite and no Nuxt? Follow How to add product analytics to a Vue app instead.
No Amplitude account yet? A free account covers the tracking on this page. Sign up for free to get your project API key. Your session replay capture quota depends on your plan; compare plans.
1. Install the SDK and add the key to runtime config
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.
npm install @amplitude/unifiedDeclare the key under runtimeConfig.public, which Nuxt exposes to browser code, and set it from an environment variable rather than in source:
export default defineNuxtConfig({
runtimeConfig: {
public: {
// Filled from NUXT_PUBLIC_AMPLITUDE_API_KEY.
amplitudeApiKey: '',
},
},
});NUXT_PUBLIC_AMPLITUDE_API_KEY=your-project-api-keyThe project API key is public and its scope is limited to the minimum needed to ingest data, so sending it to the browser is expected. Keep it in the environment anyway, since a key committed to a public repository stays there.
2. Initialize in a client-only plugin
Nuxt runs plugins on the server and in the browser unless the filename says otherwise. A plugin ending in .client.ts runs only in the browser, once per page load, which is exactly where initAll belongs.
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: {
// Every session while you build; about 1% in production.
sampleRate: import.meta.dev ? 1 : 0.01,
},
});
});Why. The server render has no window and no browser storage, so the SDK has nothing to work with there. Nuxt only registers plugins inside app/plugins/ (plugins/ on Nuxt 3), so a file anywhere else never runs and analytics stays silently off. A plain app/plugins/amplitude.ts, or an initAll call in a component's setup, runs during server rendering too. Calling it from onMounted instead runs it after the first paint and again every time that component mounts. The .client.ts plugin runs once, before your pages hydrate. 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.
3. Let autocapture handle route changes
There's no code in this step. After the first server-rendered page, Nuxt navigates in the browser with Vue Router. With pageViews: true, the SDK sends [Amplitude] Page Viewed on init and again on every URL change, which covers <NuxtLink> and navigateTo(). Each event carries the page path, title, and URL.
Don't add a route middleware or a router.afterEach hook 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 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.
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]) => {
// Component setup also runs during server rendering. Skip it there.
if (typeof window === 'undefined') return;
// 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 ?? 'unknown');
amplitude.identify(identify);
},
{ immediate: true },
);
}Nuxt imports everything in app/composables/ automatically. Call it once in app/app.vue, wherever your auth state lives:
<script setup lang="ts">
const { user } = useAuth(); // your auth composable
useAmplitudeIdentity(user);
</script>
<template>
<NuxtPage />
</template>If your auth module returns a session rather than a user, such as data from @sidebase/nuxt-auth with the Auth.js provider, pass a computed ref to the user inside it: useAmplitudeIdentity(computed(() => data.value?.user)). The user object needs an id, and Auth.js sessions don't include one by default; add it in your Auth.js session callback and to your Session type first.
Reset from your sign-out handler, not from the composable:
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. app/app.vue sets up on the server and again in the browser, and the watcher's first run happens in both. The typeof window check skips the server pass. 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 handler that only runs in the browser.
<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>Event handlers only run after a click in the browser, so this is safe on a server-rendered page. Don't call track at the top of <script setup>, which also runs on the server. For events your Nuxt server routes should send, such as a payment, use the Node.js SDK on the server; the Node.js product analytics page covers identity and flushing there.
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 npm run dev, click through two or three pages, create a project, then check in this order:
- Network tab. Filter for
api2.amplitude.com. You should see aPOSTto/2/httpapireturning200within about a second of each navigation. Amplitude requests start only after the page loads in the browser; the server render sends none. - 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.
- User lookup. Go to Users > User Profiles and search your
user-prefixed test ID. The event stream shows[Amplitude] Page Viewedfor each page andProject Createdwith itstemplateproperty within ten seconds to a minute, and the profile showsplan.
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. The variable must be named NUXT_PUBLIC_AMPLITUDE_API_KEY to fill runtimeConfig.public.amplitudeApiKey, and the dev server only reads .env at startup, so restart it. In production, set NUXT_PUBLIC_AMPLITUDE_API_KEY in the server's environment; the built server doesn't read .env. For nuxt generate, set it before generating.
"window is not defined" during server rendering
Something calls the SDK on the server. Keep initAll in app/plugins/amplitude.client.ts, keep the typeof window check in the identity composable, and only call track from event handlers.
Two page view events per navigation
A route middleware, a router.afterEach hook, or a page view composable is running alongside autocapture. Delete it.
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 in the browser.
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.
// -- nuxt.config.ts --
export default defineNuxtConfig({
runtimeConfig: {
public: {
amplitudeApiKey: '',
},
},
});
// -- .env --
NUXT_PUBLIC_AMPLITUDE_API_KEY=your-project-api-key
// -- app/plugins/amplitude.client.ts --
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,
},
});
});
// -- app/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 (typeof window === 'undefined') return;
if (!id) return;
// Amplitude drops IDs shorter than 5 characters.
amplitude.setUserId(`user-${id}`);
const identify = new amplitude.Identify();
identify.set('plan', plan ?? 'unknown');
amplitude.identify(identify);
},
{ immediate: true },
);
}
// -- app/utils/signOut.ts --
import * as amplitude from '@amplitude/unified';
export async function signOut() {
// ... your sign-out logic
amplitude.reset();
}
// -- app/app.vue --
<script setup lang="ts">
const { user } = useAuth(); // your auth composable
useAmplitudeIdentity(user);
</script>
<template>
<NuxtPage />
</template>
// -- app/pages/projects/new.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.
Add Amplitude analytics to this Nuxt app by following
https://amplitude.com/build/nuxt-product-analytics exactly. Install
@amplitude/unified. Add amplitudeApiKey under runtimeConfig.public in
nuxt.config.ts, filled from NUXT_PUBLIC_AMPLITUDE_API_KEY. Call initAll once
in app/plugins/amplitude.client.ts (plugins/ on Nuxt 3), never in a universal
plugin, component setup, or onMounted, with autocapture elementInteractions
on. Put composables, utils, and pages under app/ on Nuxt 4 and at the root
on Nuxt 3. Don't add a route middleware or router.afterEach page view hook;
autocapture tracks navigations. Set the user ID with a "user-" prefix in a
composable that watches the auth user and skips the server pass, and call
reset() only from the sign-out handler. Only call track from event
handlers. 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, already on from step 2, lets you watch the session where
Project Createdfired withtemplate: kanban. - Heatmaps show where people click on
/projects/newbefore they create one, built from those same session replays. - Feature Experiment, bundled in the same package, can test a new template picker with
Project Createdas the metric. - Guides and surveys, also bundled, can prompt users who viewed
/projects/newbut never firedProject Created.
One integration; everything else reads from the same events.
Next steps
Reference docs
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.