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

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. 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 to get your project API key. Custom Marketing Analytics views need a Growth or Enterprise plan; compare plans.

1. Install the SDK and add the key

The Quickstart recommends @amplitude/unified for browser apps installed from npm, and the SvelteKit product analytics page uses it, so this setup changes the same src/hooks.client.ts. If you already followed that page, skip to step 2.

Terminal
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
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 or the guides and surveys page, keep it around initAll.

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: {
        // 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: 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.

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
// 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
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
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
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
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
<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
<h1>Pricing</h1>
src/routes/welcome/+page.svelte
<h1>Welcome</h1>
src/routes/account/+page.svelte
<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, [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.

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.

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 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
// -- .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
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, already on from step 2, lets you watch what visitors with utm_campaign: sveltekit-web-analytics did after they landed on /pricing.
  • 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 can test a new /pricing layout, launched from the Page Engagement table, with your conversion funnel as the metric.
  • 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

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.

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.