Astro
Add MIRA FIVE analytics to an Astro site as part of its own bundle, and render feature flags on server-rendered pages.
@mirafive/sdk-astro is an Astro integration. It bundles the browser SDK into your site's own JavaScript, with only the plugins you list, so no third-party script loads. A client entry lets any script or island send events and read flags, and a server entry renders feature flags on pages rendered on demand. For a site you cannot rebuild, use the script tag instead.
Install
npm install @mirafive/sdk-astro @mirafive/sdk-browserTo read feature flags on the server as well:
npm install @mirafive/sdk-serverRequires Astro 7 and Node.js 22.12 or later. @mirafive/sdk-server is needed only for @mirafive/sdk-astro/server.
Set up
You need the website key of a website source (mf_…). See Keys for where to find it. Put it in .env:
PUBLIC_MIRAFIVE_KEY=mf_…Add the integration, or run npx astro add @mirafive/sdk-astro:
import mirafive from '@mirafive/sdk-astro'
import { defineConfig } from 'astro/config'
export default defineConfig({
integrations: [mirafive()],
})That is the whole install. At build time the integration reads PUBLIC_MIRAFIVE_KEY and injects one module into every page. It sends a pageview on load and on every <ClientRouter /> navigation. It works the same for static output and for pages rendered on demand.
The client starts in consentless mode: it sets no cookies, stores nothing on the device and needs no consent banner.
Server-rendered flags also need the secret key of a server source, in the server's environment only:
| Variable | Value | Read by |
|---|---|---|
PUBLIC_MIRAFIVE_KEY | The website key. Public. | The integration, at build time. It is compiled into the page's script. |
MIRAFIVE_SECRET_KEY | The secret key of a server source (mf_…). Server only. | miraFlagsFor(), at runtime, through astro:env/server. |
MIRAFIVE_HOST | Optional. Default https://events.mirafive.io. | miraFlagsFor(), at runtime. The browser uses the host option. |
Never prefix the secret key with PUBLIC_. The build fails when the website key equals MIRAFIVE_SECRET_KEY, and a secretKey option throws. MIRA FIVE marks a secret key that arrives from a browser as exposed, and you have to rotate it.
Verify
- Build and deploy.
astro devsends nothing unless you passdev: true, andastro previewruns onlocalhost, which sends nothing unless you passtrackLocalhost: true. - In the browser's network tab, look for
POST https://events.mirafive.io/v1/batch/mf_…answering202with"accepted": 1. Batches leave 5 seconds after the first event, or when the tab is hidden. - Open the source's live view in MIRA FIVE and find the pageview.
The built page loads the code as <script type="module" src="/_astro/…"> from your own origin, with no reference to cdn.mirafive.io. If the build logs no website key, the injected module is empty and nothing is sent. More in Verify and debug.
Track events
Import mirafive from the client entry in any <script> or island and call a verb:
<button id="signup">Sign up</button>
<script>
import { mirafive } from '@mirafive/sdk-astro/client'
document.getElementById('signup')?.addEventListener('click', () => {
mirafive('track', 'signup_clicked', { plan: 'pro' })
})
</script>Calls made before the client has started are queued on window.mirafive and run once it starts. Inline scripts can call window.mirafive('track', …) directly once the page has loaded. With features: ['autocapture'], clicks, submits and changes are recorded without code. Event names, property limits and revenue are covered in Track events.
Server-side events
The integration does not send events from the server. In an endpoint or a page rendered on demand, use the server SDK with the secret key:
import { Mira } from '@mirafive/sdk-server'
import type { APIRoute } from 'astro'
import { getSecret } from 'astro:env/server'
export const prerender = false
export const POST: APIRoute = async ({ request }) => {
const { userId } = (await request.json()) as { userId: string }
const mira = new Mira({ key: getSecret('MIRAFIVE_SECRET_KEY'), host: getSecret('MIRAFIVE_HOST') })
mira.track('signup', { userId, properties: { plan: 'pro' } })
await mira.flush()
return Response.json({ ok: true })
}userId is your own pseudonymous id for the person, never an email address. flush() never rejects; transport errors are logged with a [mirafive] prefix. For batching, retries, waitUntil and idempotency, see Server-side.
Identify users
Identifying users needs full mode. Set mode: 'full'; the integration adds the identity() plugin to the bundle:
import mirafive from '@mirafive/sdk-astro'
import { defineConfig } from 'astro/config'
export default defineConfig({
integrations: [mirafive({ mode: 'full' })],
})Wire your consent banner to the consent verb, then identify the user:
import { mirafive } from '@mirafive/sdk-astro/client'
// From your consent banner:
mirafive('consent', true) // statistics only
mirafive('consent', { statistics: true, experiments: true, targeting: false }) // by scope
mirafive('consent', false) // forgets everything stored on the device
// After login:
mirafive('identify', user.id, { plan: user.plan })
// After logout:
mirafive('reset')Before a consent answer, full mode stores and sends nothing. A consent call made before the client starts is queued and runs before the landing pageview, so a stored answer replayed on load counts the landing page. A consent manager that knows the answer before any script runs can set window.__mirafive_consent instead.
Inline scripts and consent-manager callbacks that may run before the bundle can put the queue stub first and call window.mirafive:
<script is:inline>
window.mirafive = window.mirafive || function () { (mirafive.q = mirafive.q || []).push(arguments) }
window.mirafive('consent', true)
</script>Do Not Track, Global Privacy Control, window.__mirafive_ignore = true and prerendering send nothing. See Identify users and Consent.
Feature flags
Flags work on every page once the browser has loaded them, and on pages rendered on demand before the first paint.
In the browser
Add 'flags' to features:
import mirafive from '@mirafive/sdk-astro'
import { defineConfig } from 'astro/config'
export default defineConfig({
integrations: [mirafive({ features: ['flags'] })],
})Read flags in the flags listener, which runs when flags load or change:
import { mirafive } from '@mirafive/sdk-astro/client'
mirafive('flags', () => {
const newCheckout = mirafive('flag', 'new-checkout', false)
const limits = mirafive('config', 'checkout-limits', { maxItems: 10 })
document.body.classList.toggle('new-checkout', newCheckout === true)
document.body.dataset.maxItems = String(limits.maxItems)
})flag returns the variant key, or true/false for an on/off flag. Before the client has started, flag and config return their fallback and are not queued, because a replayed read would count an exposure for a value the page never showed.
On the server
On a page rendered on demand, read the visitor's flags with miraFlagsFor(), render with them, and hand them to the browser with MiraFlagsScript:
---
import { miraFlagsFor, MiraFlagsScript } from '@mirafive/sdk-astro/server'
import { bootstrapHeaders } from '@mirafive/sdk-server/flags'
export const prerender = false
// Your own pseudonymous user id, when someone is signed in.
const flags = await miraFlagsFor(Astro, { userId: undefined })
for (const [name, value] of Object.entries(bootstrapHeaders)) {
Astro.response.headers.set(name, value)
}
---
<html lang="en">
<head>
<MiraFlagsScript flags={flags} />
</head>
<body>
<h1>{flags.enabled('new-checkout') ? 'New checkout' : 'Checkout'}</h1>
</body>
</html>MiraFlagsScript renders the <script type="application/json" id="mirafive-flags"> block the browser flags() plugin starts from, so browser reads agree with the server render. The block only carries flags the website reads. Later <ClientRouter /> pages fetch flags in the browser instead.
- The page must render on demand (
export const prerender = false, with an adapter). A bootstrap holds one visitor's answers and must not be built into a static page. - Send
Cache-Control: private, no-store(bootstrapHeaders) from the page's frontmatter. Headers set inside a component may arrive after the response has started. miraFlagsFor()readsSec-GPC: 1andDNT: 1from the request and setsoptedOut: no ids, no segment lookup, no exposure.- It needs
mirafive()inintegrations, which letsastro:env/serverresolve inside the package. - On Cloudflare Workers, pass the request's
waitUntilas the third argument, so background refreshes and exposures outlive the response. Node.js needs nothing.
UserFlags has enabled(key, fallback?), variant(key, fallback?), config(key, fallback), evaluate(key) and bootstrap(), as described in the server SDK. See Feature flags and Experiments.
View transitions
With <ClientRouter />, pageviews() follows each navigation. On back and forward the router changes the URL before it fetches the page, so the injected astro() plugin holds that pageview until astro:page-load and sends it with the new page's title and the right referrer. Without <ClientRouter /> nothing changes. The injected script is a module script, so the router runs it once, not per navigation.
API reference
@mirafive/sdk-astro
mirafive(options?) returns the integration. It is the default and a named export.
| Name | Type | Default | Description |
|---|---|---|---|
key | string | PUBLIC_MIRAFIVE_KEY | The website key. Without one, the build warns and nothing is sent. |
host | string | https://events.mirafive.io | Where the browser sends events. Needs a scheme. |
mode | 'consentless' | 'full' | 'consentless' | 'full' bundles identity() and waits for a consent answer. |
features | ('autocapture' | 'search' | 'flags' | 'experiments')[] | [] | Plugins bundled besides pageviews. search and experiments need mode: 'full'. experiments adds flags. |
dev | boolean | false | Also run under astro dev, sending from localhost there. |
trackLocalhost | boolean | false; true under astro dev with dev: true | Also send from localhost, 127.*, [::1], *.local and file:. |
It throws while the config loads for a secretKey option, an unknown mode or feature, a host without a scheme, or search or experiments in consentless mode. The build fails when the key equals MIRAFIVE_SECRET_KEY. Types: MirafiveOptions, Feature.
@mirafive/sdk-astro/client
| Export | Description |
|---|---|
mirafive(verb, ...args) | Calls the client by verb, typed per verb: track, pageview, flush, consent, identify, reset, anonymousId, search, flag, config, flags (a listener, like onFlags), flagProperties (like setFlagProperties), and every other client method by name. Queued on window.mirafive until the client runs, returning undefined meanwhile; flag and config return their fallback instead and are never queued. mirafive('anonymousId', (id) => …) answers to the callback, also when queued. Returns undefined on the server. |
astro() | The browser SDK plugin the injected script ends with. Installs window.mirafive, runs its queue, and holds a <ClientRouter /> pageview until astro:page-load. |
MirafiveCommand | The type of mirafive. |
The client's methods are listed in the browser SDK reference.
@mirafive/sdk-astro/server
| Export | Description |
|---|---|
miraFlagsFor(context, unit?, options?) | Promise<UserFlags>. context is Astro or an APIContext. unit takes userId, anonymousId, properties, consent and optedOut. options.waitUntil is this request's waitUntil. One MiraFlags per server process, from MIRAFIVE_SECRET_KEY and MIRAFIVE_HOST, with a Mira on the same key that sends the exposures of experiments counted on the server. Sec-GPC: 1 or DNT: 1 sets optedOut. |
MiraFlagsScript | Component with a flags prop (a UserFlags). Renders flags.bootstrap(), the mirafive-flags block. |
MiraFlagsScriptProps | Its props type. |
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| Nothing arrives | astro dev without dev: true, or astro preview on localhost without trackLocalhost: true. The build warned no website key: set PUBLIC_MIRAFIVE_KEY and rebuild. Do Not Track, Global Privacy Control or __mirafive_ignore is on. In mode: 'full', no consent answer yet. The site's origin is not allowed on the source. |
403 secret_key_in_path or 403 website_key_as_bearer | The keys are swapped. PUBLIC_MIRAFIVE_KEY is the website key, MIRAFIVE_SECRET_KEY the secret key of a server source. |
403 origin_not_allowed | Add the site's domain to the source in MIRA FIVE. |
| A flag always returns its fallback | The flag is not in this source's flags. Flags have not loaded yet: read them in the flags listener. The page has no MiraFlagsScript and reads before the fetch. MIRAFIVE_SECRET_KEY is missing on the server: look for [mirafive] no key. evaluate(key) says why. |
| Back or forward pageviews carry the old title | The page does not run the injected script, for example a custom setup without astro(). |
Cannot find module 'astro:env/server' | @mirafive/sdk-astro/server is used without mirafive() in integrations. |
Set up with an AI agent
Paste this into your coding agent:
Add MIRA FIVE analytics to this Astro site with @mirafive/sdk-astro.
Docs: https://docs.mirafive.io/sdks/astro.md
1. Install @mirafive/sdk-astro and @mirafive/sdk-browser with the project's package manager.
Add @mirafive/sdk-server only if pages rendered on demand read feature flags on the server.
2. Put the website key in .env as PUBLIC_MIRAFIVE_KEY=mf_… and in the build environment.
If flags are read on the server, put the secret key of a server source in MIRAFIVE_SECRET_KEY
in the server's runtime environment only; never prefix it with PUBLIC_.
3. In astro.config.*: import mirafive from '@mirafive/sdk-astro' and add mirafive() to integrations
(or run npx astro add @mirafive/sdk-astro). Do not add a <script> tag for MIRA FIVE anywhere;
pageviews, including <ClientRouter /> navigations, are automatic.
4. Track from scripts and islands with import { mirafive } from '@mirafive/sdk-astro/client' and
mirafive('track', name, properties).
5. For flags: mirafive({ features: ['flags'] }), then read them in mirafive('flags', () => ...) with
mirafive('flag', key, fallback). For server-rendered flags on a page with
export const prerender = false: const flags = await miraFlagsFor(Astro, { userId }) and
<MiraFlagsScript flags={flags} /> in the head, both from '@mirafive/sdk-astro/server', and set
bootstrapHeaders from '@mirafive/sdk-server/flags' on Astro.response.headers in the frontmatter.
6. Keep the default consentless mode, which needs no consent banner. Only if the site already has a
consent banner and needs user ids: mirafive({ mode: 'full' }) and, in the banner's handlers,
mirafive('consent', true) or mirafive('consent', false).
7. Verify: run the build and check that the HTML loads no MIRA FIVE script from another origin.
Deploy (or set trackLocalhost: true and preview) and check the network tab for
POST https://events.mirafive.io/v1/batch/<key> answering 202. Report what you changed.
Do not add other analytics libraries, cookies or consent banners.