Astro analytics in one prompt
undefined
Paste the prompt below into your coding agent (Claude Code, Cursor, Codex or any agent that can make HTTP requests) in your Astro project. It gets an AgentFirstStack key, creates the site, serves the tracker from your own domain through a small proxy, adds it to your base layout, and checks the live site until the setup check passes. Navigation with Astro's <ClientRouter /> is counted without extra code. The steps below show what it does. For other tools and platforms, see add analytics with an AI agent.
The prompt
Add analytics to this Astro site with AgentFirstStack. It's free, open-source analytics built for agents, and you can do all of it yourself: there is no signup form, and no email or verification is needed. 1. Read https://agentfirststack.com/llms.txt and https://analytics.agentfirststack.com/llms.txt. 2. If .env already has AGENTFIRSTSTACK_API_KEY, use it and read AGENTFIRSTSTACK.md. Otherwise get a key with one request: curl -X POST https://agentfirststack.com/v1/signup Save api_key in .env as AGENTFIRSTSTACK_API_KEY (never with a PUBLIC_ prefix) and in the host's production environment variables. Name the project after the domain: PATCH https://agentfirststack.com/v1/me. 3. Create a site for the production domain and read GET /v1/sites/:id/install?framework=astro. Serve the tracker from the site's own domain: with an adapter, add an on-demand endpoint at src/pages/a/[...path].ts that forwards to analytics.agentfirststack.com with the site's x-afs-proxy-key (from first_party_proxy.proxy_key, stored as the server-only env var AFS_PROXY_KEY); on a fully static build, use the host's proxy (a Cloudflare Worker route, a Netlify Edge Function, vercel.json or nginx). Then add <script is:inline defer src="/a/s.js" data-site="..."> to the <head> of the base layout (use /_a/s.js with a host-level proxy). 4. Create goals for sign-ups, purchases and the main buttons. Send conversions only the server knows about as server events. 5. Add an email destination, a weekly report, and alerts for a traffic drop and for no data. 6. Deploy (or tell me to), open the site once, then run verify and fix what it reports. 7. Write the site id and the env var names (never the values) in AGENTFIRSTSTACK.md, and create a claim link for me with POST https://agentfirststack.com/v1/claim-link. 8. Run GET https://agentfirststack.com/v1/setup?services=analytics and work through its next_steps until it says "ready": true. When you're done, tell me what you set up, how you verified it, and give me the claim link.
Why the tracker goes through your own domain
Ad blockers block requests to analytics.agentfirststack.com. Served from your own domain, the script and its collect endpoint are first-party, so those visitors are counted too. The tracker posts to the path it was loaded from, so /a/s.js sends its events to /a/e with no extra configuration.
The proxy has to add one header, x-afs-proxy-key. It's the site's own secret (first_party_proxy.proxy_key in the install answer): keep it in a server-only environment variable, never in the page. The collector trusts the visitor IP in X-Forwarded-For, and location headers, only from requests that carry it, because anyone can send those headers. Without it, every visitor arrives with your server's address and merges into one, and verify reports proxy_key.
With an adapter: an endpoint in your project
Astro ignores files and folders whose names start with _ (Astro's routing docs: "Files with the _ prefix won't be recognized by the router"), so this uses /a/ instead of the /_a/ prefix in the API's examples. The tracker works under any prefix. The endpoint is rendered on demand, so it needs an adapter (Node, Netlify, Vercel or Cloudflare):
// src/pages/a/[...path].ts
import type { APIRoute } from "astro";
import { getSecret } from "astro:env/server";
export const prerender = false;
const A = "https://analytics.agentfirststack.com";
export const ALL: APIRoute = async ({ request, url, clientAddress }) => {
const headers = new Headers(request.headers);
for (const k of [...headers.keys()]) if (k.startsWith("x-afs-")) headers.delete(k); // never pass on visitor-sent values
headers.set("x-afs-proxy-key", getSecret("AFS_PROXY_KEY") ?? "");
headers.set("x-forwarded-for", clientAddress);
const country = request.headers.get("cf-ipcountry"); // present when Cloudflare is in front
if (country) headers.set("x-afs-geo-country", country);
headers.delete("cookie");
headers.delete("host");
const res = await fetch(A + url.pathname + url.search, {
method: request.method,
headers,
body: request.method === "POST" ? await request.text() : undefined,
});
const out = new Headers(res.headers);
out.delete("content-encoding"); // fetch has already decoded the body
out.delete("content-length");
return new Response(res.body, { status: res.status, headers: out });
};
Location: on Vercel the request carries Vercel's x-vercel-ip-* headers, which the endpoint passes on and the collector reads. Behind Cloudflare it passes the country. On other hosts the collector only sees your server, so country and city show your server's location; for exact locations there, use a Cloudflare Worker route or a Netlify Edge Function below.
Fully static: proxy at the host
By default an Astro site is fully prerendered, and middleware only runs at build time for prerendered pages, so the proxy belongs to the host. GET /v1/sites/:id/install returns configs filled in with the key for a Cloudflare Worker route, vercel.json, netlify.toml and nginx, all on /_a/*. The Cloudflare Worker route is the best of them when your domain is on Cloudflare: it sends the visitor's exact country, region and city from Cloudflare's edge. On Netlify, an Edge Function does the same with Netlify's geo data:
// netlify/edge-functions/afs.ts
import type { Config, Context } from "@netlify/edge-functions";
export default async (request: Request, context: Context) => {
const url = new URL(request.url);
const headers = new Headers(request.headers);
for (const k of [...headers.keys()]) if (k.startsWith("x-afs-")) headers.delete(k);
headers.set("x-afs-proxy-key", Netlify.env.get("AFS_PROXY_KEY") ?? "");
headers.set("x-forwarded-for", context.ip);
const geo = { country: context.geo?.country?.code, "region-code": context.geo?.subdivision?.code,
city: context.geo?.city, timezone: context.geo?.timezone, lat: context.geo?.latitude, lon: context.geo?.longitude };
for (const [k, v] of Object.entries(geo)) if (v != null) headers.set("x-afs-geo-" + k, encodeURIComponent(String(v)));
headers.delete("cookie");
return fetch("https://analytics.agentfirststack.com" + url.pathname + url.search, {
method: request.method,
headers,
body: request.method === "POST" ? request.body : undefined,
});
};
export const config: Config = { path: "/_a/*" };
Give AFS_PROXY_KEY the Functions scope in Netlify, or edge functions can't read it.
The tag in your layout
Most Astro sites share one layout, often src/layouts/Layout.astro. Put the tag in its <head>:
<head>
<!-- is:inline: Astro leaves the tag as written instead of bundling it -->
<script is:inline defer src="/a/s.js" data-site="site_..."></script>
</head>
Astro processes and bundles <script> tags that have no attributes other than src. This one has defer and data-site, which already stops that, and is:inline makes it explicit. The site id is public; only the proxy key and API key are secrets. Astro only exposes PUBLIC_ variables to client code, so don't prefix either.
ClientRouter navigation
With <ClientRouter /> (called <ViewTransitions /> before Astro 5), pages change without a full load. Astro's router updates the address with history.pushState, and the tracker wraps pushState and listens for popstate, so every navigation, back and forward included, is a page view.
The tag itself survives navigation: Astro's docs say scripts already in the head "are left in if they exist on the new page", and the tracker ignores a second load anyway. Don't add data-astro-rerun to it, and don't send page views from an astro:page-load listener; both would count pages twice.
Server events and AI crawlers
With an adapter, send conversions the browser can't see from an endpoint or Action, with the visitor id the page sends along (window.afsa.visitorId()):
await fetch(`https://analytics.agentfirststack.com/v1/sites/${import.meta.env.AFS_SITE}/events`, {
method: "POST",
headers: { authorization: `Bearer ${getSecret("AGENTFIRSTSTACK_API_KEY")}`, "content-type": "application/json" },
body: JSON.stringify({ events: [{ name: "signup", visitor_id: visitorId }] }),
});
Bots don't run the tracker, so AI crawler visits are reported from the server: in src/middleware.ts, for requests whose user agent matches the prefilter from GET /v1/crawlers, POST /v1/sites/:id/crawls without waiting for the answer. That runs per request only for on-demand pages. On a fully static site, report crawlers from the host's edge (the same Worker or Edge Function), or skip it and note that in AGENTFIRSTSTACK.md.
Check it on the live site
- Open the production site once in a normal browser. Headless browsers are filtered out unless the visit is marked as test traffic with
?afsa_debug=1. POST https://analytics.agentfirststack.com/v1/sites/:id/verifyfetches your homepage, finds the tag, fetches/a/s.jsthrough your proxy to make sure it serves the tracker, checks your Content-Security-Policy and whether events arrived with the proxy key, and lists fixes.GET https://agentfirststack.com/v1/setup?services=analytics, until"ready": true.
The tag is in your static HTML, so verify also proves the domain from the homepage; no DNS record needed. If you add the tag manager later, proxy it the same way under its own prefix, with the configs and its own key from GET /v1/containers/:id/install?proxy=1.
FAQ
Why /a/ and not /_a/ like the API's examples?
Astro doesn't route files or folders whose names start with an underscore, so src/pages/_a/ would never answer. The tracker works under any path prefix, and host-level proxies (Worker, Netlify, nginx) can keep /_a/.
Do I need anything for View Transitions?
No. The ClientRouter navigates with history.pushState, which the tracker already counts. Extra astro:page-load code would double-count.
My site is fully static. Can I still proxy?
Yes, at the host: a Cloudflare Worker route, a Netlify Edge Function, a vercel.json route or nginx. Astro doesn't need to know about it.
Can I skip the proxy and paste the direct snippet?
You can: <script is:inline defer src="https://analytics.agentfirststack.com/s.js" data-site="..."> works on any host. Visitors with an ad blocker won't be counted, though.
Does the tracker use cookies?
No. By default a visitor is a hash of a daily salt, the site, the IP address and the user agent. Nothing is stored in the browser unless you switch the site to persistent mode.
Related
- AnalyticsAdd analytics with an AI agentThe prompt, the checks, and every tool and platform.
- AnalyticsNext.js analytics in one promptApp Router, Pages Router, proxy.ts and Vercel.
- AnalyticsAdd analytics with Claude CodeOne prompt in the terminal, then questions over MCP.
- AnalyticsAdd analytics with CursorAgent mode, .cursor/mcp.json and the setup check.
- AnalyticsWordPress analytics in one promptfunctions.php, header plugins and form hooks.
- All servicesOne MCP server for every serviceAnalytics, tags, mail and replay tools on one key.
- AnalyticsGoogle Analytics MCP comparedRead GA4, or let the agent create the analytics.
- ReplaySession replay for AI agentsFind out why visitors drop off.