# Astro

Source: https://docs.mirafive.io/sdks/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](https://docs.mirafive.io/sdks/browser) 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](https://docs.mirafive.io/sdks/script-tag) instead.

## Install

```bash
npm install @mirafive/sdk-astro @mirafive/sdk-browser
```

To read feature flags on the server as well:

```bash
npm install @mirafive/sdk-server
```

Requires 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](https://docs.mirafive.io/keys) for where to find it. Put it in `.env`:

```sh title=".env"
PUBLIC_MIRAFIVE_KEY=mf_…
```

Add the integration, or run `npx astro add @mirafive/sdk-astro`:

```js title="astro.config.mjs"
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](https://docs.mirafive.io/guides/consent): 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. |

> **Warning:** 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

1. Build and deploy. `astro dev` sends nothing unless you pass `dev: true`, and `astro preview` runs on `localhost`, which sends nothing unless you pass `trackLocalhost: true`.
2. In the browser's network tab, look for `POST https://events.mirafive.io/v1/batch/mf_…` answering `202` with `"accepted": 1`. Batches leave 5 seconds after the first event, or when the tab is hidden.
3. 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](https://docs.mirafive.io/guides/verify-and-debug).

## Track events

Import `mirafive` from the client entry in any `<script>` or island and call a verb:

```astro title="src/components/SignupButton.astro"
<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](https://docs.mirafive.io/guides/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](https://docs.mirafive.io/sdks/node) with the secret key:

```ts title="src/pages/api/signup.ts"
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](https://docs.mirafive.io/guides/server-side).

## Identify users

Identifying users needs [full mode](https://docs.mirafive.io/guides/consent#full-mode). Set `mode: 'full'`; the integration adds the `identity()` plugin to the bundle:

```js title="astro.config.mjs"
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:

```ts
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`:

```html
<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](https://docs.mirafive.io/guides/identify-users) and [Consent](https://docs.mirafive.io/guides/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`:

```js title="astro.config.mjs"
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:

```ts
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`:

```astro title="src/pages/checkout.astro"
---
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()` reads `Sec-GPC: 1` and `DNT: 1` from the request and sets `optedOut`: no ids, no segment lookup, no exposure.
- It needs `mirafive()` in `integrations`, which lets `astro:env/server` resolve inside the package.
- On Cloudflare Workers, pass the request's `waitUntil` as 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](https://docs.mirafive.io/sdks/node#feature-flags). See [Feature flags](https://docs.mirafive.io/guides/feature-flags) and [Experiments](https://docs.mirafive.io/guides/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](https://docs.mirafive.io/sdks/browser#api-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:

```text
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.
```
