---
title: "How to add Web Experiment to a Vue app"
description: "Load Amplitude Web Experiment in a Vue and Vite app without flicker, make Vue elements easy to target, and confirm impressions land."
canonical: "https://amplitude.com/en-us/build/vue-web-experiment"
---

# How to add Web Experiment to a Vue app

Load Amplitude Web Experiment in a Vue and Vite app without flicker, make Vue elements easy to target, and confirm impressions land.

Source: https://amplitude.com/en-us/build/vue-web-experiment

---

# How to add Web Experiment to a Vue app

Load the Amplitude Web Experiment script in your Vite \`index.html\` so it runs before Vue mounts, give the elements you want to test stable selectors, and confirm \`\[Experiment] Impression\` events land. About 15 minutes, all copy-paste.

- Vue 3
- Vite
- Vue Router
- Web Experiment script
- 15 min

Chris Tam · Last updated 9 October 2026

A Vue app built with Vite renders its whole interface in the browser. [Web Experiment](https://amplitude.com/web-experimentation) lets you A/B test the headlines, buttons, and layouts in that interface from a visual editor, so a test doesn't need a code change and a deploy.

In this tutorial, you'll load the Web Experiment script in `index.html`, make Vue elements easy to target, and launch your first experiment. It builds on [Vue product analytics](/build/vue-product-analytics).

## What you'll build

A Vue and Vue Router app that loads `API_KEY.experiment.js` synchronously at the top of `index.html`, applies a variant to the home page hero on first load and after client-side navigation, and sends `[Experiment] Impression` with `[Experiment] Flag Key` and `[Experiment] Variant` through the analytics SDK you already run. `Project Created` from the product analytics page becomes the experiment's goal. By the end you'll see an impression in your own user's event stream in Amplitude.

## Prerequisites

- A Vue 3 app built with Vite, TypeScript, and Vue Router, with Amplitude analytics already initialized in `src/lib/amplitude.ts`. If you don't have that yet, start with [How to add product analytics to a Vue app](/build/vue-product-analytics).
- Your Amplitude project API key. In Amplitude, go to Settings > Projects > your project > General. Web Experiment uses the same project key as your analytics.
- Your app running at a URL you can open in Chrome, since the Visual Editor loads your real page.

No Amplitude account yet? A free account is enough for this tutorial; the docs list A/B testing and the Visual Editor on all plans, and this page doesn't use custom code actions, which need Growth or Enterprise. [Sign up for free](https://app.amplitude.com/signup) to get your project API key.

## 1. Pick the standalone experiment script

Web Experiment ships as a script tag, not an npm package. The [Browser Unified SDK docs](https://amplitude.com/docs/sdks/analytics/browser/browser-unified-sdk) are explicit: `@amplitude/unified` from npm includes Feature Experiment, the code-based flags product, and doesn't include Web Experiment. Installing more packages won't give you the Visual Editor.

Two script tags contain Web Experiment, and on a Vue app with an npm-installed SDK only one is right. [Implement Web Experiment](https://amplitude.com/docs/web-experiment/implementation) documents the first, and the Unified SDK docs document the second:

- `https://cdn.amplitude.com/script/API_KEY.experiment.js` loads Web Experiment only. Use it when you already initialize analytics from npm, as the Vue product analytics page does.
- `https://cdn.amplitude.com/script/API_KEY.js`, the Unified Script, loads analytics, session replay, and Web Experiment together. Use it only on a site with no Amplitude SDK installed.

Use `.experiment.js`. The docs say it tracks impressions through the Browser SDK already on your site, so it needs no analytics of its own. Adding the Unified Script next to `@amplitude/unified` puts a second analytics SDK on the page.

The script URL takes your project API key, which you already have in `.env.local`. The key is public and its scope is limited to the minimum needed to ingest data, so it's expected in your HTML; keep it in the env file anyway.

.env.local

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

## 2. Load it first in index.html

In a Vite project, `index.html` sits at the project root, next to `vite.config.ts`, and it's the only HTML the browser receives before your JavaScript runs. That's where the script goes. Put it at the top of `<head>`, right after `<meta charset>` and before any stylesheet or module script. Vite (4.2 or later) replaces `%VITE_AMPLITUDE_API_KEY%` in `index.html` with the value from your env file when it serves or builds the page.

index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <!-- Amplitude Web Experiment. Keep it first after the charset, and synchronous.
         EU projects: https://cdn.eu.amplitude.com/script/ -->
    <script src="https://cdn.amplitude.com/script/%VITE_AMPLITUDE_API_KEY%.experiment.js"></script>
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My app</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>
```

**Why.** The docs ask for the script "as high as possible" in `<head>` and call the synchronous tag the best experience. Don't move it into a component's `onMounted`, a Vue plugin, or `src/main.ts`. Each of those runs after the browser has parsed the page and started your bundle, so visitors see the original content before the variant replaces it. Don't add `async` or `defer` to the tag either.

The cost is documented: the base script is about 20 KB compressed plus roughly 100 bytes per experiment, and the browser serves it from cache for 60 seconds across page loads. Archive finished experiments so it doesn't keep growing.

### If you can't ship a blocking script

If your performance budget forbids synchronous third-party scripts, the docs offer an async loader with an anti-flicker mask. It hides the page with a style rule, loads the script asynchronously, and clears the mask when Amplitude applies changes or when the timeout expires. Use it in place of the `<script src>` above, in the same spot:

index.html (async alternative, inside \<head>, first after the charset)

```html
<script>
  (function (d, h) {
    var apiKey = "%VITE_AMPLITUDE_API_KEY%";
    // Milliseconds before the mask clears even if the script is slow.
    var timeout = 1000;
    var id = "amp-exp-css";
    try {
      if (!d.getElementById(id)) {
        var st = d.createElement("style");
        st.id = id;
        st.innerText =
          "* { visibility: hidden !important; background-image: none !important; }";
        h.appendChild(st);
        window.setTimeout(function () {
          st.remove();
        }, timeout);
        var sc = d.createElement("script");
        sc.src =
          "https://cdn.amplitude.com/script/" + apiKey + ".experiment.js";
        sc.async = true;
        sc.onerror = function () {
          st.remove();
        };
        h.insertBefore(sc, d.currentScript || h.lastChild);
      }
    } catch (e) {
      console.error(e);
    }
  })(document, document.head);
</script>
```

The trade is visible: every visitor sees a blank page for up to `timeout` milliseconds instead of a flash of the original. The synchronous tag is the better default. Don't load Web Experiment through Google Tag Manager in production; the docs warn that tag managers load it asynchronously and cause flicker.

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 Web Experiment script doesn't go in that plugin: add it through `app.head` in `nuxt.config.ts` so it's in the server-rendered `<head>`. Nuxt reads that config when it builds, so the key comes from `process.env`; set `NUXT_PUBLIC_AMPLITUDE_API_KEY` in your build environment.

nuxt.config.ts

```typescript
export default defineNuxtConfig({
  app: {
    head: {
      script: [
        {
          src: `https://cdn.amplitude.com/script/${process.env.NUXT_PUBLIC_AMPLITUDE_API_KEY}.experiment.js`,
          tagPriority: 'critical',
        },
      ],
    },
});
```

## 3. Keep analytics in the npm SDK, and keep the device ID stable

The experiment script sends impressions through the analytics SDK you already initialize, so there's no second `initAll` and no second key. `src/lib/amplitude.ts` from the Vue product analytics page stays as it is, and `import './lib/amplitude';` stays the first line of `src/main.ts`:

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');
```

That page's config records every session in development and about 1% in production with session replay. Set `sessionReplay.sampleRate` to `0` if you don't want replays. On Nuxt, the same applies to `app/plugins/amplitude.client.ts`; the script itself loads from `nuxt.config.ts`, as shown at the end of step 2.

Check one thing that matters more for experiments than for analytics. The docs say most web experiments bucket by device ID, and `reset()` issues a new device ID. The base page's `useAmplitudeIdentity` composable returns early when there's no user and only `signOut()` calls `reset()`. Keep it that way. If you've added a `reset()` call anywhere that runs for signed-out visitors, every anonymous visit gets a fresh device ID and a fresh roll of the dice.

src/composables/useAmplitudeIdentity.ts

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

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

export function useAmplitudeIdentity(user: Ref<User>) {
  watch(
    () => [user.value?.id, user.value?.plan] as const,
    ([id, plan]) => {
      // Anonymous or still loading: keep the same device ID.
      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

```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();
}
```

### Optional: event-triggered pages and behavioral targeting

If an experiment uses the **On Event Tracked** page trigger or real-time behavioral targeting, the experiment client needs to hear your analytics events. The docs say that when analytics and Web Experiment don't load together through the Unified Script, you register the experiment plugin on the analytics instance with `window.amplitude.add(window.webExperiment.plugin())`. With the npm SDK, make the same call on your imported instance, right after `initAll`. This is the complete updated `src/lib/amplitude.ts`:

src/lib/amplitude.ts

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

type WebExperimentGlobal = {
  plugin: () => Parameters<typeof amplitude.add>[0];
};

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: {
      // Keep debugMode and privacyConfig here if you added them on /build/vue-session-replay.
      // Every session while you build; about 1% in production.
      sampleRate: import.meta.env.PROD ? 0.01 : 1,
    },
  });

  // Lets On Event Tracked triggers and behavioral targeting see your events.
  const webExperiment = (window as Window & { webExperiment?: WebExperimentGlobal })
    .webExperiment;
  if (webExperiment) {
    amplitude.add(webExperiment.plugin());
  }
} else {
  console.warn('VITE_AMPLITUDE_API_KEY is not set; analytics is off.');
}

export { amplitude };
```

The synchronous tag from step 2 has already run by the time this module executes, so `window.webExperiment` exists. With the async loader it may not have loaded yet, and the `if` skips the plugin without warning. Skip this block entirely if your experiments use the default **Immediately** trigger.

## 4. Make Vue elements easy to target

The Visual Editor records each change against a CSS selector, and the script applies it to whatever matches that selector in the live DOM. In a Vue app, your components build that DOM, so three things decide whether a variant sticks.

Give anything you plan to test a stable attribute. The docs warn that the editor picks the shortest unique path, which can include generated names that change between builds. Vue produces two of those: the `data-v-` attribute that `<style scoped>` adds to every element in a component, and the hashed class names from `<style module>`. A `data-` attribute you write yourself doesn't change:

src/views/HomeView\.vue

```html
<script setup lang="ts">
import { RouterLink } from 'vue-router';
</script>

<template>
  <section data-exp="home-hero">
    <h1 data-exp="home-headline">Plan your next project in minutes</h1>
    <p data-exp="home-subhead">Boards, roadmaps, and templates in one place.</p>
    <RouterLink to="/projects/new" data-exp="home-cta">Create a project</RouterLink>
  </section>
</template>
```

In the editor's Selector field, replace the generated path with `[data-exp="home-headline"]`. `RouterLink` passes the attribute through to the `<a>` it renders. On your own components, a `data-exp` attribute lands on the root element only when the component has a single root and doesn't set `inheritAttrs: false`, so put it on a plain element inside the template when in doubt.

Test content Vue doesn't rebuild. Element changes edit an element's text or append styles to it. A change lives on that DOM node, so anything that replaces the node drops it: a `v-if` that flips, a `:key` that changes, a parent that re-mounts, or text that arrives late from a fetch or a translation file. The docs' own check is to confirm JavaScript doesn't reset your changes after they apply. Static copy written into the template, like the headline above, is the safe target. For text driven by reactive state, use a [feature flag](/build/vue-feature-flags) instead.

Wait for elements that render late. The script evaluates on the first page load and every time history state is pushed or popped, which covers `router.push()` and `<RouterLink>` navigation. On each change it reverts what it applied, then re-evaluates for the new URL. A lazy route (`component: () => import(...)`), a `<Transition mode="out-in">` around `<RouterView>`, or a view that waits on data can mount its elements after that moment. If a change doesn't show on a route like that, set the Page's trigger to **When Element Appears** with the element's selector. Page triggers are in Early Access.

**Why.** A server-rendered app sends the hero in its first HTML; a Vite app sends an empty `<div id="app">` and builds the hero in JavaScript. Every Web Experiment problem specific to Vue comes back to that difference: selectors must survive rebuilds, changes must survive re-renders, and evaluation must find elements that didn't exist when the page loaded.

If your router uses `createWebHashHistory`, your routes live after the `#`. The **URL Matches** targeting rule ignores hash fragments, so it can't tell `/#/pricing` from `/#/`. Target those Pages with **URL Contains** and the hash path, for example `#/pricing`.

## 5. Open your headers to the script and editor

Skip this step if your app doesn't send a Content Security Policy or a Cross-Origin-Opener-Policy. Vite's build output is static files and sets neither, so if you have them, they come from your server or host configuration. Per the docs, two changes are required.

Add `*.amplitude.com` and `'unsafe-inline'` to your existing `script-src` directive so the script and the Visual Editor can load. Remove `Cross-Origin-Opener-Policy`, or set it to `unsafe-none`, so the Visual Editor can open your site.

Response headers (merge into the policy you already send)

```text
Content-Security-Policy: script-src 'self' *.amplitude.com 'unsafe-inline'
Cross-Origin-Opener-Policy: unsafe-none
```

The `script-src` above is a minimum, not a full policy. If you load the async snippet from step 2, `'unsafe-inline'` also covers that inline script. The editor's Mobile Simulator loads your site in an iframe, so `X-Frame-Options: DENY` or `frame-ancestors 'none'` blocks it.

## 6. Create the experiment in the Visual Editor

Run `npm run dev` or deploy, then create the experiment in Amplitude, following [Set up a web experiment](https://amplitude.com/docs/web-experiment/set-up-a-web-experiment):

1. In Amplitude Experiment, go to Experiments > Create Experiment > Web Experiment.
2. Name the experiment, enter the URL of your home page, and select your project. Amplitude uses the URL to create your first Page.
3. When the Visual Editor opens your page, click **Element Changes**, click the headline, and edit its text. Open the Selector and set it to `[data-exp="home-headline"]`. Changes apply to the current variant as you go.
4. Click **Continue**. Leave the Page targeting on your home page URL and the audience on all users.
5. Define your goals. Choose `Project Created`, the event the product analytics page tracks, as the primary metric.
6. Click **Test & Preview** and open the preview link to check the treatment, then start the experiment.

If Amplitude opens a Site Setup panel instead of the editor, it didn't find the script on that URL, or an ad blocker in your browser stopped it. After you save changes, allow up to 60 seconds for caches to refresh before you check the live page.

**MCP.** Working in Cursor or Claude Code? Amplitude's MCP server lets your agent read your project's existing event taxonomy, so when you pick a goal it points at `Project Created` instead of inventing `project_created`. [Set up the MCP server](https://amplitude.com/docs/amplitude-ai/amplitude-mcp).

## Verify it worked

Open your home page in a normal Chrome window with ad blockers off, click a `<RouterLink>` to another route and back, then check in this order:

1. **Network tab.** Filter for `amplitude.com`. You should see `API_KEY.experiment.js` from `cdn.amplitude.com` return `200` with your real key in the URL, not the literal `%VITE_AMPLITUDE_API_KEY%`, and loaded before `main.ts`. Then look for the analytics SDK's `POST` to `api2.amplitude.com/2/httpapi`. If you're in the experiment, the Instrumentation Explorer Chrome extension shows an event with `event_type` `$impression`, the form the script sends before Amplitude transforms it.
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 or your device ID. Within ten seconds to a minute the event stream shows `[Experiment] Impression`, with `[Experiment] Flag Key` naming your experiment and `[Experiment] Variant` naming the variant you saw.

No impression and no variant? You may be in control or outside the rollout. A private window gets a new device ID and a new roll. The [Amplitude Chrome extension](https://amplitude.com/docs/data/chrome-extension-debug) shows page and audience targeting and exposure status for each web experiment on the page, and **Preview Variant** lets you force a variant for your own session.

## Troubleshooting

### The original headline flashes before the variant

The script is loading late. It must be a plain `<script src>` at the top of `<head>` in `index.html`, right after `<meta charset>`, not added from a component, a Vue plugin, `main.ts`, or a tag manager, and without `async` or `defer`.

### The script request 404s with `%VITE_AMPLITUDE_API_KEY%` in the URL

Vite left the `%...%` string in place because the variable isn't set, or because the project runs a Vite version older than 4.2. Check the `VITE_` prefix and restart the dev server, since Vite reads `.env.local` at startup. For production builds, set the variable in your build environment and rebuild; the key is written into `index.html` at build time.

### The variant applies on the home page but not on a route you navigate to

The element mounts after the route change, behind a lazy route, a `<Transition>`, or a data fetch. First confirm the selector matches the rendered element: run `document.querySelector('[data-exp="home-headline"]')` in the DevTools console. If it does, switch that Page's trigger to **When Element Appears** and select the element by its `data-exp` attribute.

### The variant shows, then the original comes back

Vue replaced the element. A `v-if`, a changed `:key`, or a re-mounted parent creates a new node without the change. Pick an element that renders once from static template content, or move the test to a feature flag.

### The variant worked yesterday and stopped after a deploy

The selector included a `data-v-` scoped style attribute or a `<style module>` class name that changed in the new build. Edit the change in the Visual Editor and set its selector to the `data-exp` attribute.

### Anonymous visitors see a different variant on every visit

Something calls `amplitude.reset()` while the user is signed out, which issues a new device ID. Reset only in `signOut()`, and keep the early return in `useAmplitudeIdentity`.

### Page views or sessions doubled after adding the script

You loaded the Unified Script, `API_KEY.js`, next to the npm SDK. Replace it with `API_KEY.experiment.js`.

## Full code reference

Every file from this tutorial in one block, the version to hand to an agent. It uses the synchronous tag; swap in the async alternative from step 2 only if you must. For the optional `webExperiment.plugin()` registration, use the `src/lib/amplitude.ts` from step 3 instead. 7 files.

8 files, 101 lines

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

// -- index.html --
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <!-- Amplitude Web Experiment. Keep it first after the charset, and synchronous.
         EU projects: https://cdn.eu.amplitude.com/script/ -->
    <script src="https://cdn.amplitude.com/script/%VITE_AMPLITUDE_API_KEY%.experiment.js"></script>
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My app</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

// -- 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: {
      // Keep debugMode and privacyConfig here if you added them on /build/vue-session-replay.
      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;

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.
      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/views/HomeView.vue --
<script setup lang="ts">
import { RouterLink } from 'vue-router';
</script>

<template>
  <section data-exp="home-hero">
    <h1 data-exp="home-headline">Plan your next project in minutes</h1>
    <p data-exp="home-subhead">Boards, roadmaps, and templates in one place.</p>
    <RouterLink to="/projects/new" data-exp="home-cta">Create a project</RouterLink>
  </section>
</template>

// -- src/App.vue, src/router/index.ts, src/views/NewProject.vue --
// Unchanged from /build/vue-product-analytics.
```

## 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 Web Experiment to this Vue + Vite + Vue Router app by
following https://amplitude.com/build/vue-web-experiment exactly. Add
<script src="https://cdn.amplitude.com/script/%VITE_AMPLITUDE_API_KEY%.experiment.js"></script>
as the first tag after <meta charset> inside <head> in the root
index.html, synchronous, with no async or defer. Do not load it from a component, onMounted, a Vue
plugin, main.ts, or a tag manager. Do not add the Unified Script
(API_KEY.js) and do not install another npm package for Web Experiment;
@amplitude/unified doesn't include it. Keep analytics in the existing
src/lib/amplitude.ts and don't call initAll a second time. Make sure no
amplitude.reset() call runs when the user is null; call reset() only in
the sign-out handler. Add data-exp attributes to the elements I name for
testing, on plain elements or RouterLink, never relying on data-v- or
CSS module class names. If the app sends Content-Security-Policy or
Cross-Origin-Opener-Policy headers, add *.amplitude.com and
'unsafe-inline' to script-src and set COOP to unsafe-none. Don't hardcode
the key. Report every file you changed.
```

## What this unlocks

- [Session replay](https://amplitude.com/session-replay), on at the sample rate from the product analytics page, shows sessions where visitors saw the treatment variant and `Project Created` never fired, so you can watch why.
- [Heatmaps](https://amplitude.com/heatmaps) are built from session replays, so they compare where people click on the home page hero in each variant.
- [Feature Experiment](https://amplitude.com/amplitude-experiment), already in `@amplitude/unified`, puts the winning headline in `HomeView.vue` behind a flag when you want a gradual rollout, since the docs recommend moving a winner into code rather than leaving the web experiment at 100%.
- [Guides and surveys](https://amplitude.com/guides-and-surveys) can ask visitors who saw the treatment what they expected after clicking `home-cta`.

One integration; everything else reads from the same events.

## Next steps

[Web frameworksHow to add feature flags and experiments to a Vue appTest changes that depend on reactive state or data, where a visual edit would be lost on re-render.](/build/vue-feature-flags)[Web frameworksHow to add session replay to a Vue appWatch the sessions behind each variant's numbers.](/build/vue-session-replay)[Web frameworksHow to add heatmaps to a Vue appCompare where people click on the hero in control and treatment.](/build/vue-heatmaps)

Reference docs

[Implement Web Experiment →](https://amplitude.com/docs/web-experiment/implementation)[Set up a web experiment →](https://amplitude.com/docs/web-experiment/set-up-a-web-experiment)[Web Experiment Pages and triggers →](https://amplitude.com/docs/web-experiment/pages)[Web Experiment actions →](https://amplitude.com/docs/web-experiment/actions)[Web Experiment event tracking →](https://amplitude.com/docs/web-experiment/tracking)[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-web-experiment.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)
