Metrickle

Upload accessibility scans from CI

Last updated

Run axe-core in CI and post its results to Metrickle. Each scanned page's violations then show beside the friction on that page, so a failing button sits next to the people it's stopping. A violation on an element people rage-click or abandon lands on that element's friction hotspot. For how to read the results, see accessibility breakdowns.

Get the scan hook URL

Open the app's Integrations page and choose Set up scan hook under Accessibility scans. Only owners and admins, or people with the apps.keys permission, can see it. The URL looks like this:

https://in.metrickle.com/v1/a11y-scans/ah_…

Anyone with the URL can replace the app's scan results, so keep it in your CI secrets. If it leaks, choose Replace URL. The old URL stops working straight away.

Post results

With the axe CLI, against your staging or preview site after each deploy:

npx @axe-core/cli https://staging.example.com/ https://staging.example.com/checkout \
  --save axe-results.json --dir .
curl -X POST "$METRICKLE_SCAN_HOOK?version=$GIT_SHA&environment=staging" \
  -H "content-type: application/json" --data-binary @axe-results.json

From Playwright tests with @axe-core/playwright, once per page:

import AxeBuilder from "@axe-core/playwright";

const results = await new AxeBuilder({ page }).analyze();
await fetch(process.env.METRICKLE_SCAN_HOOK!, {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify(results),
});

What Metrickle accepts

  • One axe-core results object, an array of them (as the axe CLI saves), or { "results": [...] }.
  • Each result needs url, or a path starting with /. Metrickle files violations under the page's path.
  • version and environment, each up to 64 characters, are optional. Send them in the query string or as top-level fields of an object body. They're shown with the scan.

Limits

LimitPer scan
Request size10 MB. Split larger scans by page
Pages500
Elements per rule on a page25
Violations5,000

How results are kept

Each page keeps only its latest scan. A page in a new upload replaces that page's earlier violations, so a clean run clears it. Pages that aren't in the upload keep their last results.

Several runs of one page in the same upload, for example at different screen widths, are combined: each rule and element is listed once.

Responses

StatusBodyMeans
201{ "ok": true, "scan": {...} }Stored
400{ "error": "invalid_json" }The body isn't JSON
400{ "error": "not_axe_results", "message": "…" }The body isn't axe-core results, or no result had a page address. message says which
404{ "error": "not_found" }The URL is wrong or was replaced, or the app is archived
413{ "error": "payload_too_large" }The body is over 10 MB
429{ "error": "rate_limited" }Too many requests. Try again shortly

The scan object has id, tool (the axe version that ran), version, environment, pages, violations and at (Unix milliseconds).

Read results from an assistant

An assistant connected over MCP can read the latest scans with get_a11y_scans and the violations on a page with get_a11y_violations.