How to add web analytics to an Astro site

Configure the Amplitude Unified SDK so every Astro page load and <ClientRouter /> navigation becomes one page view, every visit carries its UTM and referrer data, and sessions feed bounce rate and session length. Then keep campaign parameters alive through prerendered and on-demand redirects, and open the Marketing Analytics views that read them. About 15 minutes, all copy-paste.

  • Astro 6+
  • Static or on-demand
  • Unified SDK 1.x
  • 15 min

Chris Tam · Last updated 19 October 2026

Astro is built for content: blogs, docs, and marketing sites that ship as static HTML. Web analytics tells you where those readers come from, which campaigns bring them, and which pages keep them reading.

In this tutorial, you'll configure Amplitude for traffic data, keep campaign parameters through Astro redirects, and set up the channel and campaign views. It builds on the Astro product analytics page.

What you'll build

An Astro site that sends [Amplitude] Page Viewed once per page, whether a link triggers a full page load or a <ClientRouter /> swap, 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 you moved or a page that redirects signed-out visitors. By the end you'll see a test campaign in your event stream and a Marketing Analytics view configured to read it.

Prerequisites

  • The setup from How to add product analytics to an Astro site: @amplitude/unified installed, PUBLIC_AMPLITUDE_API_KEY in .env, src/scripts/amplitude.ts loaded by <Amplitude /> in src/layouts/Layout.astro, and src/middleware.ts setting Astro.locals.user (from your auth helper or the page's src/lib/auth.ts stub).
  • For the sign-in guard in step 4, an adapter and at least one page rendered on demand. A fully prerendered site skips src/pages/account.astro; the other redirects still work.
  • 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.

No Amplitude account yet? 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 installs @amplitude/unified for browser apps that use npm, and the Astro product analytics page uses it, so this setup changes the same src/scripts/amplitude.ts. If you already followed that page, the SDK is installed and .env holds PUBLIC_AMPLITUDE_API_KEY; skip to step 2. Otherwise install it, then add the key to .env as that page's step 1 shows.

Terminal
npm install @amplitude/unified

2. Configure autocapture for traffic data

Attribution, page views, and sessions are on by default, and the base page already names them in src/scripts/amplitude.ts, so nobody turns one off later without knowing what it feeds. This page changes one option: pageViews gets trackHistoryChanges. Page URL enrichment is on by default too, so there's nothing to add for it.

This is the complete file. The first comment names the lines other Astro pages add here; if you followed any of them, keep those lines and change only pageViews.

src/scripts/amplitude.ts
// Keep lines other Astro pages added here: debugMode and privacyConfig
// (/build/astro-session-replay); the amplitude-cookies import, the device ID
// handoff lines, the deviceId spread, and the experiment block
// (/build/astro-feature-flags); the engagement block with renderCssInDom
// (/build/astro-guides-surveys); the optional webExperiment plugin lines below
// the amplitudeReady export (/build/astro-web-experiment). This page changes only
// pageViews. Import this module only from <script> tags, never from component
// frontmatter.
import * as amplitude from "@amplitude/unified";

export * from "@amplitude/unified";

const apiKey = import.meta.env.PUBLIC_AMPLITUDE_API_KEY;

if (!apiKey) {
  console.warn("PUBLIC_AMPLITUDE_API_KEY is not set; analytics is off.");
}

// Resolves once initAll finishes. Feature flags and guides and surveys
// wait on it; nothing on this page needs to.
export const amplitudeReady: Promise<void> = apiKey
  ? amplitude
      .initAll(apiKey, {
        analytics: {
          autocapture: {
            // UTM, referrer, and click ID capture. Feeds Traffic by Channel and Campaign.
            attribution: true,
            // [Amplitude] Page Viewed on every page load and every path change.
            // "pathOnly" ignores changes to only the query string or #hash.
            pageViews: { trackHistoryChanges: "pathOnly" },
            // [Amplitude] Start Session and End Session. Feeds bounce rate and session length.
            sessions: true,
            formInteractions: true,
            fileDownloads: true,
            elementInteractions: true,
          },
        },
        sessionReplay: {
          // Every session while you build; about 1% in production.
          sampleRate: import.meta.env.DEV ? 1 : 0.01,
        },
      })
      .catch((error) => {
        console.warn("Amplitude failed to initialize", error);
      })
  : Promise.resolve();

Why. The module runs in the browser, so the SDK reads campaign data from the URL the browser actually loaded. Anything that happened before that, such as a redirect, already decided which URL that is. That's why step 4 fixes redirects and not this file. Keep initAll out of component frontmatter, middleware, and endpoints; they run on the server or during the build.

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

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 events sent from the landing URL. Later pages and custom events such as Project Created don't carry them, so segment those on the user properties. If the Verify 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.

The rest of autocapture already suits a content site: formInteractions records newsletter and contact form starts and submits, fileDownloads records clicks on links to files such as a PDF guide, and elementInteractions records clicks on calls to action.

3. Let autocapture count page views

There's no page view code on this site, and there shouldn't be. With pageViews on, the SDK sends [Amplitude] Page Viewed when it initializes and again when the URL changes in the browser. Each event carries [Amplitude] Page Path, [Amplitude] Page Title, [Amplitude] Page URL, [Amplitude] Page Location, and [Amplitude] Page Counter. That covers both ways an Astro site navigates:

  • Without <ClientRouter />, every link is a full page load. src/scripts/amplitude.ts runs again, initAll runs again, and the new page view is sent on init. The device ID, session, and first-touch campaign carry over, because the SDK stores them in the browser. Internal links don't overwrite the campaign either: by default the SDK excludes your own domain as a referrer.
  • With <ClientRouter /> in your layout's <head>, the router swaps the page in place and pushes the new URL to history. The module doesn't run again, and the SDK sends the page view on the URL change. Back and forward buttons count the same way, but their page view carries the previous page's [Amplitude] Page Title, because the browser fires the history event before the router swaps in the new page. On a <ClientRouter /> site, break Page Engagement down by [Amplitude] Page Path, not title.

trackHistoryChanges: "pathOnly" sends a page view when the path changes, such as /blog to /blog/launch-notes, and skips changes to only the query string or hash. With the default, "all", any URL change counts, so a reader clicking down a docs page's table of contents (#install, #configure) or a blog index that rewrites ?tag=astro would add a page view per click, which inflates page views per session and skews entry and exit pages. On a site without <ClientRouter />, page loads carry most of the count and the option only touches those in-page URL changes. Use "all" if your query strings mark different pages, such as /blog?page=2.

Keep internal links on the router's default history behavior. A link with data-astro-history="replace", or navigate(url, { history: "replace" }) from astro:transitions/client, rewrites the current history entry instead of pushing a new one; if one of those navigations shows no page view in Verify, switch it back to the default.

Don't add an astro:page-load listener that calls track('Page Viewed'). It produces a second, differently named page view on every navigation, and Marketing Analytics only counts the one it's configured to read.

4. Keep UTM parameters through redirects

The SDK can only record campaign data that's in the URL, and it only records a new campaign when the referrer isn't your own domain. Astro redirects break both, in different ways depending on how the page renders:

  • A redirect on a prerendered page, either an entry in redirects in astro.config.mjs or Astro.redirect() in a page that isn't rendered on demand, builds to an HTML file with a <meta http-equiv="refresh"> tag and a fixed target URL. The query string is gone on arrival, and that file has no layout, so the SDK never runs on it. A newsletter link to /plans?utm_source=newsletter lands on a bare /pricing, and the visit records no campaign at all, not even first-touch initial_utm_source.
  • A redirect on a page rendered on demand, Astro.redirect("/login") or context.redirect("/login") in middleware, is a real HTTP redirect, but it goes exactly where you point it, and a plain path has no query string.
  • With an adapter, configured redirects can be written to your host's redirect config instead. Whether those keep the query string depends on the host, so test one with a ?utm_source=test URL. @astrojs/node doesn't: it answers 308 with the bare path.

Amplitude's docs call the general case out for single-page apps: UTM parameters can drop during SPA redirects.

For redirects that run on the server, 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",
];

// Astro.redirect("/login") on its own drops ?utm_source=...
// Same-origin paths only.
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 + target.hash;
}

Use it in every guard on a page rendered on demand, passing Astro.url. This one reads the user the base page's middleware put on locals:

src/pages/account.astro
---
import Layout from "../layouts/Layout.astro";
import { withCampaign } from "../lib/campaign";

// Rendered on demand, so the guard sees the request. Needs an adapter.
export const prerender = false;

if (!Astro.locals.user) {
  return Astro.redirect(withCampaign("/login", Astro.url));
}
---

<Layout>
  <main>
    <h1>Account</h1>
    <p>Your account</p>
  </main>
</Layout>

The same goes for context.redirect() in middleware: pass withCampaign(path, context.url). If you send people on from the browser after sign-in, wrap the path the same way: navigate(withCampaign("/welcome", new URL(location.href))) with navigate from astro:transitions/client, or location.assign(...) on a site without the router.

A URL you moved on a prerendered site needs a different fix, because the meta refresh page can't read the query string. Replace the redirects entry with a small page that redirects in the browser and keeps the query string. A redirects entry for the same path wins over a page file, in astro dev, in a static build, and with an adapter, so delete the /plans entry from astro.config.mjs or the page never runs.

src/pages/plans.astro
---
// Old campaign URL, moved to /pricing. Replaces "/plans" in the redirects
// option of astro.config.mjs, which drops ?utm_source=... in a static build.
// No layout on purpose: the SDK runs once, on /pricing.
---

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="robots" content="noindex" />
    <!-- Without this, /pricing sees your own domain as the referrer, and the
         SDK skips the campaign. -->
    <meta name="referrer" content="no-referrer" />
    <title>Moved to /pricing</title>
    <script is:inline>
      location.replace("/pricing" + location.search + location.hash);
    </script>
  </head>
  <body>
    <a href="/pricing">This page moved to /pricing.</a>
  </body>
</html>

The script needs is:inline: it must run before anything else on that page and import nothing. It also keeps the whole query string, not only campaign keys, which is what a moved URL should do. The trade-off is the original referrer: a visitor from a search result with no UTM parameters arrives at /pricing as direct traffic. If your adapter writes real HTTP redirects to your host's config and the Verify check shows the query string survives, keep the redirects entry instead, since an HTTP redirect also keeps the referrer.

Why. Event property tracking re-reads campaign parameters on page load and on every in-page navigation, so whatever is in the URL after the redirect is what gets attached to the events sent from that URL. For the same reason, don't strip UTM parameters from the address bar with history.replaceState to tidy the URL.

Give the guard and the moved URL somewhere to land. Skip either stub if you already have the page.

src/pages/pricing.astro
---
import Layout from "../layouts/Layout.astro";
---

<Layout>
  <main>
    <h1>Pricing</h1>
  </main>
</Layout>
src/pages/login.astro
---
// Stub until you add your sign-in form.
import Layout from "../layouts/Layout.astro";
---

<Layout>
  <main>
    <h1>Sign in</h1>
  </main>
</Layout>

Check every redirect on your site, configured or in code, with a ?utm_source=test URL before you trust your channel numbers.

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, such as your app on a different domain.
  3. Define a conversion funnel. Under Conversion Funnel, choose the events that make up your funnel. If you followed the Astro 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, which is where a blog or docs site finds its best and worst landing pages. 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.

Verify it worked

Redirects on prerendered pages only behave like production in a production build, so run npm run build and npm run preview. Open http://localhost:4321/plans?utm_source=build-test&utm_medium=email&utm_campaign=astro-web-analytics in a fresh private window, click through two pages (type another page's path into the address bar if your stubs have no links), then check in this order:

  1. Network tab. Turn on Preserve log first. The document request for /plans should load the small redirect page, followed by a document request for /pricing?utm_source=build-test&utm_medium=email&utm_campaign=astro-web-analytics, and the address bar should keep those parameters. With an adapter, also open /account?utm_source=build-test while signed out (with the auth stub, everyone is): it should return 302 with utm_source still in the Location header for /login. If your adapter has no preview command, check /account with npm run dev. 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.

On localhost the SDK doesn't treat your own domain as an internal referrer, so the first internal link sends a second identify that clears utm_source, and a missing no-referrer tag won't show up. Repeat the /plans check on a deployed preview URL before you trust it: there, clicking through sends no second identify, and the profile keeps utm_source: build-test.

  1. 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.
  2. User lookup. Go to Users > User Profiles and search your device ID (the Instrumentation Explorer shows it as device_id). Within ten seconds to a minute, the event stream shows [Amplitude] Start Session (session_start in the Instrumentation Explorer), then one [Amplitude] Page Viewed per page, and the profile shows initial_utm_source: build-test.
  3. Marketing Analytics. Open Traffic by Campaign for today. astro-web-analytics should appear as a campaign.

Using <ClientRouter />? In the same window, click two internal links, use the back button once, and click a #heading link on a long page. The Instrumentation Explorer should show one [Amplitude] Page Viewed per path change and none for the heading link.

To watch payloads in the console, add logLevel: amplitude.Types.LogLevel.Debug to the analytics block in development.

Troubleshooting

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 follow every redirect: a redirects entry or Astro.redirect() on a prerendered page builds to a meta refresh with a fixed URL, and Astro.redirect() or context.redirect() on demand drops the query unless you wrap the path with withCampaign. A navigate() or location.assign() 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.

UTM values on events, but no initial_utm_source on the profile

The visitor arrived through a redirect page on your own domain, so the referrer is your domain, and by default the SDK doesn't treat a same-domain referrer as a new campaign. Keep the <meta name="referrer" content="no-referrer" /> line in any browser-side redirect page like src/pages/plans.astro.

NoAdapterInstalled during the build

src/pages/account.astro renders on demand. Add an adapter (for example npx astro add node) or delete the file; the /plans fix doesn't need one.

Two page view events per navigation

An astro:page-load listener or a page view helper is running alongside autocapture. Delete it. If both events are [Amplitude] Page Viewed, check that only src/scripts/amplitude.ts calls initAll and that nothing loads it with an is:inline script.

The page writes to the hash or the query string, and trackHistoryChanges is "all". Set pageViews: { trackHistoryChanges: "pathOnly" } as in step 2.

Every page in Page Engagement has the same title

Your layout hardcodes <title>, so every [Amplitude] Page Title is the same. Give src/layouts/Layout.astro a title prop, render <title>{title}</title>, and pass it from each page, or break the view down by [Amplitude] Page Path under Customize > Breakdown. If your layout has no frontmatter yet (the create astro starter's doesn't), add the --- fences at the top of the file. On a <ClientRouter /> site, back and forward views also carry the previous page's title, so use [Amplitude] Page Path there.

The same page shows up twice in Page Engagement

Visitors reach it as both /blog and /blog/, and each is its own [Amplitude] Page Path. Pick one form with trailingSlash in astro.config.mjs and link to it consistently. Your host decides trailing slashes for prerendered pages, so check its settings too.

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.

Requests fail with ERR_BLOCKED_BY_CLIENT

An ad blocker is dropping the call. Route Amplitude traffic through a proxy on your own domain and point every endpoint at it: serverUrl (events) and remoteConfig.serverUrl (remote configuration) in the analytics block, plus trackServerUrl and configServerUrl in the sessionReplay block.

Full code reference

Every file this tutorial adds or changes, in one block, the version to hand to an agent. 6 files. Shared files start with a comment naming the lines other Astro pages add there; merge into your file and keep those lines. Unchanged from earlier pages; keep your versions: .env, src/layouts/Layout.astro, src/components/Amplitude.astro, src/middleware.ts, src/env.d.ts, src/lib/auth.ts, src/components/SignOutButton.astro, src/components/CreateProjectForm.astro, src/pages/projects/new.astro.

6 files
// -- src/scripts/amplitude.ts --
// Keep lines other Astro pages added here: debugMode and privacyConfig
// (/build/astro-session-replay); the amplitude-cookies import, the device ID
// handoff lines, the deviceId spread, and the experiment block
// (/build/astro-feature-flags); the engagement block with renderCssInDom
// (/build/astro-guides-surveys); the optional webExperiment plugin lines below
// the amplitudeReady export (/build/astro-web-experiment). This page changes only
// pageViews. Import this module only from <script> tags, never from component
// frontmatter.
import * as amplitude from "@amplitude/unified";

export * from "@amplitude/unified";

const apiKey = import.meta.env.PUBLIC_AMPLITUDE_API_KEY;

if (!apiKey) {
  console.warn("PUBLIC_AMPLITUDE_API_KEY is not set; analytics is off.");
}

// Resolves once initAll finishes. Feature flags and guides and surveys
// wait on it; nothing on this page needs to.
export const amplitudeReady: Promise<void> = apiKey
  ? amplitude
      .initAll(apiKey, {
        analytics: {
          autocapture: {
            // UTM, referrer, and click ID capture. Feeds Traffic by Channel and Campaign.
            attribution: true,
            // [Amplitude] Page Viewed on every page load and every path change.
            // "pathOnly" ignores changes to only the query string or #hash.
            pageViews: { trackHistoryChanges: "pathOnly" },
            // [Amplitude] Start Session and End Session. Feeds bounce rate and session length.
            sessions: true,
            formInteractions: true,
            fileDownloads: true,
            elementInteractions: true,
          },
        },
        sessionReplay: {
          // Every session while you build; about 1% in production.
          sampleRate: import.meta.env.DEV ? 1 : 0.01,
        },
      })
      .catch((error) => {
        console.warn("Amplitude failed to initialize", error);
      })
  : Promise.resolve();

// -- 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",
];

// Astro.redirect("/login") on its own drops ?utm_source=...
// Same-origin paths only.
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 + target.hash;
}

// -- src/pages/account.astro --
---
import Layout from "../layouts/Layout.astro";
import { withCampaign } from "../lib/campaign";

// Rendered on demand, so the guard sees the request. Needs an adapter.
export const prerender = false;

if (!Astro.locals.user) {
  return Astro.redirect(withCampaign("/login", Astro.url));
}
---

<Layout>
  <main>
    <h1>Account</h1>
    <p>Your account</p>
  </main>
</Layout>

// -- src/pages/plans.astro --
---
// Old campaign URL, moved to /pricing. Replaces "/plans" in the redirects
// option of astro.config.mjs, which drops ?utm_source=... in a static build.
// No layout on purpose: the SDK runs once, on /pricing.
---

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="robots" content="noindex" />
    <!-- Without this, /pricing sees your own domain as the referrer, and the
         SDK skips the campaign. -->
    <meta name="referrer" content="no-referrer" />
    <title>Moved to /pricing</title>
    <script is:inline>
      location.replace("/pricing" + location.search + location.hash);
    </script>
  </head>
  <body>
    <a href="/pricing">This page moved to /pricing.</a>
  </body>
</html>

// -- src/pages/pricing.astro --
---
import Layout from "../layouts/Layout.astro";
---

<Layout>
  <main>
    <h1>Pricing</h1>
  </main>
</Layout>

// -- src/pages/login.astro --
---
// Stub until you add your sign-in form.
import Layout from "../layouts/Layout.astro";
---

<Layout>
  <main>
    <h1>Sign in</h1>
  </main>
</Layout>

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/astro-product-analytics first, or
confirm this Astro site already has its setup; create its src/lib/auth.ts
stub, src/middleware.ts, and src/pages/projects/new.astro if they're
missing. Then set up Amplitude web analytics by following
https://amplitude.com/build/astro-web-analytics exactly. Merge into existing
shared files (src/scripts/amplitude.ts) and never delete lines another
Amplitude Build page added; each file's first comment names them, including
any debugMode or privacyConfig lines, the experiment block, and Web
Experiment plugin lines. In src/scripts/amplitude.ts change only
analytics.autocapture.pageViews, to { trackHistoryChanges: "pathOnly" }, and
keep initAll called once there. Never import the SDK in component
frontmatter, middleware, or endpoints, and don't add an astro:page-load page
view listener. Add the withCampaign helper in src/lib/campaign.ts with the
page's 6 UTM and 11 click ID keys. Find every Astro.redirect() and
context.redirect() in pages rendered on demand and middleware, and every
navigate() or location.assign() after sign-in, and make each one carry the
UTM and click ID parameters from Astro.url, context.url, or location.href.
Find every entry in the redirects option of astro.config.mjs and every
Astro.redirect() on a prerendered page, list them for me, and replace each
static one with a page like the page's src/pages/plans.astro (is:inline
location.replace keeping location.search and location.hash, and a
no-referrer meta tag), unless I tell you my host's redirects keep the
query string. When you replace one, delete the matching redirects entry;
the entry wins over the page. Add the page's demo files so the Verify steps run:
src/pages/plans.astro (to /pricing), src/pages/account.astro (prerender =
false, redirects to /login when Astro.locals.user is null), and stub pages
for /pricing and /login if they don't exist. If the site has no adapter,
skip src/pages/account.astro 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: astro-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.