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/unifiedinstalled,PUBLIC_AMPLITUDE_API_KEYin.env,src/scripts/amplitude.tsloaded by<Amplitude />insrc/layouts/Layout.astro, andsrc/middleware.tssettingAstro.locals.user(from your auth helper or the page'ssrc/lib/auth.tsstub). - 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.
npm install @amplitude/unified2. 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.
// 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.tsruns again,initAllruns 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
redirectsinastro.config.mjsorAstro.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=newsletterlands on a bare/pricing, and the visit records no campaign at all, not even first-touchinitial_utm_source. - A redirect on a page rendered on demand,
Astro.redirect("/login")orcontext.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=testURL.@astrojs/nodedoesn't: it answers308with 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:
// 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:
---
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.
---
// 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.
---
import Layout from "../layouts/Layout.astro";
---
<Layout>
<main>
<h1>Pricing</h1>
</main>
</Layout>---
// 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.
- 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. - Confirm the page view event. Open Marketing Analytics, click Customize, and select Page View and Filter. Choose
[Amplitude] Page Viewedas the event. Set a domain filter if the project also receives events from another site, such as your app on a different domain. - 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 Viewedfollowed byProject Createdworks as a first funnel. - 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:
- Network tab. Turn on Preserve log first. The document request for
/plansshould 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-testwhile signed out (with the auth stub, everyone is): it should return 302 withutm_sourcestill in theLocationheader for/login. If your adapter has no preview command, check/accountwithnpm run dev. If either loses its parameters, the fix is step 4, and nothing downstream will work until it is. Then filter forapi2.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 Viewedevent for/pricingshould carryutm_source: build-testinevent_properties, and an identify event should setutm_sourceandinitial_utm_sourceas 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.
- 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.
- 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_startin the Instrumentation Explorer), then one[Amplitude] Page Viewedper page, and the profile showsinitial_utm_source: build-test. - Marketing Analytics. Open Traffic by Campaign for today.
astro-web-analyticsshould 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.
A page view for every heading link or filter click
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.
// -- 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.
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-analyticsdid after they landed on/pricing. - Heatmaps, built from those session replays, show where visitors click on
/pricing, and a segment oninitial_utm_sourcesplits paid from organic. - Web Experiment can test a new
/pricinglayout, launched from the Page Engagement table, with your conversion funnel as the metric. - Guides and surveys can ask visitors whose
utm_mediumisemailwhat brought them back.
One integration; everything else reads from the same events.
Next steps
Reference docs
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.