---
title: "How to add web analytics to a SvelteKit app"
description: "Track page views, sessions, and UTM attribution in a SvelteKit app, keep campaign data through server redirects, and read traffic by channel."
canonical: "https://amplitude.com/en-us/build/sveltekit-web-analytics"
---

# How to add web analytics to a SvelteKit app

Track page views, sessions, and UTM attribution in a SvelteKit app, keep campaign data through server redirects, and read traffic by channel.

Source: https://amplitude.com/en-us/build/sveltekit-web-analytics

---

# How to add web analytics to a SvelteKit app

Configure the Amplitude Unified SDK so every SvelteKit navigation becomes a page view, every visit carries its UTM and referrer data, and sessions feed bounce rate and session length. Then keep campaign parameters alive through redirect() and goto(), and open the Marketing Analytics views that read them. About 15 minutes, all copy-paste.

- SvelteKit 2.10+
- Svelte 5
- Unified SDK 1.x
- 15 min

Chris Tam · Last updated 13 October 2026

SvelteKit renders the first page on the server, then handles every navigation after that in the browser. [Web analytics](https://amplitude.com/web-analytics) tells you where those visitors come from, which campaigns bring them, and which pages keep them.

In this tutorial, you'll configure Amplitude for traffic data, keep campaign parameters through SvelteKit redirects, and set up the channel and campaign views. It builds on the [SvelteKit product analytics page](/build/sveltekit-product-analytics).

## What you'll build

A SvelteKit app that sends `[Amplitude] Page Viewed` on first load and on every path change, starts and ends sessions automatically, and stores `utm_source`, `utm_medium`, `utm_campaign`, `referrer`, and click IDs as both user properties and event properties, including on visits that land on a URL a server `redirect()` or a `goto()` rewrites. By the end you'll see a test campaign in your event stream and a Marketing Analytics view configured to read it.

## Prerequisites

- A SvelteKit 2.10 or later app with Svelte 5 and TypeScript, for example one created with `npx sv create`. The `init` export in `src/hooks.client.ts` arrived in SvelteKit 2.10.
- An auth setup that puts the signed-in user on `event.locals.user` in `src/hooks.server.ts`, with `user` declared on `App.Locals` in `src/app.d.ts`, as on the [SvelteKit product analytics page](/build/sveltekit-product-analytics). Step 4 uses it for a sign-in guard.
- An Amplitude project API key. In Amplitude, go to Settings > Projects > your project > General.
- An Admin or Manager role in the Amplitude project for step 5. The Member role can't edit Marketing Analytics settings.

A free account covers the tracking on this page. [Sign up for free](https://app.amplitude.com/signup) to get your project API key. Custom Marketing Analytics views need a Growth or Enterprise plan; [compare plans](https://amplitude.com/pricing).

## 1. Install the SDK and add the key

[The Quickstart](https://amplitude.com/docs/get-started/amplitude-quickstart) recommends `@amplitude/unified` for browser apps installed from npm, and the [SvelteKit product analytics page](/build/sveltekit-product-analytics) uses it, so this setup changes the same `src/hooks.client.ts`. If you already followed that page, skip to step 2.

Terminal

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

SvelteKit only exposes env vars prefixed with `PUBLIC_` 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

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

## 2. Configure autocapture for traffic data

Attribution, page views, sessions, and page URL enrichment are all on by default. Name them anyway, so nobody turns one off later without knowing what it feeds, and change one: `trackHistoryChanges`. This is the complete file, with the base page's clicks and session replay settings kept.

If you followed other SvelteKit pages first, keep any options you added there, and only change the `autocapture` and `sessionReplay` blocks this page names. If you added the `setAmplitudeReady(...)` wrapper on [the feature flags page](/build/sveltekit-feature-flags) or [the guides and surveys page](/build/sveltekit-guides-surveys), keep it around `initAll`.

src/hooks.client.ts

```typescript
import type { ClientInit } from '@sveltejs/kit';
import { dev } from '$app/environment';
import { PUBLIC_AMPLITUDE_API_KEY } from '$env/static/public';
import * as amplitude from '@amplitude/unified';

export const init: ClientInit = () => {
  if (!PUBLIC_AMPLITUDE_API_KEY) {
    console.warn('PUBLIC_AMPLITUDE_API_KEY is not set; analytics is off.');
    return;
  }

  void amplitude.initAll(PUBLIC_AMPLITUDE_API_KEY, {
    analytics: {
      autocapture: {
        // UTM, referrer, and click ID capture. Feeds Traffic by Channel and Campaign.
        attribution: true,
        // [Amplitude] Page Viewed on load and on every path change.
        // 'pathOnly' ignores query-string-only changes such as ?sort=price.
        // Using hash routing? Read step 3 before keeping this line.
        pageViews: { trackHistoryChanges: 'pathOnly' },
        // [Amplitude] Start Session and End Session. Feeds bounce rate and session length.
        sessions: true,
        // Adds current and previous page URL properties.
        pageUrlEnrichment: true,
        formInteractions: true,
        fileDownloads: true,
        elementInteractions: true,
      },
    sessionReplay: {
      // Every session while you build; about 1% in production.
      sampleRate: dev ? 1 : 0.01,
      debugMode: dev,
      // From /build/sveltekit-session-replay. Keep your masking rules when you paste this file.
      privacyConfig: {
        maskSelector: ['[data-amp-mask]'],
        blockSelector: ['[data-amp-block]'],
        unmaskSelector: ['[data-amp-unmask]'],
      },
  });
};
```

**Why.** The client hook runs once in the browser, before the server-rendered page hydrates, so the SDK reads campaign data from the URL the browser actually loaded. Anything that happened on the server before that, such as a `redirect()` in a load function, already decided which URL that is. That's why step 4 fixes redirects on the server and not here. Keep `initAll` out of `+layout.svelte` and load functions; both run on the server.

`@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. Web analytics doesn't need it; it's here because the base page set it. `debugMode: dev` and the `privacyConfig` selectors come from the [SvelteKit session replay page](/build/sveltekit-session-replay): `debugMode` ignores the focus check so replays record with DevTools open in development, and the selectors match nothing until you add the `data-amp-*` attributes, so the block is safe to keep either way.

From Browser SDK 2.45.0, the SDK stores campaign parameters two ways by default: as user properties (including first-touch `initial_utm_source` and friends, set once) and as event properties on every event. If the Network tab check below shows no `utm_` values in `event_properties`, update `@amplitude/unified` to a release that bundles Browser SDK 2.45.0 or later. For referrer exclusions and the other attribution options, see [Track marketing attribution](https://amplitude.com/docs/sdks/analytics/browser/browser-sdk-2#track-marketing-attribution).

## 3. Let autocapture count navigations

There's no page view code in this app, and there shouldn't be. With `pageViews` on, the SDK sends `[Amplitude] Page Viewed` on init and again when the URL changes in the browser, which covers `<a href>` links and `goto()`. Each event carries `[Amplitude] Page Path`, `[Amplitude] Page Title`, `[Amplitude] Page URL`, `[Amplitude] Page Location`, and `[Amplitude] Page Counter`.

`trackHistoryChanges: 'pathOnly'` sends a page view when the path changes, such as `/pricing` to `/docs`, and skips changes to only the query string or hash. Use the default, `'all'`, if your query strings mark different pages, such as `/blog?page=2`. With `'all'`, a product list that calls `goto('?sort=price', { replaceState: true, keepFocus: true, noScroll: true })` on every filter click records a page view per click, which inflates page views per session and skews entry and exit pages.

Shallow routing follows the same rule. `pushState('/photos/42', { selected: 42 })` from `$app/navigation` changes the path, so it records a page view; `pushState('', { showModal: true })` keeps the URL and records nothing.

SvelteKit routes on the path by default. If you set `kit.router.type` to `'hash'` in `svelte.config.js` (SvelteKit 2.14 and later), the route moves into the hash (`/#/pricing`). `[Amplitude] Page Path` excludes the hash, so it reads `/` on every screen, and with `'pathOnly'` route changes never send a page view. With hash routing, set `pageViews: true`, which tracks hash changes, and break page reports down by `[Amplitude] Page Location` (the full URL) rather than `[Amplitude] Page Path`. Campaign links then need their UTM parameters in the real query string, before the `#`: `/?utm_source=newsletter#/pricing`. Hash routing has no server redirects, so step 4 applies only to `goto()` calls.

Don't add an `afterNavigate` callback that calls `track('Page Viewed')`. It produces a second, differently named page view per navigation, and Marketing Analytics only counts the one it's configured to read.

## 4. Keep UTM parameters through SvelteKit redirects

The SDK can only record campaign data that's in the URL. SvelteKit's `redirect()` goes exactly where you point it, and a plain path has no query string. A visitor clicks a newsletter link to `/account?utm_source=newsletter`, a server load calls `redirect(303, '/login')`, and the browser follows the redirect to a bare `/login`. On that first request the redirect is a real HTTP response, so it happens before any JavaScript runs: the SDK starts on `/login`, never sees the original URL, and records no campaign at all, not even first-touch `initial_utm_source`. Amplitude's docs call this out for single-page apps: UTM parameters can drop during SPA redirects.

Four patterns drop them:

- A `redirect()` in a `+page.server.ts`, `+layout.server.ts`, or universal `+page.ts` load, such as an auth guard. On a visitor's first request, all three run on the server.
- A `redirect()` in a `+server.ts` endpoint or the `handle` hook in `src/hooks.server.ts`, such as an old campaign URL you moved. In `handle`, the URL is `event.url`: `if (event.url.pathname === '/old-landing') redirect(308, withCampaign('/pricing', event.url));`.
- A `redirect()` from a form action, such as sending people to `/welcome` after sign-in.
- A `goto('/welcome', { replaceState: true })` in the browser, which keeps first-touch values but drops the campaign from the event properties of every event after it.

Carry the campaign parameters across with one small helper. It copies only the UTM and click ID parameters the SDK reads, so it never drags unrelated query keys onto the new page:

src/lib/campaign.ts

```typescript
// The UTM and click ID parameters the Browser SDK reads for attribution.
const CAMPAIGN_KEYS = [
  'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'utm_id',
  'gclid', 'fbclid', 'dclid', 'gbraid', 'wbraid', 'ko_click_id',
  'li_fat_id', 'msclkid', 'rdt_cid', 'ttclid', 'twclid',
];

// redirect(303, '/login') on its own drops ?utm_source=...
export function withCampaign(path: string, from: URL): string {
  const target = new URL(path, from);
  for (const key of CAMPAIGN_KEYS) {
    const value = from.searchParams.get(key);
    if (value !== null && !target.searchParams.has(key)) {
      target.searchParams.set(key, value);
    }
  return target.pathname + target.search;
}
```

Use it in every guard, passing the incoming `url` from the load event:

src/routes/account/+layout.server.ts

```typescript
import { redirect } from '@sveltejs/kit';
import type { LayoutServerLoad } from './$types';
import { withCampaign } from '$lib/campaign';

export const load: LayoutServerLoad = ({ locals, url }) => {
  if (!locals.user) {
    redirect(303, withCampaign('/login', url));
  }
};
```

Use it in every endpoint that moves an old URL, including ones that add their own query:

src/routes/plans/+server.ts

```typescript
import { redirect } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { withCampaign } from '$lib/campaign';

// Old campaign URL, moved to /pricing for good.
export const GET: RequestHandler = ({ url }) => {
  redirect(308, withCampaign('/pricing', url));
};
```

src/routes/offer/\[code]/+server.ts

```typescript
import { redirect } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { withCampaign } from '$lib/campaign';

// Sets its own query, so the helper merges ?utm_... into it.
export const GET: RequestHandler = ({ params, url }) => {
  redirect(307, withCampaign(`/pricing?offer=${encodeURIComponent(params.code)}`, url));
};
```

And in the sign-in action. `signIn` stands in for your auth library's sign-in call; keep your own and change only the `redirect` line. Leave the `action` attribute off the form so it posts to the current URL, query string included, which is what puts the campaign parameters on the action's `url`:

src/routes/login/+page.server.ts

```typescript
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
import { withCampaign } from '$lib/campaign';
import { signIn } from '$lib/server/auth'; // your sign-in logic

export const actions: Actions = {
  default: async ({ request, cookies, url }) => {
    const ok = await signIn(await request.formData(), cookies);
    if (!ok) return fail(400, { error: 'Wrong email or password' });

    // url is /login?utm_source=..., because the form posts to the current URL.
    redirect(303, withCampaign('/welcome', url));
  },
};
```

src/routes/login/+page.svelte

```html
<script lang="ts">
  import { enhance } from '$app/forms';

  let { form } = $props();
</script>

<!-- No action attribute: the form posts to /login?utm_source=... -->
<form method="POST" use:enhance>
  <input name="email" type="email" required />
  <input name="password" type="password" required />
  {#if form?.error}<p>{form.error}</p>{/if}
  <button>Sign in</button>
</form>
```

A named action such as `action="?/signin"` replaces the whole query string with `?/signin`, so the campaign is gone before the action runs. If the form must use a named action, append the current query: `action="?/signin&{page.url.searchParams}"` with `page` from `$app/state` (SvelteKit 2.12 and later). SvelteKit picks the action from the parameter that starts with `/` and leaves the rest on the action's `url`. If you navigate from the browser instead, wrap the path the same way: `goto(withCampaign('/welcome', new URL(location.href)), { replaceState: true })` inside an event handler.

**Why.** Event property tracking re-reads campaign parameters on page load and on every SPA navigation, including the `replaceState` a client-side redirect performs. Whatever is in the URL after the redirect is what gets attached to the events that follow. For the same reason, don't strip UTM parameters from the address bar with `replaceState` to tidy the URL.

Check each guard, endpoint, and action in your app with a `?utm_source=test` URL before you trust your channel numbers.

The Verify steps below visit `/pricing`, `/welcome`, and `/account`. If your app doesn't have those pages yet, add a one-line stub for each:

src/routes/pricing/+page.svelte

```html
<h1>Pricing</h1>
```

src/routes/welcome/+page.svelte

```html
<h1>Welcome</h1>
```

src/routes/account/+page.svelte

```html
<h1>Account</h1>
```

## 5. Set up the Marketing Analytics views

In Amplitude, web analytics lives under **Marketing Analytics** in the navigation bar. It uses Browser SDK events by default, so this step is configuration, not code.

1. Create the channel classifier. Go to Data > Properties > Channels. Opening that page creates `[Amplitude] Default Channels`, which maps UTM parameters and referrer data into GA4-aligned channels such as Paid Search and Organic Social. If your org predates it, click **Build with the new logic** when prompted.
2. Confirm the page view event. Open Marketing Analytics, click **Customize**, and select Page View and Filter. Choose `[Amplitude] Page Viewed` as the event. Set a domain filter if the project also receives events from another site.
3. Define a conversion funnel. Under Conversion Funnel, choose the events that make up your funnel. If you followed the [SvelteKit product analytics page](/build/sveltekit-product-analytics), `[Amplitude] Page Viewed` followed by `Project Created` works as a first funnel.
4. Read the views. Traffic by Channel, Traffic by Campaign, Page Engagement, and Conversion each show a time series and a table. Page Engagement covers visitors, page views, bounce rate, page views per session, and entry and exit rate per page. Ad Performance needs an ad network source such as Google Ads connected first.

For every setting, see [Out-of-the-box Marketing Analytics](https://amplitude.com/docs/analytics/ootb-marketing-analytics).

**MCP.** Working in Cursor or Claude Code? Amplitude's MCP server lets your agent look up the events already in your project before it writes `track` calls, so a conversion event matches a name 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`, open `http://localhost:5173/offer/spring?utm_source=build-test&utm_medium=email&utm_campaign=sveltekit-web-analytics` in a fresh private window, click through two pages, then check in this order:

1. **Network tab.** Turn on Preserve log first. The document request for `/offer/spring` should return `307`, and its `Location` response header should read `/pricing?offer=spring&utm_source=build-test&utm_medium=email&utm_campaign=sveltekit-web-analytics`. Then open `/account?utm_source=build-test` while signed out: it should return `303` with `utm_source` still in the `Location` header for `/login`. If either loses its parameters, the fix is step 4, and nothing downstream will work until it is. Then filter for `api2.amplitude.com`. You should see a `POST` returning `200`. The SDK compresses request bodies of 2 KB or more, so read the events in the Instrumentation Explorer Chrome extension instead: the `[Amplitude] Page Viewed` event for `/pricing` should carry `utm_source: build-test` in `event_properties`, and an identify event should set `utm_source` and `initial_utm_source` as user properties.
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 device ID (the Instrumentation Explorer Chrome extension shows it as device\_id). Within ten seconds to a minute, the event stream shows `[Amplitude] Start Session`, then one `[Amplitude] Page Viewed` per page, and the profile shows `initial_utm_source: build-test`.
4. **Marketing Analytics.** Open Traffic by Campaign for today. `sveltekit-web-analytics` should appear as a campaign. It can take a few minutes to appear.

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 dev server only reads `.env` at startup, so restart it after editing. Because the key is baked in at build time, changing it in production means rebuilding.

### Campaign traffic shows up as Direct

The UTM parameters were gone before the SDK started. Open a `?utm_source=test` URL with Preserve log on and read the `Location` header of every `3xx` document response: a guard, endpoint, or action that calls `redirect()` with a bare path is the usual cause. A form with a named action, or a `goto()` that doesn't pass the query, does the same in the browser. The Channels docs also note that inconsistent `set()` and `unset()` calls on UTM user properties can classify traffic as Other.

### Two page view events per navigation

An `afterNavigate` callback or a page view helper is running alongside autocapture. Delete it. If both events are `[Amplitude] Page Viewed`, check that `initAll` is only called from `src/hooks.client.ts`.

### A page view every time a filter or sort changes

The page writes state to the query string with `goto('?sort=...')` and `trackHistoryChanges` is `'all'`. Set `pageViews: { trackHistoryChanges: 'pathOnly' }` as in step 2.

### Only one page view per visit, no matter how many pages

The app uses hash routing with `trackHistoryChanges: 'pathOnly'`. Every route shares the path `/`, so no path change ever happens. Set `pageViews: true`, or switch back to path routing.

### Every page in Page Engagement has the same title

Your pages never set a `<title>`, so every `[Amplitude] Page Title` is the same. Add `<svelte:head><title>Pricing</title></svelte:head>` to each page, or break the view down by `[Amplitude] Page Path` under Customize > Breakdown.

### Page views stopped even though the code enables them

Remote configuration overrides local autocapture settings feature by feature, and Browser SDK 2.16.1 and later fetch it by default. Check Data > Settings > Autocapture and make sure page views and sessions aren't turned off there. To keep autocapture under code control, add `remoteConfig: { fetchRemoteConfig: false }` to the `analytics` block.

### The build fails with "PUBLIC\_AMPLITUDE\_API\_KEY is not exported"

`$env/static/public` reads variables at build time, and the variable isn't set where the build runs. Add it to `.env` locally and to your CI or host's build environment. The name must start with `PUBLIC_`.

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

An ad blocker is dropping the call. Route events through a [proxy on your own domain](https://amplitude.com/docs/sdks/analytics/proxy-requests) and set `serverUrl` in the `analytics` block to point at it. Point `remoteConfig.serverUrl` at it too, so remote configuration requests aren't blocked.

## Full code reference

Every file from this tutorial in one block, the version to hand to an agent. 11 files, plus your existing `src/app.d.ts`, hooks, and auth.

11 files, 127 lines

```typescript
// -- .env --
PUBLIC_AMPLITUDE_API_KEY=your-project-api-key

// -- src/hooks.client.ts --
import type { ClientInit } from '@sveltejs/kit';
import { dev } from '$app/environment';
import { PUBLIC_AMPLITUDE_API_KEY } from '$env/static/public';
import * as amplitude from '@amplitude/unified';

export const init: ClientInit = () => {
  if (!PUBLIC_AMPLITUDE_API_KEY) {
    console.warn('PUBLIC_AMPLITUDE_API_KEY is not set; analytics is off.');
    return;
  }

  void amplitude.initAll(PUBLIC_AMPLITUDE_API_KEY, {
    analytics: {
      autocapture: {
        attribution: true,
        pageViews: { trackHistoryChanges: 'pathOnly' },
        sessions: true,
        pageUrlEnrichment: true,
        formInteractions: true,
        fileDownloads: true,
        elementInteractions: true,
      },
    sessionReplay: {
      sampleRate: dev ? 1 : 0.01,
      debugMode: dev,
      // From /build/sveltekit-session-replay. Keep your masking rules when you paste this file.
      privacyConfig: {
        maskSelector: ['[data-amp-mask]'],
        blockSelector: ['[data-amp-block]'],
        unmaskSelector: ['[data-amp-unmask]'],
      },
  });
};

// -- src/lib/campaign.ts --
const CAMPAIGN_KEYS = [
  'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'utm_id',
  'gclid', 'fbclid', 'dclid', 'gbraid', 'wbraid', 'ko_click_id',
  'li_fat_id', 'msclkid', 'rdt_cid', 'ttclid', 'twclid',
];

export function withCampaign(path: string, from: URL): string {
  const target = new URL(path, from);
  for (const key of CAMPAIGN_KEYS) {
    const value = from.searchParams.get(key);
    if (value !== null && !target.searchParams.has(key)) {
      target.searchParams.set(key, value);
    }
  return target.pathname + target.search;
}

// -- src/routes/account/+layout.server.ts --
import { redirect } from '@sveltejs/kit';
import type { LayoutServerLoad } from './$types';
import { withCampaign } from '$lib/campaign';

export const load: LayoutServerLoad = ({ locals, url }) => {
  if (!locals.user) {
    redirect(303, withCampaign('/login', url));
  }
};

// -- src/routes/plans/+server.ts --
import { redirect } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { withCampaign } from '$lib/campaign';

export const GET: RequestHandler = ({ url }) => {
  redirect(308, withCampaign('/pricing', url));
};

// -- src/routes/offer/[code]/+server.ts --
import { redirect } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { withCampaign } from '$lib/campaign';

export const GET: RequestHandler = ({ params, url }) => {
  redirect(307, withCampaign(`/pricing?offer=${encodeURIComponent(params.code)}`, url));
};

// -- src/routes/login/+page.server.ts --
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
import { withCampaign } from '$lib/campaign';
import { signIn } from '$lib/server/auth'; // your sign-in logic

export const actions: Actions = {
  default: async ({ request, cookies, url }) => {
    const ok = await signIn(await request.formData(), cookies);
    if (!ok) return fail(400, { error: 'Wrong email or password' });
    redirect(303, withCampaign('/welcome', url));
  },
};

// -- src/routes/login/+page.svelte --
<script lang="ts">
  import { enhance } from '$app/forms';

  let { form } = $props();
</script>

<form method="POST" use:enhance>
  <input name="email" type="email" required />
  <input name="password" type="password" required />
  {#if form?.error}<p>{form.error}</p>{/if}
  <button>Sign in</button>
</form>

// -- src/routes/pricing/+page.svelte --
<h1>Pricing</h1>

// -- src/routes/welcome/+page.svelte --
<h1>Welcome</h1>

// -- src/routes/account/+page.svelte --
<h1>Account</h1>

// -- src/app.d.ts, src/hooks.server.ts and src/lib/server/auth.ts --
// Your existing auth. App.Locals must declare user, as in src/app.d.ts on
// /build/sveltekit-product-analytics. 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
Complete https://amplitude.com/build/sveltekit-product-analytics first, or
confirm this app already has its setup. Then set up Amplitude web
analytics by following https://amplitude.com/build/sveltekit-web-analytics
exactly. Use @amplitude/unified and call initAll once inside the init
export of src/hooks.client.ts, reading PUBLIC_AMPLITUDE_API_KEY from
$env/static/public. If you added the setAmplitudeReady(...) wrapper on
/build/sveltekit-feature-flags or /build/sveltekit-guides-surveys, keep it
around initAll. Never initialize in a component script, onMount, or a
load function. Inside analytics: { autocapture } keep attribution,
sessions, pageUrlEnrichment, formInteractions, fileDownloads and
elementInteractions on, as on the base page, and set pageViews to
{ trackHistoryChanges: 'pathOnly' } unless svelte.config.js sets
kit.router.type to 'hash', in which case use pageViews: true. Keep the
sessionReplay sampleRate at dev ? 1 : 0.01 with dev from $app/environment,
set debugMode: dev and the privacyConfig maskSelector, blockSelector and
unmaskSelector for the data-amp-mask, data-amp-block and data-amp-unmask
attributes, keep any other options already in src/hooks.client.ts, and
tell me how to turn session replay off. Don't add an afterNavigate page
view callback. Add the withCampaign helper in src/lib/campaign.ts with the
page's 6 UTM and 11 click ID keys, then find every redirect() in
hooks.server.ts (using event.url), +server.ts endpoints, load functions,
and form actions, and every goto() after sign-in, and make each one carry
the UTM and click ID parameters from the incoming URL. Make sign-in forms
post to the current URL with a default action; if a form needs a named
action, append the current query with page.url.searchParams from
$app/state. Also add the page's demo routes so the Verify steps run:
src/routes/offer/[code]/+server.ts (307 to /pricing?offer=<code>),
src/routes/plans/+server.ts (308 to /pricing), the
src/routes/account/+layout.server.ts guard (303 to /login when
locals.user is missing), and one-line stub pages for /pricing, /welcome
and /account if they don't exist. If the app has no auth on locals.user
(declared on App.Locals in src/app.d.ts) or no sign-in action, skip the
account guard and src/routes/login and tell me. Don't hardcode the key
and don't invent event names. Report every file you changed and every
redirect you checked.
```

## What this unlocks

- [Session replay](https://amplitude.com/session-replay), already on from step 2, lets you watch what visitors with `utm_campaign: sveltekit-web-analytics` did after they landed on `/pricing`.
- [Heatmaps](https://amplitude.com/heatmaps), built from those session replays, show where visitors click on `/pricing`, and a segment on `initial_utm_source` splits paid from organic.
- [Web Experiment](https://amplitude.com/web-experimentation) can test a new `/pricing` layout, launched from the Page Engagement table, with your conversion funnel as the metric.
- [Guides and surveys](https://amplitude.com/guides-and-surveys) can ask visitors whose `utm_medium` is `email` what brought them back.

One integration; everything else reads from the same events.

## Next steps

[Web frameworksHow to add product analytics to a SvelteKit appIdentify signed-in users and track \`Project Created\`, the event your conversion funnel needs.](/build/sveltekit-product-analytics)[Web frameworksHow to add session replay to a SvelteKit appWatch the sessions behind your campaign traffic, with masking and sampling tuned for production.](/build/sveltekit-session-replay)[Web frameworksHow to add Web Experiment to a SvelteKit appTest a page change without a deploy, measured against the same traffic data.](/build/sveltekit-web-experiment)

Reference docs

[Out-of-the-box Marketing Analytics →](https://amplitude.com/docs/analytics/ootb-marketing-analytics)[Browser SDK 2: Track marketing attribution →](https://amplitude.com/docs/sdks/analytics/browser/browser-sdk-2#track-marketing-attribution)[Browser Unified SDK →](https://amplitude.com/docs/sdks/analytics/browser/browser-unified-sdk)[Channels →](https://amplitude.com/docs/data/channels)[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/sveltekit-web-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)
