# Verify and debug

Source: https://docs.mirafive.io/guides/verify-and-debug

> Confirm an install works, read the server's receipt and refusals, and find out why events or flags are missing.

Use this page to confirm that events reach MIRA FIVE, and to find out why they do not. Every check below works without the dashboard except the last step, the live view. For the codes themselves, see [Errors](https://docs.mirafive.io/ingest-api/errors).

## Check an install

In the browser:

1. Open the site on its real domain. From `localhost`, nothing is sent unless you allow it (see [Localhost](#localhost)).
2. Open the browser's developer tools on the Network tab.
3. Send the queue now instead of waiting up to 5 seconds: run `mirafive('flush')` in the console for the script tag, or call `mira.flush()` in your code.
4. Find `POST https://events.mirafive.io/v1/batch/mf_…`. It answers `202` with a receipt:

```json
{ "batch": "0192d4a8-7b1c-4e8a-9c1d-2b3e4f5a6b7c", "accepted": 1, "dropped": 0 }
```

5. Open the source's live view in MIRA FIVE and find the event.

| Receipt field | Meaning |
| --- | --- |
| `batch` | The batch id the SDK sent |
| `accepted` | Events stored |
| `dropped` | Events not stored |
| `reason` | Present when nothing was kept: `bot`, `install_check`, `ingestion_paused` or `allowance_exhausted` |

A `202` is final, also when it kept nothing. The reasons:

| Reason | Cause and fix |
| --- | --- |
| `bot` | A website key received a batch from a bot user agent: a crawler, a headless or automated browser (Playwright, Puppeteer, Selenium, Lighthouse), or a request without a user agent. Test with a normal browser. |
| `install_check` | The batch held only `$install_check` events. Expected, see [Install check](#install-check). |
| `ingestion_paused` | Your organization's ingestion is paused. |
| `allowance_exhausted` | The plan's monthly event allowance is used up. |

For a server, send an install check and read the receipt, or pass `onError` and watch it.

## Localhost

Browser SDKs send nothing from `localhost`, `127.*`, `[::1]`, `*.local` or a `file:` page, so development does not pollute your numbers. To send from there:

**Script tag**

```html
<script defer src="https://cdn.mirafive.io/mira.js" data-key="mf_…" data-track-localhost></script>
```

**Browser**

```ts
const mira = createMira({ key: import.meta.env.VITE_MIRAFIVE_KEY, trackLocalhost: true })
```

The Astro integration is also off in `astro dev` unless you pass `dev: true` (see [Astro](https://docs.mirafive.io/sdks/astro)).

The server always accepts `localhost` and `127.0.0.1` as origins, so you do not add them to a source's allowed origins. Use a separate source for development if you want to keep test events apart.

## Install check

`$install_check` is an event that proves a key and a host work. The server answers it with a receipt and never stores or bills it:

**curl**

```sh
curl https://events.mirafive.io/v1/batch \
  -H "Authorization: Bearer $MIRAFIVE_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"v":1,"batch":"0192d4a8-7b1c-4e8a-9c1d-2b3e4f5a6b7c","mode":"full","events":[{"name":"$install_check"}]}'
```

**Node.js**

```ts
import { Mira } from '@mirafive/sdk-server'

const mira = new Mira({ key: process.env.MIRAFIVE_SECRET_KEY })

console.log(await mira.send([{ name: '$install_check' }]))
```

**Laravel**

```sh
php artisan mirafive:check
```

**Symfony**

```sh
bin/console mirafive:check
```

The answer:

```json
{ "batch": "0192d4a8-7b1c-4e8a-9c1d-2b3e4f5a6b7c", "accepted": 0, "dropped": 1, "reason": "install_check" }
```

`mirafive:check` prints the host, the key's namespace and the mode, sends the check and fails unless the reason is `install_check`. The install check runs before the bot filter, so `curl` works with a website key too: `POST /v1/batch/mf_…` with the same body and no `Authorization` header.

## Refusals

A refused batch answers with an error body, `{ "code": "…", "detail": "…" }`:

| Status and code | Cause | Fix |
| --- | --- | --- |
| `401 unauthorized` | The key is missing, unknown, revoked or archived | Copy the key again from [Keys](https://docs.mirafive.io/keys). Servers read `MIRAFIVE_SECRET_KEY` |
| `403 origin_not_allowed` | The page's origin is not in the source's allowed origins | Add the site's domain to the source. `www.` and the bare domain count as one site |
| `403 website_key_as_bearer` | A website key was sent as `Authorization: Bearer` | Server code needs the secret key of a server source |
| `403 secret_key_in_path` | A secret key was put in a browser URL. The key is now marked exposed | Browsers use the website key. Rotate the secret key |
| `403 secret_key_exposed` | Flags only: a secret key arrived with browser headers | Rotate the key, and keep it out of browser code |
| `403 lookup_not_allowed` | Flags only: a segment lookup on a consentless source | Use a full source, or no `targeting` consent in the page |
| `400 collection_mode_not_allowed` | A full batch to a consentless source | Switch the source to full, or send consentless |
| `400 validation_failed` | An event breaks a rule; `errors` names it, such as `events.0.name` | See [event names](https://docs.mirafive.io/guides/track-events#event-names) and [property limits](https://docs.mirafive.io/guides/track-events#properties) |
| `400 invalid_json` | The body is not valid JSON, often a string cut inside a character | Use an SDK, or send well-formed UTF-8 |
| `413 payload_too_large` | The body is over 1 MiB | Send fewer events per batch |
| `429 rate_limited` | Too many requests or events | Wait for `Retry-After`; the SDKs do |

The browser SDKs show the code as a development warning (`[mirafive] batch: origin_not_allowed`). MIRA FIVE also records refusals that come after the key check on the source, so you can see them there.

## Nothing arrives

Work down this list:

1. The page runs on `localhost` or another local host without `trackLocalhost` (see [Localhost](#localhost)).
2. Do Not Track or Global Privacy Control is on in your browser. The browser SDKs then send nothing, by design. Test in a profile without them.
3. `window.__mirafive_ignore` is set, for example because you are signed in as an admin (see [Ignore your own visits](https://docs.mirafive.io/guides/consent#ignore-your-own-visits)).
4. The page is in full mode and no statistics consent was given. Full mode sends nothing before the grant (see [Consent](https://docs.mirafive.io/guides/consent#full-mode)).
5. The page's Content Security Policy blocks the request. Allow `connect-src https://events.mirafive.io` (or your host), and for the script tag `script-src https://cdn.mirafive.io`.
6. The receipt says `dropped` with a reason, or the request is refused (see above).
7. Server events: the process ended before the buffer was sent (see [Batching and flushing](https://docs.mirafive.io/guides/server-side#batching-and-flushing)), or `onError` reports a failure.

## Development warnings

Browser SDKs log warnings starting with `[mirafive]` only on a local host name (`localhost`, `127.*`, `[::1]`, `*.local`, `file:`), once per message. To see warnings about the server's answers, develop on `localhost` with `trackLocalhost: true`.

| Warning | Meaning |
| --- | --- |
| `local host: set trackLocalhost` | Nothing is sent from this host |
| `bad name: …` | `track()` got an empty, too long or `$` name |
| `event dropped: …` | The event's properties break a limit, so it was dropped alone |
| `batch: …` | The server refused the batch (the code) or kept nothing (the reason) |
| `batch dropped` | A batch failed three times and was given up |
| `consent() needs its plugin` (or another method) | The client lacks the plugin: `identity()`, `flags()`, `siteSearch()` |
| `identity() needs mode "full"` | `identity()` is in a consentless client |
| `siteSearch() does nothing in mode "consentless"` | Site search needs full mode |
| `identify() needs a user id of 1–256 characters` | The user id is empty or too long |
| `identify(): the id looks like an email` | Pass your internal id instead |
| `flag "…" is unknown` | This source's flags have no such key |
| `second client stays inert` | `createMira()` ran twice on one page; the second client does nothing |

The script tag logs these on every host, since they mean a broken install: `[mirafive] no data-key`, `[mirafive] bad key` (the `data-key` is not a website key), `[mirafive] no script tag`, and `[mirafive] <feature> chunk failed` (a part of the script was blocked by the network, a Content Security Policy or an integrity check).

`createMira()` throws a `TypeError` for a `secretKey` option, a host without a scheme, and `mode: 'full'` without `identity()`. Server SDKs report failures to `onError`, or log `[mirafive] …` without one.

## Tests

Keep tests from sending events:

| SDK | In tests |
| --- | --- |
| Laravel | `Mira::fake()`, then `assertTracked()`, `assertNotTracked()`, `assertIdentified()`, `assertNothingTracked()`. See [Laravel](https://docs.mirafive.io/sdks/laravel) |
| Symfony | `mirafive: { test: true }` under `when@test`, then the `InteractsWithMira` trait and `MiraFake`. See [Symfony](https://docs.mirafive.io/sdks/symfony) |
| PHP | `new Mira(enabled: false)` sends nothing and needs no key, but still refuses bad input. See [PHP](https://docs.mirafive.io/sdks/php) |
| `@mirafive/sdk-server` | Pass a `fetch` function to `new Mira()` and assert on the requests it receives. See [Node.js and edge](https://docs.mirafive.io/sdks/node) |

The fakes still check every event, so an event MIRA FIVE would refuse fails the test as it would fail in production.
