Script tag
Add MIRA FIVE analytics, feature flags and page experiments to any website by pasting two lines into the page head.
@mirafive/tracker is the hosted script: two lines in the <head> of every page, no build step. Use it for WordPress, Webflow, Shopify themes, static HTML and server-rendered templates. For an app you bundle yourself, use the browser SDK or a framework package from the SDK overview instead. On Laravel and Symfony, the server package renders this tag for you: see Laravel and Symfony.
Install
Paste this into the <head> of every page, usually the shared layout or theme header:
<script>window.mirafive=window.mirafive||function(){(mirafive.q=mirafive.q||[]).push(arguments)}</script>
<script defer src="https://cdn.mirafive.io/mira.js" data-key="mf_…"></script>- The first line is the command queue. It defines
mirafive(…)at once, so calls made before the script arrives wait inmirafive.qand run, in order, when it has loaded. Leave it out only if nothing on the page callsmirafive(…)before the script loads. - The second line loads the loader,
mira.js(4.31 kB min + gzip). It records a pageview for the landing page and for every client-side navigation.
Load mira.js by src. It finds its feature chunks from its own URL, so it does not work inlined, as type="module" or through eval.
The loader is ES2020 (Safari 14 and later). The feature-flag code uses Object.hasOwn (Safari 15.4 and Chrome 93 and later). The script has no dependencies.
Set up
You need the website key of a website source (mf_…). See Keys for where to find it. Put it in data-key. The key is public and safe in page source.
If your templates read configuration from the environment, keep the key in MIRAFIVE_WEBSITE_KEY and print it into the tag. A literal key in the template works too.
Turn on more features with attributes on the same tag:
<script>window.mirafive=window.mirafive||function(){(mirafive.q=mirafive.q||[]).push(arguments)}</script>
<script defer src="https://cdn.mirafive.io/mira.js" data-key="mf_…" data-autocapture data-flags></script>On/off attributes are on when present and off when absent or set to off or false (any case). A template can write data-autocapture="{{ autocapture }}". Every attribute is listed in the API reference.
The script starts in 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.
Never put a secret key in a page. MIRA FIVE refuses a secret key in the batch URL with 403 secret_key_in_path and marks the key as exposed.
Verify
- Open the site on its real domain. Pages on
localhost,127.*,[::1],*.localandfile:send nothing unless you adddata-track-localhost. - In the browser's network tab, check that
mira.jsloads with200. - Run
mirafive('flush')in the console. Batches otherwise leave 5 seconds after the first event, after 20 events, or when the tab is hidden. - Look for
POST https://events.mirafive.io/v1/batch/mf_…. It answers202:
{ "batch": "…", "accepted": 1, "dropped": 0 }- Open the source's live view in MIRA FIVE and find the pageview.
Nothing arriving? See Troubleshooting.
Track events
Call mirafive('track', name, properties) anywhere on the page, also before the script has loaded:
<script>
document.querySelector('#signup').addEventListener('submit', () => {
mirafive('track', 'signup', { plan: 'pro' })
})
</script>Names are 1 to 128 characters and cannot start with $: those are reserved for events MIRA FIVE sends itself. Property limits and revenue are covered in Track events.
To record clicks, form submits and field changes without code, add data-autocapture. Each becomes an $autocapture event that describes the element (tag, a short selector, id and stable classes, link or button text, the cleaned href, name, type, and data-testid, data-test, data-cy, data-qa and data-track). It never records what a visitor typed. It skips password, email and hidden inputs and everything inside an element with data-mira-no-capture. Autocapture works in both modes.
Consent
Consentless mode needs no consent call. To collect ids, identify users, run page experiments or target segments, switch the tag to full mode with data-mode="full" and put it behind your consent management platform (CMP).
In full mode, nothing is sent and nothing is stored until the visitor answers. The identity code is not even downloaded. Pass the answer with mirafive('consent', …):
| Answer | Effect |
|---|---|
true | Grants statistics only. |
{ statistics, experiments, targeting } | Answers by scope. A scope you leave out keeps its last answer. |
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 CMP's callback to the verb. statistics is the usual "analytics" category, targeting the "marketing" one:
<script>
// Cookiebot
window.addEventListener('CookiebotOnConsentReady', () => {
const { statistics, preferences, marketing } = Cookiebot.consent
mirafive('consent', { statistics, experiments: preferences, targeting: marketing })
})
// OneTrust (default group ids: C0002 performance, C0003 functional, C0004 targeting)
window.OptanonWrapper = () => {
const groups = window.OnetrustActiveGroups || ''
mirafive('consent', {
statistics: groups.includes(',C0002,'),
experiments: groups.includes(',C0003,'),
targeting: groups.includes(',C0004,'),
})
}
// Your own banner
document.querySelector('#accept').addEventListener('click', () => mirafive('consent', true))
document.querySelector('#decline').addEventListener('click', () => mirafive('consent', false))
</script>If the answer is known before the script runs (your CMP stored it, or the server knows it), set it first. The identity code then loads at once and the landing pageview counts as answered on arrival ($boot: 1):
<script>window.__mirafive_consent = { statistics: true, experiments: true, targeting: false }</script>
<script>window.mirafive=window.mirafive||function(){(mirafive.q=mirafive.q||[]).push(arguments)}</script>
<script defer src="https://cdn.mirafive.io/mira.js" data-key="mf_…" data-mode="full"></script>Set window.__mirafive_consent = false for a stored decline. A consent call queued with the snippet before the script ran counts the same way as the pre-set answer.
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.
In consentless mode, consent, identify and reset do nothing, silently, and anonymousId answers undefined, so your CMP wiring can stay in place if you switch modes. Do Not Track, Global Privacy Control, window.__mirafive_ignore = true (set it for your own visits) and prerendered pages send nothing in either mode. See Consent.
Identify users
In full mode, after the visitor granted statistics consent, identify the signed-in user and forget them on logout:
<script>
// After login
mirafive('identify', 'user_123', { plan: 'pro' })
// After logout
mirafive('reset')
</script>identify sends $identify and puts the user id on every later event. Ids are 1 to 256 characters. When a different user signs in on the same browser, the anonymous and session ids start fresh.
To link server-side events to the visitor, read the anonymous id. The callback gets undefined without statistics consent:
<script>
mirafive('anonymousId', (id) => {
document.querySelector('#checkout-form input[name=mira_aid]').value = id ?? ''
})
</script>In full mode, identify, reset and anonymousId wait until the first consent grant has loaded the identity code, then run in call order. See Identify users.
Feature flags
Read flags inside a flags listener. It runs once flags have loaded, and again whenever they change:
<script>
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.querySelector('#max-items').textContent = String(limits.maxItems)
})
</script>flagreturns the variant key, ortrue/falsefor an on/off flag.configreturns the remote-config value of the variant.flagPropertiespasses facts for targeting rules. They stay in memory and are never sent:mirafive('flagProperties', { plan: 'pro' }).
Read outside a listener, flag and config answer undefined before mira.js has run, and the fallback until flags have loaded. These reads are not replayed later, because a late read would count an exposure for a value the page never showed.
The flag code loads on the first flag, config, flags or flagProperties call. Add data-flags to start loading at once. A <script type="application/json" id="mirafive-flags"> block rendered by a server SDK also loads it, and gives the first paint its answers: see Feature flags. To preview a variant, open the page with ?mirafive-preview=new-checkout:b.
Experiments
Code experiments are flags: read them as above. In full mode, with statistics and experiments consent, the script counts an exposure for experiments measured in the browser, once per flag and page load.
A page experiment is drawn before the page renders by a head snippet that MIRA FIVE generates for the experiment (full website sources only). Place that snippet in <head> above the two lines. Mark the variants in your HTML with data-mirafive-experiment and data-mirafive-variant, and the snippet's style shows only the drawn one. When the snippet has drawn a variant, the script loads the flag code, and in full mode also the experiments code, which sends the exposure once the visitor grants experiments consent. See Experiments.
Single-page apps
Automatic pageviews follow every same-document navigation: the Navigation API where the browser has it, else pushState, replaceState and 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.
- Hash routing (
/#/pricing): adddata-hash. Hash changes become pageviews and the fragment is kept. - Full control: add
data-manualand callmirafive('pageview')after each route change. Pass{ url, title, referrer }to override the current page's values.
For an app you bundle, prefer the browser SDK or a framework package: you get types and ship only what you import.
What loads when
The loader holds the core, automatic pageviews and the command queue. Everything else is a feature chunk it downloads only when the page needs it:
| File | min + gzip | Loaded when |
|---|---|---|
mira.js | 4.31 kB | Always. |
chunks/identity.<hash>.js | 1.19 kB | data-mode="full", on the first consent grant or a pre-set window.__mirafive_consent. A decline loads it only when ids from an earlier visit must be forgotten. |
chunks/autocapture.<hash>.js | 0.92 kB | data-autocapture. |
chunks/search.<hash>.js | 0.47 kB | data-site-search in full mode, on the first consent grant. |
chunks/flags.<hash>.js | 2.98 kB | data-flags, the first flag, config, flags or flagProperties call, a #mirafive-flags bootstrap block, or a page experiment. |
chunks/experiments.<hash>.js | 0.54 kB | Full mode, when a page experiment snippet has drawn a variant (window.__mirafive_experiments is not empty). |
What a page downloads in total:
| Page | Size |
|---|---|
| Default: consentless, automatic pageviews | 4.31 kB |
With data-autocapture | 5.23 kB |
data-mode="full", visitor has not answered or declined | 4.31 kB |
data-mode="full", visitor consented | 5.50 kB |
Full mode, consented, with data-site-search | 5.97 kB |
| Any page reading flags | + 2.98 kB |
| A full-mode page running a page experiment | + 3.52 kB |
Calls whose chunk has not arrived yet wait and run in call order once it has. Between a consent grant and the identity chunk's arrival, events (pageviews, autocapture, track) are held and sent with their original times and the new ids. A flush in that window waits for identity too.
Self-hosting
The same files are on npm. The package contains only dist/:
npm install @mirafive/trackerdist/mira.js stable name, changes with every release
dist/mira.<hash>.js pinned copy of that loader, never changes
dist/chunks/<feature>.<hash>.js identity, flags, experiments, search, autocapture
dist/manifest.json { version, loader: { file, integrity }, chunks: { <feature>: { file, integrity } } }Serve dist/ from any static host, keeping its layout, and point the tag at your copy:
cp -R node_modules/@mirafive/tracker/dist public/mirafive<script>window.mirafive=window.mirafive||function(){(mirafive.q=mirafive.q||[]).push(arguments)}</script>
<script defer src="/mirafive/mira.js" data-key="mf_…"></script>The loader requests chunks from chunks/ next to its own URL, with integrity and crossorigin="anonymous". Send these headers:
| File | Headers |
|---|---|
mira.js | Cache-Control: public, max-age=3600 |
mira.<hash>.js, chunks/* | Cache-Control: public, max-age=31536000, immutable |
Every .js file | Content-Type: text/javascript; charset=utf-8. From another origin also Access-Control-Allow-Origin: *, because chunks are requested in CORS mode for SRI. |
manifest.json | Cache-Control: no-cache |
When you update, add the new files and keep the old chunks. A browser that cached the previous mira.js for up to an hour still asks for the chunks it names, and pinned loaders ask for theirs forever. Serve the files unchanged: a proxy that rewrites them, or a folder that mixes files of two builds, breaks the integrity check.
To send events through your own domain, point data-host at a proxy of https://events.mirafive.io, with scheme: data-host="https://stats.example.com".
Content Security Policy
Allow the script origin and the events host:
script-src https://cdn.mirafive.io
connect-src https://events.mirafive.ioWhen self-hosting, use your own origin in script-src. With data-host, use that host in connect-src. connect-src covers both batches and flags.
- The inline queue snippet needs the page's
nonceor its hash inscript-src. - Under
'strict-dynamic'the host allowlist is ignored. Give themira.jstag the page'snoncetoo. The chunks it inserts are then allowed through it.
Subresource Integrity
mira.js cannot carry an integrity attribute: it changes with every release. For a loader that never changes under you, use the pinned copy. Take loader.file and loader.integrity (sha384) from dist/manifest.json of the version you want:
<script defer src="https://cdn.mirafive.io/mira.<hash>.js" integrity="sha384-…" crossorigin="anonymous" data-key="mf_…"></script>Each loader carries the sha256 digests of its chunks and checks every chunk it loads, so a pinned loader only ever runs the chunks it was built with.
API reference
Attributes
The loader reads these attributes of its own <script> tag and no others:
| Attribute | Type | Default | Description |
|---|---|---|---|
data-key | string | none | Required. The source's website key (mf_…). |
data-host | URL | https://events.mirafive.io | Events host, with scheme. For a first-party proxy. |
data-mode | "full" | consentless | full adds ids and device context after consent, and loads identity on the first grant. |
data-hash | on/off | off | The fragment is the route (#/pricing): hash changes are pageviews and the fragment is kept. |
data-manual | on/off | off | No automatic pageviews. Send them with mirafive('pageview'). |
data-autocapture | on/off | off | Clicks, submits and changes as $autocapture. |
data-site-search | on/off or list | off | Full mode only: $search from the q, s, search or query URL parameter. A comma list names your own parameters: data-site-search="term, k". |
data-flags | on/off | off | Load the flag code at once instead of on the first flag call. |
data-track-localhost | on/off | off | Also send from localhost, 127.*, [::1], *.local and file:. |
On/off attributes are on when present and off when absent or set to off or false, in any case.
Verbs
Every call is mirafive(verb, ...args):
| Verb | Arguments | Needs | Description |
|---|---|---|---|
track | name, properties? | Queues an event. Names starting with $ are reserved. | |
pageview | { url?, title?, referrer? }? | Queues $pageview for the current page, or the one given. | |
flush | Sends the queue now. | ||
consent | true | false | { statistics?, experiments?, targeting? } | full mode | true grants statistics only; scopes by name; false forgets ids and the user and clears the queue. |
identify | userId, traits? | full mode | Sends $identify, then userId on later events. |
reset | full mode | Forgets ids, user and session. | |
anonymousId | callback | full mode | Calls callback(id); id is undefined without statistics consent. |
search | query | full mode, data-site-search | Queues $search. |
flag | key, fallback | Returns the variant, or true/false for an on/off flag. | |
config | key, fallback | Returns the variant's remote-config value. | |
flags | listener | Runs listener when flags load or change, at once if loaded. Returns an unsubscribe function once the flag code has arrived; a listener queued before that gets none. | |
flagProperties | properties | Facts for targeting rules, held in memory and never sent. |
An unknown verb warns in development (on a local host) and does nothing. A call that throws, for example a callback that throws, is skipped with a development warning; the calls after it still run.
Globals
Set these before the tag:
| Global | Description |
|---|---|
window.__mirafive_consent | A consent answer known before the script runs: { statistics, experiments, targeting } or false. Full mode only. |
window.__mirafive_ignore | true sends nothing from this browser. Set it for your own visits. |
Batches are text/plain POSTs to {host}/v1/batch/{key}, so they need no CORS preflight. They leave after 20 events or 5 seconds, and by navigator.sendBeacon when the page is hidden or closed. They report mirafive-tracker/1.0.0 as the SDK.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| Nothing arrives | On a local host, add data-track-localhost. Do Not Track, Global Privacy Control or __mirafive_ignore is on. With data-mode="full", mirafive('consent', …) has not run. Batches wait up to 5 seconds: run mirafive('flush'). |
[mirafive] no data-key | The tag has no data-key. |
[mirafive] bad key | data-key is not a website key: a secret key or a typo. The page still sends. |
[mirafive] no script tag | The loader was inlined, loaded as type="module" or run through eval. Load it by src. |
[mirafive] <chunk> chunk failed | A chunk was blocked by the network, a CSP without the script origin (or without the loader's nonce under 'strict-dynamic'), or an integrity mismatch. The waiting calls are dropped and the chunk is requested again when next needed. |
| "Failed to find a valid digest in the 'integrity' attribute" | A proxy or CDN rewrote a file, or a self-hosted folder mixes files of two builds. Serve dist/ unchanged. |
403 secret_key_in_path | You pasted 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 | data-mode="full" on a consentless source. Switch the source to full mode or remove the attribute. |
| A flag always returns its fallback | It was read before flags loaded: read it inside mirafive('flags', …). Otherwise the flag is not enabled for this source, a segment rule lacks targeting consent, or a user flag was read before identify. |
The four [mirafive] setup messages above appear on every host. Other warnings appear only on local hosts.
Set up with an AI agent
Paste this into your coding agent:
Add MIRA FIVE analytics to this website with the hosted script tag.
Docs: https://docs.mirafive.io/sdks/script-tag.md
1. Get the source's website key (mf_…). Never put a secret key (MIRAFIVE_SECRET_KEY) in a page.
2. Into the <head> of every page (the shared layout, theme header or base template), add exactly:
<script>window.mirafive=window.mirafive||function(){(mirafive.q=mirafive.q||[]).push(arguments)}</script>
<script defer src="https://cdn.mirafive.io/mira.js" data-key="WEBSITE_KEY"></script>
Print the key from the project's config or env (MIRAFIVE_WEBSITE_KEY) if templates read it;
the key is public, so a literal is acceptable. Load mira.js by src, never inline it.
On Laravel use @mirafiveScript from mirafive/sdk-laravel; on Symfony mirafive_script() from mirafive/sdk-symfony.
3. Keep the default consentless mode: it needs no consent banner. Only if the site already has a
consent manager (CMP) and the owner wants ids: add data-mode="full" to the tag and call
mirafive('consent', { statistics, experiments, targeting }) from the CMP's consent callback,
or set window.__mirafive_consent before the tag when the answer is already known.
4. If the site sends a Content-Security-Policy, add https://cdn.mirafive.io to script-src and
https://events.mirafive.io to connect-src, and give the inline snippet the page's nonce.
If script-src uses 'strict-dynamic', give the mira.js tag the page's nonce as well.
5. Read feature flags only inside mirafive('flags', () => { … mirafive('flag', key, fallback) … }).
6. Verify: load a page on a non-localhost domain (or add data-track-localhost), run
mirafive('flush') in the console, 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.