# Browser (JavaScript)

Source: https://docs.mirafive.io/sdks/browser

> Add MIRA FIVE analytics and feature flags to any bundled frontend with @mirafive/sdk-browser, one small import per feature.

`@mirafive/sdk-browser` is the JavaScript client for any frontend you bundle (Vite, webpack, esbuild, Rollup) without a MIRA FIVE framework package: Svelte, Solid, Angular, Lit or plain TypeScript. It is also the client the [React](https://docs.mirafive.io/sdks/react) and [Vue](https://docs.mirafive.io/sdks/vue) packages wrap. No build step? Use the [script tag](https://docs.mirafive.io/sdks/script-tag). On Next.js, TanStack Start, Nuxt or Astro, use their [framework packages](https://docs.mirafive.io/sdks).

## Install

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

The package is ESM only, has no dependencies and is built for ES2022 browsers. Every feature is its own import, and what you do not import is not shipped:

| Import | min + gzip |
| --- | --- |
| `@mirafive/sdk-browser` (`createMira`) | 2.48 kB |
| with `@mirafive/sdk-browser/pageviews` | 2.76 kB |
| `@mirafive/sdk-browser/identity` | 1.13 kB |
| `@mirafive/sdk-browser/autocapture` | 0.86 kB |
| `@mirafive/sdk-browser/search` | 0.40 kB |
| `@mirafive/sdk-browser/flags` | 2.91 kB |
| `@mirafive/sdk-browser/experiments` | 0.47 kB |
| everything together | 7.31 kB |

Each plugin is measured as a bundler adds it to a page that already has the core.

## 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 your bundler's public env var:

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

Create the client once, in code that runs only in the browser, and export it:

```ts title="src/mira.ts"
import { createMira } from '@mirafive/sdk-browser'
import { pageviews } from '@mirafive/sdk-browser/pageviews'

export const mira = createMira({
  key: import.meta.env.VITE_MIRAFIVE_KEY,
  plugins: [pageviews()],
})
```

Import that module from your entry point. `pageviews()` records the landing page and every client-side navigation for any router. You do not add a router hook.

The client starts in [consentless mode](https://docs.mirafive.io/guides/consent#consentless-mode): it sets no cookies, stores nothing on the device, never reads the browser's language, time zone or screen size, and needs no consent banner.

> **Warning:** Never ship `MIRAFIVE_SECRET_KEY` to the browser. `createMira` throws for a `secretKey` option, and MIRA FIVE refuses a secret key in the batch URL and marks it as exposed.

## Verify

1. Open the app on its real domain. Events from `localhost`, `127.*`, `[::1]`, `*.local` and `file:` are dropped unless you pass `trackLocalhost: true`.
2. In the browser's network tab, look for `POST https://events.mirafive.io/v1/batch/mf_…`. Batches leave 5 seconds after the first event, after 20 events, or when the tab is hidden. Call `mira.flush()` to send at once.
3. It answers `202`:

```json
{ "batch": "…", "accepted": 1, "dropped": 0 }
```

4. Open the source's live view in MIRA FIVE and find the pageview.

Nothing arriving? See [Troubleshooting](#troubleshooting).

## Track events

Call `track(name, properties)` where the thing happens:

```ts
import { mira } from './mira'

document.querySelector('#signup')?.addEventListener('submit', () => {
  mira.track('signup', { plan: 'pro' })
})
```

Names are 1 to 128 characters, cannot start with `$` and cannot start or end with whitespace. The core drops an event the server would refuse, with a development warning, so it cannot take its batch down: properties over 32 KB of JSON, over 64 leaf values, deeper than 5 levels, a key over 128 characters, or a string with a lone surrogate. Event names, property limits and revenue are covered in [Track events](https://docs.mirafive.io/guides/track-events).

Name your events in a type to have TypeScript check every call. Typed events are types only and add nothing to the bundle:

```ts title="src/mira.ts"
import { createMira } from '@mirafive/sdk-browser'
import { pageviews } from '@mirafive/sdk-browser/pageviews'

type Events = {
  signup: { plan: string }
  logout: undefined
}

export const mira = createMira<Events>({
  key: import.meta.env.VITE_MIRAFIVE_KEY,
  plugins: [pageviews()],
})

mira.track('signup', { plan: 'pro' }) // checked
mira.track('logout') // properties optional
```

To record clicks, form submits and field changes without code, add `autocapture()`:

```ts
import { autocapture } from '@mirafive/sdk-browser/autocapture'

const mira = createMira({ key: import.meta.env.VITE_MIRAFIVE_KEY, plugins: [pageviews(), autocapture()] })
```

It sends `$autocapture` for `a`, `button`, `input`, `select`, `textarea` and elements with a button, link, tab or menuitem role. It records the tag, a short selector, the id and classes (generated names skipped), link and button text, the cleaned `href`, `name`, `type`, and `data-testid`, `data-test`, `data-cy`, `data-qa`, `data-track` plus any `selectorAttributes` you pass. It never records what a visitor typed, and skips password, email and hidden inputs and everything inside `[data-mira-no-capture]`. Autocapture works in both modes.

## Identify users

Identifying users needs [full mode](https://docs.mirafive.io/guides/consent#full-mode) and the `identity()` plugin:

```ts title="src/mira.ts"
import { createMira } from '@mirafive/sdk-browser'
import { identity } from '@mirafive/sdk-browser/identity'
import { pageviews } from '@mirafive/sdk-browser/pageviews'

export const mira = createMira({
  key: import.meta.env.VITE_MIRAFIVE_KEY,
  mode: 'full',
  plugins: [pageviews(), identity()],
})
```

```ts
// After login, once the visitor granted statistics consent
mira.identify(user.id, { plan: user.plan })

// After logout
mira.reset()
```

`identify` sends `$identify` and puts the user id on every later event. Ids are 1 to 256 characters; numbers are turned into strings. When a different user signs in on the same browser, the anonymous and session ids start fresh.

Events before statistics consent are dropped, `$identify` included. The user id is kept in memory and stamped on later events, but call `identify` again after the grant if you need the `$identify` event.

To link server-side events to the visitor, send `mira.anonymousId()` to your server. It is `undefined` without statistics consent. See [Identify users](https://docs.mirafive.io/guides/identify-users).

## Consent

In full mode, nothing is sent and nothing is stored until the visitor answers. Pass the answer with `consent()`:

| Call | Effect |
| --- | --- |
| `consent(true)` | Grants statistics only. |
| `consent({ statistics, experiments, targeting })` | Answers by scope. A scope you leave out keeps its last answer. |
| `consent(false)` | Forgets the ids and the user, and clears the queue. |

The landing pageview is sent when consent is first granted, so a visitor who accepts on the first page still counts that page. Wire your consent manager's callback to it. `statistics` is the usual "analytics" category, `targeting` the "marketing" one:

```ts
import { mira } from './mira'

// Cookiebot
window.addEventListener('CookiebotOnConsentReady', () => {
  const { statistics, preferences, marketing } = Cookiebot.consent
  mira.consent({ statistics, experiments: preferences, targeting: marketing })
})

// OneTrust (default group ids: C0002 performance, C0003 functional, C0004 targeting)
window.OptanonWrapper = () => {
  const groups = window.OnetrustActiveGroups ?? ''
  mira.consent({
    statistics: groups.includes(',C0002,'),
    experiments: groups.includes(',C0003,'),
    targeting: groups.includes(',C0004,'),
  })
}
```

If the answer is known before the client starts, set `window.__mirafive_consent = { statistics, experiments, targeting }` (or `false`) first. `identity()` applies it at creation, so the landing pageview is counted with the answer (`$boot: 1`).

Full mode stores, in `localStorage` only and never in cookies, `mirafive:{namespace}:aid` (the anonymous id, kept 365 days since last seen), `:sid` (the session, 30 minutes idle) and `:uid` (a hash of the user id). The namespace is the key without its last `_…` part.

Do Not Track, Global Privacy Control, `window.__mirafive_ignore = true` (set it for your own visits) and prerendering send nothing in either mode. A prerendered page sends once it is shown. See [Consent](https://docs.mirafive.io/guides/consent).

## Feature flags

Add `flags()`, then read flags in an `onFlags` listener. It runs once flags have loaded, and again whenever they change:

```ts title="src/mira.ts"
import { createMira } from '@mirafive/sdk-browser'
import { flags } from '@mirafive/sdk-browser/flags'
import { pageviews } from '@mirafive/sdk-browser/pageviews'

export const mira = createMira({
  key: import.meta.env.VITE_MIRAFIVE_KEY,
  plugins: [pageviews(), flags()],
})

mira.onFlags(() => {
  const newCheckout = mira.flag('new-checkout', false)
  const limits = mira.config('checkout-limits', { maxItems: 10 })

  document.body.classList.toggle('new-checkout', newCheckout === true)
  document.querySelector('#max-items')!.textContent = String(limits.maxItems)
})
```

- `flag(key, fallback)` returns the variant key, or `true`/`false` for an on/off flag. It returns the fallback until flags load, and for a flag this source does not have (with a development warning).
- `config(key, fallback)` returns the remote-config value of the variant.
- `setFlagProperties(properties)` passes facts for targeting rules. They stay in memory and are never sent.

The plugin fetches the source's flags at start, every `refreshSeconds` (300) while the page is visible, when the page becomes visible again and when the URL changes. In consentless mode it fetches only the answers; in full mode it evaluates rules in the browser, and with `targeting` consent it also looks up the visitor's segments. If the page has a `<script type="application/json" id="mirafive-flags">` block from a server SDK, the first read already has its answers: see [Feature flags](https://docs.mirafive.io/guides/feature-flags).

To preview a variant, open the page with `?mirafive-preview=new-checkout:b`. To pin answers in development, pass `overrides` (full mode only): `flags({ overrides: { 'new-checkout': true } })`. Previews and overrides are never counted.

## Experiments

Code experiments are flags: read them with `flag()`. In full mode, with `statistics` and `experiments` consent, the client sends one `$exposure` per flag and page load for experiments counted in the browser. An experiment keeps the variant the page first showed until the page reloads, unless the experiment is turned off.

Page experiments are drawn before the page renders by a head snippet that MIRA FIVE generates. To count them, add `experiments()` after `flags()`, in full mode:

```ts title="src/mira.ts"
import { createMira } from '@mirafive/sdk-browser'
import { experiments } from '@mirafive/sdk-browser/experiments'
import { flags } from '@mirafive/sdk-browser/flags'
import { identity } from '@mirafive/sdk-browser/identity'
import { pageviews } from '@mirafive/sdk-browser/pageviews'

export const mira = createMira({
  key: import.meta.env.VITE_MIRAFIVE_KEY,
  mode: 'full',
  plugins: [pageviews(), identity(), flags(), experiments()],
})
```

It sends `$exposure` for each page experiment the snippet drew (`window.__mirafive_experiments`), once the `experiments` scope is granted. See [Experiments](https://docs.mirafive.io/guides/experiments).

## Single-page apps

- `pageviews()` listens to the Navigation API's `navigatesuccess` where the browser has it, else patches `pushState` and `replaceState` and listens to `popstate`. The title is read one task after the URL changes, so a router has time to set it.
- A navigation that leaves the cleaned URL unchanged is not a new view. The cleaned URL keeps only `utm_*`, `ref`, `source` and click-id parameters, and drops the fragment.
- Hash routing (`/#/pricing`): pass `pageviews({ hash: true })`. Hash changes become pageviews and the fragment is kept.
- After the first view, the referrer is the previous page.
- For full control, leave out `pageviews()` and call `mira.pageview()` after each route change.

Create one client per page. A second `createMira` while one is running stays inert and warns in development. `destroy()` stops timers and listeners, undoes the history patch, drops the queue and releases the page, so a component that creates the client on mount and destroys it on unmount (React StrictMode mounts twice) works. `createMira` needs `window`: do not call it during a server render.

## Writing a plugin

A plugin is `{ name, setup(core) }`. `setup` may return a teardown that `destroy()` runs. Plugins given to `createMira` are set up in order during creation; `use()` adds one later.

```ts title="src/outbound-links.ts"
import type { Plugin } from '@mirafive/sdk-browser'

export const outboundLinks = (): Plugin => ({
  name: 'outbound-links',
  setup(core) {
    const onClick = (event: MouseEvent) => {
      const link = (event.target as Element | null)?.closest('a')

      if (link && link.host !== location.host) {
        core.send('outbound_link', { host: link.host }, { url: core.clean(location.href) })
      }
    }

    document.addEventListener('click', onClick, true)

    return () => document.removeEventListener('click', onClick, true)
  },
})
```

`setup` receives `MiraCore`:

| Member | Description |
| --- | --- |
| `options` | The options as given, with `host` (no trailing slash) and `mode` resolved. |
| `state` | Shared state: `mode`, `context`, `page`, and what plugins own (`consent`, `user`, `aid()`, `flags`). Write only the parts your plugin owns. |
| `client` | The `Mira` client the site holds. |
| `send(name, properties?, page?)` | Queues an event. Opt-outs, the full-mode gate and `beforeSend` hooks apply. |
| `expose(methods)` | Adds methods to the client, replacing those of the same name. |
| `on(hook, listener): () => void` | Subscribes to a hook. Returns an unsubscribe function. |
| `emit(hook, argument?): boolean` | Runs a hook's listeners; `false` when one returned `false`. |
| `ready(run)` | Runs in a microtask: after every plugin given at creation is set up, or right after a late `use()`. |
| `flush(unload?)` | Sends the queue; `true` uses `sendBeacon`, as on page hide. |
| `clear()` | Empties the queue and the hold buffer, and cancels batches waiting for a retry. |
| `hold()`, `release(keep)` | Keeps up to 100 events while full mode waits for identity; `release(true)` queues them, `release(false)` drops them. |
| `clean(url)` | The URL cleaned as pageviews are. |
| `cut(text, max)` | Cuts text to `max` UTF-16 units without splitting a surrogate pair. |
| `uuid()` | A v4 UUID. |
| `warn(message)` | A development warning, once per message, on local hosts only. |
| `optedOut()` | `true` under Do Not Track, Global Privacy Control, `__mirafive_ignore` or prerendering. |

| Hook | Argument | When |
| --- | --- | --- |
| `beforeSend` | The event. Change it in place; return `false` to drop it. | As each event is queued. |
| `pageview` | The page URL before cleaning. | After each pageview. |
| `consent` | The consent answer. | After each answer (identity). |
| `user` | none | After `identify()` and `reset()` (identity). |
| `flags` | none | After flags load or change (flags). |
| `flush` | none | Before each flush, including the one on page hide. |

In mode `'full'` the core sends nothing until `state.mode` is `'full'`, whichever plugins are present.

## API reference

```ts
import { createMira } from '@mirafive/sdk-browser'
import { autocapture } from '@mirafive/sdk-browser/autocapture'
import { experiments } from '@mirafive/sdk-browser/experiments'
import { flags } from '@mirafive/sdk-browser/flags'
import { identity } from '@mirafive/sdk-browser/identity'
import { pageviews } from '@mirafive/sdk-browser/pageviews'
import { siteSearch } from '@mirafive/sdk-browser/search'
```

There are no default exports.

### createMira

`createMira<Events>(options): Mira<Events>`

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `key` | `string` | none | **Required.** The source's website key (`mf_…`). |
| `host` | `string` | `https://events.mirafive.io` | Events host, with scheme. For a first-party proxy. |
| `mode` | `'consentless' \| 'full'` | `'consentless'` | `'full'` needs `identity()` in `plugins`. |
| `plugins` | `Plugin[]` | `[]` | Set up in order during creation. |
| `flushAt` | `number` | `20` | Send once this many events are queued, 1 to 1000. Each batch also stays under about 48 KB. |
| `flushAfterMs` | `number` | `5000` | Send this long after the first queued event, 50 to 300000. |
| `trackLocalhost` | `boolean` | `false` | Also send from `localhost`, `127.*`, `[::1]`, `*.local` and `file:`. |

A `flushAt` or `flushAfterMs` out of range is clamped with a development warning. `createMira` throws a `TypeError` for a `secretKey` option, a `host` without a scheme, and `mode: 'full'` without `identity()`. Nothing else throws: transport problems drop the batch with a development warning.

### Client members

| Member | Plugin | Description |
| --- | --- | --- |
| `track(name, properties?)` | core | Queues an event. Names starting with `$` are reserved. |
| `pageview(page?)` | core | Queues `$pageview` for the current page, or `{ url?, title?, referrer? }`. The referrer defaults to the previous pageview's URL, then `document.referrer`. |
| `flush(): Promise<void>` | core | Sends the queue now. |
| `use(plugin)` | core | Adds a plugin after creation. |
| `destroy()` | core | Stops timers and listeners, undoes patches, drops the queue and lets a new client start. |
| `consent(answer)` | identity | `true`, `false` or `{ statistics?, experiments?, targeting? }`. See [Consent](#consent). |
| `identify(userId, traits?)` | identity | Sends `$identify`, then `userId` on later events. A different user starts fresh ids. |
| `reset()` | identity | Forgets ids, user and session. |
| `anonymousId(): string \| undefined` | identity | The anonymous id; `undefined` without statistics consent. |
| `search(query)` | search | Queues `$search`. |
| `flag(key, fallback): string \| boolean` | flags | The variant, or `true`/`false` for an on/off flag. |
| `config<T>(key, fallback: T): T` | flags | The variant's remote-config value. |
| `onFlags(listener): () => void` | flags | Runs when flags load or change, at once if loaded. Returns an unsubscribe function. |
| `setFlagProperties(properties)` | flags | Facts for targeting rules, held in memory and never sent. |

A member whose plugin is missing warns once in development and does nothing: `flag` and `config` return the fallback, `onFlags` returns a no-op unsubscribe.

### Plugins

| Plugin | Import | Description |
| --- | --- | --- |
| `pageviews(options?)` | `@mirafive/sdk-browser/pageviews` | The landing pageview and every same-document navigation. |
| `identity()` | `@mirafive/sdk-browser/identity` | Consent, ids and the signed-in user. Required for mode `'full'`. |
| `autocapture(options?)` | `@mirafive/sdk-browser/autocapture` | `$autocapture` for clicks, submits and changes. Both modes. |
| `siteSearch(options?)` | `@mirafive/sdk-browser/search` | `$search` from the page URL and from `search(query)`. Mode `'full'` only, after consent. |
| `flags(options?)` | `@mirafive/sdk-browser/flags` | Feature flags, remote config and code experiments. |
| `experiments()` | `@mirafive/sdk-browser/experiments` | `$exposure` for page experiments. Mode `'full'`, after `flags()` and with `identity()`. |

`pageviews` options:

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `hash` | `boolean` | `false` | The fragment is the route (`#/pricing`): hash changes are pageviews and the fragment is kept. |
| `initial` | `boolean` | `true` | Send the landing pageview, a microtask after creation. |

`autocapture` options:

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `selectorAttributes` | `string[]` | `[]` | More attributes to report in `$el_attrs`, besides `data-testid`, `data-test`, `data-cy`, `data-qa` and `data-track`. |

`siteSearch` options:

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `parameters` | `string[]` | `['q', 's', 'search', 'query']` | URL parameters that carry the search term. The term is read before URL cleaning and sent after 1 second without a change, or when the visitor moves on or leaves. |

`flags` options:

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `bootstrap` | `FlagBootstrap` | none | Answers until the first fetch. A `#mirafive-flags` block on the page wins. Ignored when older than 7 days. |
| `overrides` | `Record<string, string \| boolean>` | `{}` | Fixed answers for development, mode `'full'` only, never counted. `true` is `on`, `false` is `off`. |
| `refreshSeconds` | `number` | `300` | Refetch interval while the page is visible. |

### Types

```ts
import type {
  ConsentAnswer,
  EventMap,
  FlagBootstrap,
  FlagState,
  Json,
  Mira,
  MiraCore,
  MiraEvent,
  MiraHooks,
  MiraOptions,
  MiraState,
  Mode,
  Page,
  Plugin,
  Properties,
} from '@mirafive/sdk-browser'
```

| Type | Description |
| --- | --- |
| `Mira<Events>` | The client `createMira` returns. |
| `MiraOptions` | The options of `createMira`. |
| `EventMap` | `Record<string, Properties \| undefined>`: event name to its properties; `undefined` makes them optional. |
| `ConsentAnswer` | `{ statistics?, experiments?, targeting? }`. |
| `Page` | `{ url?, title?, referrer? }`. |
| `Properties` | `Record<string, unknown>`. |
| `Mode` | `'consentless' \| 'full'`. |
| `FlagBootstrap` | The bootstrap a server SDK renders for the `flags()` plugin. |
| `Json` | A JSON value, the type of remote config. |
| `Plugin`, `MiraCore`, `MiraHooks`, `MiraState`, `MiraEvent`, `FlagState` | The plugin surface. See [Writing a plugin](#writing-a-plugin). |

The plugin entries also export their option types: `PageviewsOptions`, `AutocaptureOptions`, `SiteSearchOptions` and `FlagsOptions`.

Batches are `text/plain` POSTs to `{host}/v1/batch/{key}`, so they need no CORS preflight. On page hide they go by `navigator.sendBeacon`. A failed send is tried up to three times in all, with backoff, honouring `Retry-After` up to 10 seconds. The client reports `mirafive-browser/1.0.0` as the SDK. If your site sends a Content Security Policy, add `connect-src https://events.mirafive.io` (or your `host`).

## Troubleshooting

| Symptom | Cause and fix |
| --- | --- |
| Nothing arrives | Events from local hosts are off by default (`trackLocalhost: true`). Do Not Track, Global Privacy Control or `__mirafive_ignore` is on. In mode `'full'`, `consent()` has not run. Check `host`. |
| `TypeError: [mirafive] mode "full" needs identity()` | Add `identity()` to `plugins`. |
| `TypeError: [mirafive] host needs a scheme` | Write `host` with `https://`. |
| `[mirafive] second client stays inert` | `createMira` ran twice on the page. Create one client, or `destroy()` the first. |
| `[mirafive] consent() needs its plugin` (or another member) | The member's plugin is not in `plugins`. |
| `403 secret_key_in_path` | You passed a secret key. Use the website key of a website source. |
| `403 origin_not_allowed` | Add the site's origin to the source's allowed origins in MIRA FIVE. |
| `400 collection_mode_not_allowed` | `mode: 'full'` on a consentless source. Switch the source to full mode or remove `mode`. |
| A flag always returns its fallback | Flags have not loaded yet (read in `onFlags`), the flag is not enabled for this source (a development warning says so), a segment rule lacks `targeting` consent, or a user flag was read before `identify()`. |
| An experiment never counts | It needs mode `'full'` with `statistics` and `experiments` consent. Previews and overrides are never counted. |

Development warnings appear only on local hosts. Pass `trackLocalhost: true` to see events and warnings while you develop.

## Set up with an AI agent

Paste this into your coding agent:

```text
Add MIRA FIVE analytics to this bundled frontend with @mirafive/sdk-browser.
Docs: https://docs.mirafive.io/sdks/browser.md

1. Install @mirafive/sdk-browser with the project's package manager. If the app uses React,
   Next.js, TanStack Start, Vue, Nuxt or Astro, use that MIRA FIVE package instead (see the docs).
2. Put the source's website key (mf_…) in the bundler's public env var: VITE_MIRAFIVE_KEY,
   NEXT_PUBLIC_MIRAFIVE_KEY, PUBLIC_MIRAFIVE_KEY or NUXT_PUBLIC_MIRAFIVE_KEY.
   Never put MIRAFIVE_SECRET_KEY in browser code.
3. In a module that runs once and only in the browser, add:
     import { createMira } from '@mirafive/sdk-browser'
     import { pageviews } from '@mirafive/sdk-browser/pageviews'
     export const mira = createMira({ key: <the env var>, plugins: [pageviews()] })
   Add autocapture() from '@mirafive/sdk-browser/autocapture' if clicks should be counted,
   and flags() from '@mirafive/sdk-browser/flags' if the app reads feature flags.
4. Do not add router hooks for pageviews; pageviews() covers every router.
5. Keep the default consentless mode: it needs no consent banner. Only if the site already has a
   consent manager and wants user ids: add identity() from '@mirafive/sdk-browser/identity',
   pass mode: 'full', and call mira.consent({ statistics, experiments, targeting }) from the
   consent manager's callback.
6. Verify: load a page on a non-localhost domain (or pass trackLocalhost: true), call
   mira.flush(), 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.
```
