MIRA FIVE

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 in mirafive.q and run, in order, when it has loaded. Leave it out only if nothing on the page calls mirafive(…) 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

  1. Open the site on its real domain. Pages on localhost, 127.*, [::1], *.local and file: send nothing unless you add data-track-localhost.
  2. In the browser's network tab, check that mira.js loads with 200.
  3. Run mirafive('flush') in the console. Batches otherwise leave 5 seconds after the first event, after 20 events, or when the tab is hidden.
  4. Look for POST https://events.mirafive.io/v1/batch/mf_…. It answers 202:
{ "batch": "…", "accepted": 1, "dropped": 0 }
  1. 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.

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', …):

AnswerEffect
trueGrants statistics only.
{ statistics, experiments, targeting }Answers by scope. A scope you leave out keeps its last answer.
falseForgets 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>
  • flag returns the variant key, or true/false for an on/off flag.
  • config returns the remote-config value of the variant.
  • flagProperties passes 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): add data-hash. Hash changes become pageviews and the fragment is kept.
  • Full control: add data-manual and call mirafive('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:

Filemin + gzipLoaded when
mira.js4.31 kBAlways.
chunks/identity.<hash>.js1.19 kBdata-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>.js0.92 kBdata-autocapture.
chunks/search.<hash>.js0.47 kBdata-site-search in full mode, on the first consent grant.
chunks/flags.<hash>.js2.98 kBdata-flags, the first flag, config, flags or flagProperties call, a #mirafive-flags bootstrap block, or a page experiment.
chunks/experiments.<hash>.js0.54 kBFull mode, when a page experiment snippet has drawn a variant (window.__mirafive_experiments is not empty).

What a page downloads in total:

PageSize
Default: consentless, automatic pageviews4.31 kB
With data-autocapture5.23 kB
data-mode="full", visitor has not answered or declined4.31 kB
data-mode="full", visitor consented5.50 kB
Full mode, consented, with data-site-search5.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/tracker
dist/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:

FileHeaders
mira.jsCache-Control: public, max-age=3600
mira.<hash>.js, chunks/*Cache-Control: public, max-age=31536000, immutable
Every .js fileContent-Type: text/javascript; charset=utf-8. From another origin also Access-Control-Allow-Origin: *, because chunks are requested in CORS mode for SRI.
manifest.jsonCache-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.io

When 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 nonce or its hash in script-src.
  • Under 'strict-dynamic' the host allowlist is ignored. Give the mira.js tag the page's nonce too. 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:

AttributeTypeDefaultDescription
data-keystringnoneRequired. The source's website key (mf_…).
data-hostURLhttps://events.mirafive.ioEvents host, with scheme. For a first-party proxy.
data-mode"full"consentlessfull adds ids and device context after consent, and loads identity on the first grant.
data-hashon/offoffThe fragment is the route (#/pricing): hash changes are pageviews and the fragment is kept.
data-manualon/offoffNo automatic pageviews. Send them with mirafive('pageview').
data-autocaptureon/offoffClicks, submits and changes as $autocapture.
data-site-searchon/off or listoffFull 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-flagson/offoffLoad the flag code at once instead of on the first flag call.
data-track-localhoston/offoffAlso 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):

VerbArgumentsNeedsDescription
trackname, properties?Queues an event. Names starting with $ are reserved.
pageview{ url?, title?, referrer? }?Queues $pageview for the current page, or the one given.
flushSends the queue now.
consenttrue | false | { statistics?, experiments?, targeting? }full modetrue grants statistics only; scopes by name; false forgets ids and the user and clears the queue.
identifyuserId, traits?full modeSends $identify, then userId on later events.
resetfull modeForgets ids, user and session.
anonymousIdcallbackfull modeCalls callback(id); id is undefined without statistics consent.
searchqueryfull mode, data-site-searchQueues $search.
flagkey, fallbackReturns the variant, or true/false for an on/off flag.
configkey, fallbackReturns the variant's remote-config value.
flagslistenerRuns 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.
flagPropertiespropertiesFacts 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:

GlobalDescription
window.__mirafive_consentA consent answer known before the script runs: { statistics, experiments, targeting } or false. Full mode only.
window.__mirafive_ignoretrue 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

SymptomCause and fix
Nothing arrivesOn 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-keyThe tag has no data-key.
[mirafive] bad keydata-key is not a website key: a secret key or a typo. The page still sends.
[mirafive] no script tagThe loader was inlined, loaded as type="module" or run through eval. Load it by src.
[mirafive] <chunk> chunk failedA 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_pathYou pasted a secret key. Use the website key of a website source.
403 origin_not_allowedAdd the site's origin to the source's allowed origins in MIRA FIVE.
400 collection_mode_not_alloweddata-mode="full" on a consentless source. Switch the source to full mode or remove the attribute.
A flag always returns its fallbackIt 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.

On this page