---
title: "How to add guides and surveys to a React app"
description: "Show Amplitude guides and surveys in a Vite and React Router app: one init, router-aware guide links, stable pin targets, and a survey button."
canonical: "https://amplitude.com/en-us/build/react-guides-surveys"
---

# How to add guides and surveys to a React app

Show Amplitude guides and surveys in a Vite and React Router app: one init, router-aware guide links, stable pin targets, and a survey button.

Source: https://amplitude.com/en-us/build/react-guides-surveys

---

# How to add guides and surveys to a React app

Turn on guides and surveys in a Vite and React Router app, send guide links and buttons through your router, anchor a pin to a React component, and open a survey from your own button. About 20 minutes, all copy-paste.

- React 18+
- Vite
- React Router
- Unified SDK 1.x
- 20 min

Chris Tam · Last updated 7 October 2026

React apps change screens without reloading the page. [Guides and surveys](https://amplitude.com/guides-and-surveys) walk users through those screens and ask for feedback in context, and you edit them in Amplitude instead of shipping a deploy.

In this tutorial, you'll turn on guides and surveys in a Vite app, route guide links through React Router, anchor a pin to a component, and open a survey from a button. It builds on the [React product analytics page](/build/react-product-analytics).

## What you'll build

A Vite and React Router app where guides and surveys boot once with the same user and device ID as your analytics, guide links marked **Use router** navigate with `useNavigate`, a guide button runs an `open_new_project` callback in your code, a pin anchors to `[data-guide="new-project-button"]`, and a "Give feedback" button opens the survey with the key `product-feedback`. By the end you'll see the SDK report a booted user, and `Guide Viewed` and `Survey Submitted` in your own user's event stream.

## Prerequisites

- A Vite project with React 18 or later, TypeScript, and React Router's `BrowserRouter`, with Amplitude analytics already initialized in `src/lib/amplitude.ts`. If you don't have that yet, start with [Add product analytics to a React app](/build/react-product-analytics).
- Your Amplitude project API key. In Amplitude, go to Settings > Projects > your project > General. Guides and surveys must use the same key as your analytics.
- A way to know who's signed in on the client, such as the `useAuth` hook from the product analytics page.

No Amplitude account yet? A free account is enough for this tutorial; the docs list the standard triggers it uses on all plans. [Sign up for free](https://app.amplitude.com/signup) to get your project API key.

## 1. Install the SDK and turn guides and surveys on

[The Quickstart](https://amplitude.com/docs/get-started/amplitude-quickstart) recommends `@amplitude/unified` for browser apps installed from npm, and the [Browser Unified SDK](https://amplitude.com/docs/sdks/analytics/browser/browser-unified-sdk) includes guides and surveys and initializes them by default. If you followed the product analytics page, it's already installed.

Terminal

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

.env.local

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

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.

Then turn the product on in Amplitude. The [Guides and Surveys Web SDK docs](https://amplitude.com/docs/sdks/guides-and-surveys/sdk) say to enable guides and surveys in your project settings before anything can display, and the project-level apps setting has to include web so it appears as a target when you create a guide or survey. Open Project Settings > Guides and Surveys and turn on both.

### If your app sends security headers

Skip this if your server or host doesn't send a `Content-Security-Policy` or `Cross-Origin-Opener-Policy` header; Vite sets neither. If yours does, the docs require two changes:

Response headers (merge into the policy you already send)

```text
Content-Security-Policy: script-src 'self' https://*.amplitude.com; connect-src 'self' https://*.amplitude.com; img-src 'self' https://*.amplitude.com; media-src 'self' https://*.amplitude.com; style-src 'self' https://*.amplitude.com
Cross-Origin-Opener-Policy: same-origin-allow-popups
```

`Cross-Origin-Opener-Policy: same-origin` blocks the messages Preview mode passes between Amplitude and your page, so previews never open; `same-origin-allow-popups` keeps them working. If your `style-src` blocks inline styles, pass your CSP nonce as `engagement: { nonce }` in the `initAll` options in step 2.

## 2. Initialize once, before React renders

Keep initialization in the module the product analytics page created, and import it first in `src/main.tsx`. The only change is a promise that resolves when `initAll` has finished, so components that talk to guides and surveys can wait for it.

src/lib/amplitude.ts

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

const apiKey = import.meta.env.VITE_AMPLITUDE_API_KEY;

let ready: Promise<void> = Promise.resolve();

if (apiKey) {
  // initAll starts analytics, session replay, Feature Experiment, and
  // guides and surveys. Guides and surveys boot with the analytics user.
  ready = Promise.resolve(
    amplitude.initAll(apiKey, {
      analytics: {
        autocapture: {
          attribution: true,
          pageViews: true,
          sessions: true,
          formInteractions: true,
          fileDownloads: true,
          elementInteractions: true,
        },
      sessionReplay: {
        sampleRate: 1,
      },
    }),
  ).then(() => undefined);
} else {
  console.warn('VITE_AMPLITUDE_API_KEY is not set; analytics is off.');
}

/** Resolves once initAll, including guides and surveys, has finished. */
export function amplitudeReady() {
  return ready;
}

export { amplitude };
```

src/main.tsx

```typescript
import './lib/amplitude';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { App } from './App';

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </StrictMode>,
);
```

**Why.** With the Unified SDK, guides and surveys initialize alongside analytics and boot with the same user and session. Don't call `engagement.init()` or `engagement.boot()` yourself, and don't call them from a `useEffect`. The docs say calling `boot` more than once causes unexpected behavior, especially for guides and surveys set to appear immediately, and Strict Mode runs every effect twice in development. Module code runs once per page load, before `createRoot`, so it can't double up.

`sessionReplay.sampleRate: 1` records every session, as on the product analytics page; set it to `0` if you don't want replays. Don't pass `engagement: { skip: true }`, which stops guides and surveys from loading at all.

`window.engagement` isn't in TypeScript's `Window` type. Declare the methods this page calls:

src/types/engagement.d.ts

```typescript
export {};

declare global {
  interface Window {
    engagement?: {
      setRouter: (routerFn: (url: string) => void) => void;
      addCallback: (name: string, callback: () => void) => void;
      gs: {
        show: (key: string, stepIndex?: number) => void;
        closeAll: () => void;
      };
      _debugStatus?: () => unknown;
    };
  }
```

## 3. Connect guide buttons to React Router

A guide's **Visit link** action reloads the page unless you give the SDK a router. A **Run callback** action does nothing unless your code has registered a function with that name. Both need `useNavigate`, which only works inside `BrowserRouter`, so they live in one component you mount once at the top of `App`.

src/analytics/EngagementBridge.tsx

```typescript
import { useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { amplitudeReady } from '../lib/amplitude';

export function EngagementBridge() {
  const navigate = useNavigate();

  // Keep the latest navigate without re-registering on every route change.
  const navigateRef = useRef(navigate);
  useEffect(() => {
    navigateRef.current = navigate;
  }, [navigate]);

  useEffect(() => {
    let cancelled = false;

    amplitudeReady().then(() => {
      const engagement = window.engagement;
      if (cancelled || !engagement) return;

      // Guide links set to "Use router" call this instead of reloading.
      engagement.setRouter((url) => {
        const target = new URL(url, window.location.origin);
        if (target.origin === window.location.origin) {
          navigateRef.current(target.pathname + target.search + target.hash);
        } else {
          window.location.assign(url);
        }
      });

      // Guide buttons with "Run callback" and the name open_new_project.
      engagement.addCallback('open_new_project', () => {
        navigateRef.current('/projects/new');
      });

    return () => {
      cancelled = true;
    };
  }, []);

  return null;
}
```

Then, in the builder, set the URL behavior on every **Visit link** action to **Use router**, and enter `open_new_project` as the name on any **Run callback** action. The docs are explicit: **Same tab** and **New tab** ignore the router you set, and the callback name must match the string passed to `addCallback`.

**Why.** The docs' React Router example calls `setRouter` in a `useEffect`; this version waits for `amplitudeReady()` first, so `window.engagement` exists when the effect runs. The same-origin check keeps links to other sites as normal navigations, since `navigate` only handles routes inside your app. Mount the component where it never unmounts, so the callback stays registered.

## 4. Keep identity stable so guides show once

Amplitude remembers which guides and surveys a user has seen, completed, or dismissed as part of that user's history. The docs warn that if a user's identity changes between visits, Amplitude treats them as new and shows the experience again. The product analytics page's `IdentifyUser` calls `amplitude.reset()` whenever `user` is `null`, which is every anonymous page load, and `reset()` issues a new device ID. A visitor who isn't signed in would see the same welcome guide on every visit.

Reset only when someone signs out:

src/analytics/IdentifyUser.tsx

```typescript
import { useEffect } from 'react';
import { amplitude } from '../lib/amplitude';

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

export function IdentifyUser({ user }: { user: User }) {
  useEffect(() => {
    if (!user) return; // anonymous: keep the same device ID

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

    const identify = new amplitude.Identify();
    identify.set('plan', user.plan); // a flat string you can target on
    amplitude.identify(identify);
  }, [user?.id, user?.plan]);

  return null;
}
```

src/auth/signOut.ts

```typescript
import { amplitude } from '../lib/amplitude';

export async function signOut() {
  // ... your sign-out logic

  window.engagement?.gs.closeAll(); // close anything still on screen
  amplitude.reset(); // clears the user ID and issues a new device ID
}
```

**Why.** Guides and surveys refresh targeting automatically when the user or user properties change, so `setUserId` and `identify` are all targeting needs. Set `plan` as a flat string: targeting doesn't flatten arrays of objects, so a dotted path like `subscription.plan` from an array isn't available. To target new users, filter on the built-in **First Seen** property rather than a cohort, since cohort membership updates with a delay and won't match on a user's first visit. The default bucketing unit for rollouts is User ID, so set the user ID before you rely on a percentage rollout.

## 5. Give pins a target that survives a rebuild

Pins, tooltips, and card embeds attach to an element by CSS selector, and the docs recommend data attributes added specifically for guide targeting. A selector the visual picker builds from CSS Modules or CSS-in-JS class names can change on the next build, and then the pin stops rendering. Tag the elements you plan to point at:

src/pages/Home.tsx

```typescript
import { Link } from 'react-router-dom';

export function Home() {
  return (
    <section data-guide="home">
      <h1>Your projects</h1>
      <Link to="/projects/new" data-guide="new-project-button">
        New project
      </Link>
    </section>
  );
}
```

In the guide builder, add a Pin step and enter `[data-guide="new-project-button"]` as its CSS selector instead of the generated one.

**Why.** The docs say that if the target element isn't in the DOM when the step renders, the pin doesn't display, and the SDK doesn't move it to another position. In a React app, anything behind `Suspense`, a `lazy()` route, or a data fetch can render after the guide triggers. For those, set the trigger to **When element appears** with the same selector, so the guide fires when the element renders rather than when the page loads. On a later step in a tour, turn on **Skip if target not found**, which moves on if the target doesn't appear within 250 ms.

## 6. Open a survey from your own button

When a survey should open because someone clicked something, the docs recommend setting its trigger to **None** in the builder and calling `show` from the click handler.

src/components/FeedbackButton.tsx

```typescript
export function FeedbackButton() {
  return (
    <button
      type="button"
      onClick={() => window.engagement?.gs.show('product-feedback')}
    >
      Give feedback
    </button>
  );
}
```

Mount everything in `App`:

src/App.tsx

```typescript
import { Routes, Route } from 'react-router-dom';
import { EngagementBridge } from './analytics/EngagementBridge';
import { IdentifyUser } from './analytics/IdentifyUser';
import { useAuth } from './auth/useAuth';
import { FeedbackButton } from './components/FeedbackButton';
import { Home } from './pages/Home';
import { NewProject } from './pages/NewProject';

export function App() {
  const { user } = useAuth(); // your auth hook

  return (
    <>
      <EngagementBridge />
      <IdentifyUser user={user} />
      <header>
        <FeedbackButton />
      </header>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/projects/new" element={<NewProject />} />
      </Routes>
    </>
  );
}
```

Replace `product-feedback` with your survey's key from the builder. `show` ignores targeting rules and limits but still respects page targeting, so the survey's Where settings must allow every route where the button renders.

If you'd rather use the **On event tracked** trigger, the event has to fire in the browser. `Project Created`, tracked with `amplitude.track` on the product analytics page, works. An event your backend sends never reaches the guides and surveys SDK; call `show` from the client after that request resolves instead.

**MCP.** Working in Cursor or Claude Code? Amplitude's MCP server includes `list_guides_surveys` and `get_guide_or_survey`, which list your project's guides and surveys and return each one's page targeting and trigger configuration, so your agent can check that the key it wires into `show` exists and is allowed on that route. [Set up the MCP server](https://amplitude.com/docs/amplitude-ai/amplitude-mcp).

## Verify it worked

Run `npm run dev` and sign in as a test user. In Amplitude, create a guide with a Pin step on `[data-guide="new-project-button"]` and a button whose action is **Run callback** with `open_new_project`, and a survey with the key `product-feedback` and the trigger **None**. Add your test user under **Test users** and set both to **Testing**, or click **Preview** and enter your local URL. Then check in this order:

1. **Browser console.** `window.engagement` returns an object, not `undefined`. Run `window.engagement._debugStatus()`: `user.user_id` should be your `user-` prefixed ID, `apiKey` should be set, `stateInitialized` and `decideSuccessful` should be `true`, and `num_guides_surveys` should be non-zero on the home page.
2. **Amplitude Chrome extension.** The Guides & Surveys tab shows four setup checks: **Installed**, **Initialized**, **Analytics Connected**, and **Booted**. Let the page finish loading first, since the tab doesn't update live.
3. **In the app.** The pin appears on **New project**. Its button navigates to `/projects/new` without a full page load, and the Network tab shows no new document request. **Give feedback** opens the survey.
4. **Network tab.** Filter for `amplitude.com`. You should see requests to `gs.amplitude.com`, the default guides and surveys host, and the analytics SDK's `POST` to `api2.amplitude.com/2/httpapi` returning `200`.
5. **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.
6. **User lookup.** Go to Users > User Profiles and search your `user-` prefixed test ID. Within ten seconds to a minute the event stream shows `Guide Viewed` for the pin, and `Survey Viewed` and `Survey Submitted` after you answer. Guides and surveys events don't count toward your event volume.

A guide you already completed or dismissed won't show again. Open your user profile, go to the Guides or Surveys tab, and select **Clear history** for that experience, or click **Reset User History** in the preview bar.

## Troubleshooting

### `window.engagement` is undefined

Guides and surveys never initialized. Confirm `import './lib/amplitude'` is the first line of `src/main.tsx`, that `VITE_AMPLITUDE_API_KEY` is set (Vite reads `.env.local` only at startup, so restart the dev server), and that nothing passes `engagement: { skip: true }`.

### A guide set to Immediately shows twice in development, or not at all

Something calls `engagement.boot()` or `engagement.init()` on top of `initAll`, often in a `useEffect` that Strict Mode runs twice. Delete those calls.

### Anonymous visitors see the same welcome guide on every visit

`amplitude.reset()` runs while the user is signed out and issues a new device ID, so every visit looks like a new user. Remove the `reset()` branch from `IdentifyUser` and reset only in `signOut()`.

### The pin never appears, but the survey button works

The target element isn't in the DOM when the step renders, or the selector no longer matches. Check `document.querySelector('[data-guide="new-project-button"]')` in the console, and for late-rendered elements use the **When element appears** trigger.

### Guide links reload the whole page

The link's URL behavior isn't **Use router**, or `EngagementBridge` isn't mounted inside `BrowserRouter`. A `useNavigate() may be used only in the context of a <Router>` error means the second.

### A Run callback button does nothing

The name in the builder doesn't match the `addCallback` string. Run `Object.keys(window.engagement._.callbacks)` in the console; `open_new_project` should be in the list, next to internal entries like `engagement-router`.

### Preview never opens on your page

Your server sends `Cross-Origin-Opener-Policy: same-origin`, which blocks the messages Preview mode depends on. Set it to `same-origin-allow-popups`, as in step 1.

## Full code reference

Every file from this tutorial in one block, the version to hand to an agent. It assumes no CSP nonce. 9 files.

11 files, 193 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;

let ready: Promise<void> = Promise.resolve();

if (apiKey) {
  ready = Promise.resolve(
    amplitude.initAll(apiKey, {
      analytics: {
        autocapture: {
          attribution: true,
          pageViews: true,
          sessions: true,
          formInteractions: true,
          fileDownloads: true,
          elementInteractions: true,
        },
      sessionReplay: {
        sampleRate: 1,
      },
    }),
  ).then(() => undefined);
} else {
  console.warn('VITE_AMPLITUDE_API_KEY is not set; analytics is off.');
}

export function amplitudeReady() {
  return ready;
}

export { amplitude };

// -- src/types/engagement.d.ts --
export {};

declare global {
  interface Window {
    engagement?: {
      setRouter: (routerFn: (url: string) => void) => void;
      addCallback: (name: string, callback: () => void) => void;
      gs: {
        show: (key: string, stepIndex?: number) => void;
        closeAll: () => void;
      };
      _debugStatus?: () => unknown;
    };
  }

// -- src/main.tsx --
import './lib/amplitude';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { App } from './App';

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </StrictMode>,
);

// -- src/analytics/EngagementBridge.tsx --
import { useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { amplitudeReady } from '../lib/amplitude';

export function EngagementBridge() {
  const navigate = useNavigate();
  const navigateRef = useRef(navigate);
  useEffect(() => {
    navigateRef.current = navigate;
  }, [navigate]);

  useEffect(() => {
    let cancelled = false;

    amplitudeReady().then(() => {
      const engagement = window.engagement;
      if (cancelled || !engagement) return;

      engagement.setRouter((url) => {
        const target = new URL(url, window.location.origin);
        if (target.origin === window.location.origin) {
          navigateRef.current(target.pathname + target.search + target.hash);
        } else {
          window.location.assign(url);
        }
      });

      engagement.addCallback('open_new_project', () => {
        navigateRef.current('/projects/new');
      });

    return () => {
      cancelled = true;
    };
  }, []);

  return null;
}

// -- src/analytics/IdentifyUser.tsx --
import { useEffect } from 'react';
import { amplitude } from '../lib/amplitude';

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

export function IdentifyUser({ user }: { user: User }) {
  useEffect(() => {
    if (!user) return;
    amplitude.setUserId(`user-${user.id}`);
    const identify = new amplitude.Identify();
    identify.set('plan', user.plan);
    amplitude.identify(identify);
  }, [user?.id, user?.plan]);

  return null;
}

// -- src/auth/signOut.ts --
import { amplitude } from '../lib/amplitude';

export async function signOut() {
  // ... your sign-out logic
  window.engagement?.gs.closeAll();
  amplitude.reset();
}

// -- src/pages/Home.tsx --
import { Link } from 'react-router-dom';

export function Home() {
  return (
    <section data-guide="home">
      <h1>Your projects</h1>
      <Link to="/projects/new" data-guide="new-project-button">
        New project
      </Link>
    </section>
  );
}

// -- src/components/FeedbackButton.tsx --
export function FeedbackButton() {
  return (
    <button
      type="button"
      onClick={() => window.engagement?.gs.show('product-feedback')}
    >
      Give feedback
    </button>
  );
}

// -- src/App.tsx --
import { Routes, Route } from 'react-router-dom';
import { EngagementBridge } from './analytics/EngagementBridge';
import { IdentifyUser } from './analytics/IdentifyUser';
import { useAuth } from './auth/useAuth';
import { FeedbackButton } from './components/FeedbackButton';
import { Home } from './pages/Home';
import { NewProject } from './pages/NewProject';

export function App() {
  const { user } = useAuth();

  return (
    <>
      <EngagementBridge />
      <IdentifyUser user={user} />
      <header>
        <FeedbackButton />
      </header>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/projects/new" element={<NewProject />} />
      </Routes>
    </>
  );
}

// -- src/pages/NewProject.tsx --
// Unchanged from /build/react-product-analytics.
```

## Hand it to your agent

Prefer to have Cursor or Claude Code 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 guides and surveys to this Vite + React Router app by
following https://amplitude.com/build/react-guides-surveys exactly. Use
@amplitude/unified and call initAll once at module scope in
src/lib/amplitude.ts, imported first in src/main.tsx, and export an
amplitudeReady() promise. Never call engagement.init() or
engagement.boot(), and never initialize inside useEffect. Add an
EngagementBridge component inside BrowserRouter that waits for
amplitudeReady(), then calls window.engagement.setRouter with
useNavigate for same-origin URLs and registers callbacks with
window.engagement.addCallback. Call amplitude.reset() only in the
sign-out handler, never when the user is null. Add data-guide
attributes to elements I want guides to point at. Open surveys from
click handlers with window.engagement.gs.show(key). Read
VITE_AMPLITUDE_API_KEY from the environment and don't hardcode it.
Tell me session replay records at sampleRate 1 and how to turn it off.
Report every file you changed.
```

## What this unlocks

- [Session replay](https://amplitude.com/session-replay), on at the sample rate from step 2, lets you watch what users did right after they submitted `product-feedback`.
- [Heatmaps](https://amplitude.com/heatmaps) are built from session replays, so they show whether people click `new-project-button` more after the pin goes live.
- [Feature Experiment](https://amplitude.com/amplitude-experiment), bundled in the same package, can gate the feature a guide announces, so the flag and the guide reach the same users.
- [Product analytics](https://amplitude.com/amplitude-analytics) charts `Guide Viewed` next to `Project Created`, so you can see whether the pin moves project creation.

One integration; everything else reads from the same events.

## Next steps

[Web frameworksAdd feature flags and experiments to a React appGate the feature a guide announces behind a flag.](/build/react-feature-flags)[Web frameworksAdd session replay to a React appRecord the sessions around a guide or survey, with the masking a React app needs.](/build/react-session-replay)[Web frameworksAdd guides and surveys to a Next.js appThe same setup when your React app renders on the server.](/build/nextjs-guides-surveys)

Reference docs

[Guides and Surveys Web SDK →](https://amplitude.com/docs/sdks/guides-and-surveys/sdk)[Browser Unified SDK →](https://amplitude.com/docs/sdks/analytics/browser/browser-unified-sdk)[Setup and Targeting →](https://amplitude.com/docs/guides-and-surveys/setup-and-target)[Testing and Publishing →](https://amplitude.com/docs/guides-and-surveys/testing)[Guide or Survey not showing →](https://amplitude.com/docs/guides-and-surveys/troubleshooting/guide-or-survey-not-showing)[Amplitude MCP server →](https://amplitude.com/docs/amplitude-ai/amplitude-mcp)

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

For agents

[View raw .md](/build/react-guides-surveys.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)
