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 apathstarting with/. Metrickle files violations under the page's path. versionandenvironment, 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
| Limit | Per scan |
|---|---|
| Request size | 10 MB. Split larger scans by page |
| Pages | 500 |
| Elements per rule on a page | 25 |
| Violations | 5,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
| Status | Body | Means |
|---|---|---|
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.