Astro
Astro with a client script for the browser, and endpoints for the proxy and server-side tracking.
There is no Astro adapter. The core client runs in a bundled client script, and pages without a client router get one pageview per page load. The proxy and server-side tracking run in on-demand endpoints, which need a server adapter.
Install
npm install @spoar/sdk@nextKeys and environment
.env:
PUBLIC_RA_CONFIG='{"project":"example.com","key":"pk_...","endpoint":"/ra"}'
RA_SECRET=sk_...
RA_ENDPOINT=https://api.example.comThe client reads PUBLIC_RA_CONFIG from import.meta.env, which Astro exposes to browser code because of the PUBLIC_ prefix. RA_SECRET has no prefix, so it stays on the server. RA_SECRET and RA_ENDPOINT are names used on this page; the SDK does not read them itself.
A fully static site without a server adapter has no endpoint of its own: set endpoint to https://api.example.com/v2/events and skip the proxy section.
Create the client
src/lib/events.ts:
import type { NoProps } from "@spoar/sdk";
export type Events = {
signup: { plan: "free" | "pro" };
newsletter_subscribed: NoProps;
};src/lib/analytics.ts:
import { createAnalytics } from "@spoar/sdk";
import { clicks, errors, speedInsights } from "@spoar/sdk/plugins";
import type { Events } from "./events";
export const analytics = createAnalytics<Events>({
plugins: [clicks(), speedInsights(), errors()],
});Import it from a <script> in the layout every page uses. Astro bundles the script and runs it in the browser only:
---
const { title } = Astro.props;
---
<html lang="en">
<head>
<title>{title}</title>
</head>
<body>
<slot />
<script>
import "../lib/analytics";
</script>
</body>
</html>Pageviews
Each page load sends a pageview. With <ClientRouter />, navigation happens in the browser through pushState, which the default pageviews plugin follows.
Custom events
In a client script:
<button id="upgrade">Upgrade</button>
<script>
import { analytics } from "../lib/analytics";
document.querySelector("#upgrade")?.addEventListener("click", () => {
analytics.track("signup", { plan: "pro" });
});
</script>For static markup, the clicks() plugin in the client above sends a click event for any element with data-ra-click, plus its data-ra-prop-* attributes:
<button data-ra-click="upgrade" data-ra-prop-plan="pro">Upgrade</button>In a framework island (React, Vue, Svelte), import the same module and call analytics.track.
Proxy endpoint
src/pages/ra.ts serves /ra on your own domain, so ad blockers see a first-party request. Astro does not route files whose name starts with _, so this page uses /ra; any same-origin path works as long as the client's endpoint matches.
import type { APIRoute } from "astro";
import { createProxy } from "@spoar/sdk/proxy";
export const prerender = false;
const proxy = createProxy({
secret: import.meta.env.RA_SECRET,
endpoint: import.meta.env.RA_ENDPOINT,
});
export const POST: APIRoute = ({ request }) => proxy(request);prerender = false makes the endpoint run on demand in a site that is otherwise prerendered. See Proxy.
Server-side tracking
src/lib/server-analytics.ts:
import { createServerAnalytics } from "@spoar/sdk/server";
import type { Events } from "./events";
export const serverAnalytics = createServerAnalytics<Events>({
secret: import.meta.env.RA_SECRET,
endpoint: import.meta.env.RA_ENDPOINT,
});In an on-demand endpoint, pass the incoming request so the visitor's IP and user agent are forwarded:
import type { APIRoute } from "astro";
import { serverAnalytics } from "../../lib/server-analytics";
export const prerender = false;
export const POST: APIRoute = async ({ request }) => {
await serverAnalytics.track("newsletter_subscribed", {}, { request });
return Response.json({ ok: true });
};Every method resolves to { ok, error, accepted, duplicates, failed } and never throws. See Server.
Consent and identity
analytics.consent.grant();
analytics.identify("user_123", { plan: "pro" });
analytics.reset();With consent: "required", events are held until consent.grant(). See Consent and opt-out.