How to set this up from zero. This is the exact tool stack we used — an AI coding agent (Claude Code, but any MCP-capable agent works) plus three integrations:
| role in the loop | tool we used | swap in your own |
|---|---|---|
| DETECT / TRACE / VERIFY (error tracker) | Sentry MCP | any error tracker with an API |
| TRACE (logs) | Grafana Cloud / Loki HTTP API | Mezmo, CloudWatch, Datadog… |
| REPRODUCE / VERIFY (synthetic checks) | Checkly CLI | — |
The agent does the reading, correlation and writing; the three integrations are what it reads from and acts through. You can also run every step by hand — nothing here requires an agent.
Checkly checks are defined as code ("constructs") and managed with the CLI — no UI
clicking. A project is a directory with a checkly.config.ts plus TypeScript files
that instantiate constructs (CheckGroupV2, MultiStepCheck, …). The two commands
that matter:
checkly test— bundles the project and runs the checks once, ad hoc, in Checkly's cloud (not locally), without deploying anything.checkly deploy— creates/updates the checks in the account as standing monitors.
Setup:
npm install -g checkly # or use npx checkly everywhere
npx checkly login # browser OAuth; or set CHECKLY_API_KEY + CHECKLY_ACCOUNT_ID
npx checkly whoami # confirm the account you're about to deploy intoRecommended: a dedicated account (or at least a dedicated check group + throwaway API key) for reproductions — the checks create and delete real resources on every run.
Three different kinds of "ids/parameters" are in play — easy to mix up:
- Logical IDs (in code): every construct gets one
(
new MultiStepCheck('roar-repro-race', {...}),logicalId: 'roar-repro-ISSUE-123'in the config). They are the stable keys the CLI uses to match code to account state — re-deploying with the same logical IDs updates the same checks in place; changing one creates a new check. This is what makes the project idempotent and safe to re-deploy from CI. - Account environment variables (your secrets/config): set once with the CLI,
available as
process.env.*inside every check run:Never put credentials in the check code — it's committed and visible in the UI.npx checkly env add REPRO_API_KEY <key-for-the-target-api> --secret npx checkly env add REPRO_TARGET_API https://api.yourservice.example npx checkly env add REPRO_ISSUE_ID <error-tracker issue id> npx checkly env ls # verify (secrets print empty)
- Runtime-provided variables (Checkly sets these per run):
CHECK_RESULT_IDis the reliable per-run id (we use it as the run id in every header). Others —CHECK_RUN_ID,CHECK_NAME,REGION, and a runtimeCHECKLY_TRACEPARENT— vary by run type (ad-hoc test session vs scheduled run), sorepro-context.tsfalls back tolocal/ a random UUID and the same specs also run under plainnpx playwright test. (We mint our own trace id rather than reusing the runtime's so the per-run id in our output lines is guaranteed to match what the target saw.)
Check-level parameters worth setting deliberately (see
repro.check.ts):
retryStrategy: RetryStrategyBuilder.noRetries()on the group and every check — repro checks are supposed to fail; the default double-check re-runs every failure and doubles your noise and cost.frequency: Frequency.EVERY_1H,locations: ['eu-west-1']— run hourly from the same region as the original incident.activated: true, muted: truewhile you iterate (runs, but never alerts); flipmuted: falseonce you want it to page — the template ships muted.tagscarry the issue id so the UI maps back to the error tracker.- Account env vars are account-wide, not per-project: prefix yours per incident
(or use a dedicated account) so
REPRO_API_KEYdoesn't collide with a colleague's. Also noteenv add <key> <value>puts the secret in your shell history — mint throwaway keys and delete them after.
cd template && npm install
npx checkly test --record --verboseWhat you get back, and where we read each piece:
- stdout: a live
✔/✖line per check, plus everything your specconsole.logs — this is why the template printsROAR_REPRO_CONTEXT/ROAR_REPRO_ROUND/ROAR_REPRO_RESULTas single-line JSON. An agent (or a script) parses the verdict and the per-request ids out of the CLI output:ROAR_REPRO_RESULT {"verdict":"REPRODUCED","put500s":[{"round":0,"requestId":"…"}…]}. Parsing caveats: pass--verboseor passing checks print no logs at all; the output contains ANSI color codes (strip them); most marker lines are prefixed withHH:MM:SS INFO, so match theROAR_REPRO_*marker mid-line, not line-start. - exit code: non-zero if any check failed — usable directly as a CI gate
(
checkly testin a PR workflow is exactly that). --record: persists the run as a test session and prints web links — a "View result" link per failing check (full Playwright trace, every request/response; passing checks only appear in the session summary) and a session summary link. Put these links in the PR.
checkly test is how we proved REPRODUCED (red race + green control) before
deploying anything.
npx checkly deploy # idempotent; add --force to skip the confirmation promptThis creates the group + checks as standing monitors that run on their schedule.
Scheduled-run results arrive in the dashboard and via your alert channels; for
scripted verification use the CLI's built-in API pass-through (checkly api — handles
auth from your login session or CHECKLY_API_KEY/CHECKLY_ACCOUNT_ID, no curl/headers
needed):
# find your group (the groups list is short; /v1/checks paginates at 10 by default,
# so tag-filtering the flat checks list silently misses checks in a real account)
npx checkly api /v1/check-groups --jq '.[] | select(.tags | index("roar")) | {id, name}'
# then list exactly its checks
npx checkly api /v1/check-groups/<group id>/checks --jq '.[] | {id, name, muted, retryStrategy, doubleCheck}'Expected output for RetryStrategyBuilder.noRetries(): "retryStrategy": null plus
"doubleCheck": false — null here means "no retries", not "unset". --jq needs a
local jq install (drop the flag to get raw JSON). checkly api works for any
endpoint and method (-X POST -F name=… -F activated:=true, --input body.json);
the OpenAPI spec is at https://api.checklyhq.com/openapi.json.
The gate is the same repro pointed at your staging environment of the target
service — if your main branch auto-deploys to staging, the check is red before the
fix merges and green after, so the loop closes without a production release. The
simplest setup needs nothing new: copy the project to a second directory (or just a
second logicalId, e.g. roar-repro-ISSUE-123-staging), set its target env var to
your staging API, and test/deploy as before:
npx checkly env add REPRO_TARGET_API_STAGING https://api-staging.yourservice.example
# (and read that var instead of REPRO_TARGET_API in repro-context.ts for the staging copy)
npx checkly test --record
npx checkly deployBoth copies live in the same Checkly account; only the target URL differs.
If your org separates Checkly accounts per environment, the CLI retargets with env
vars instead of a login: CHECKLY_API_KEY=… CHECKLY_ACCOUNT_ID=… npx checkly deploy
deploys the identical project (same code, same logical IDs) into that other account.
There's a good case for keeping the checks forever: once the fix ships they're a green regression monitor for a failure mode you know is reachable, they're muted or cheap to run hourly, and the group name maps straight back to the incident. Keeping them is the default we'd recommend — just unmute and wire an alert channel once green.
If you do want them gone (incident closed, probe superseded, account hygiene):
npx checkly destroy --force # deletes everything this project deployed (by logicalId)
npx checkly env rm REPRO_API_KEY --force
npx checkly env rm REPRO_TARGET_API --force
npx checkly env rm REPRO_ISSUE_ID --force
# verify nothing tagged yours remains
npx checkly api /v1/check-groups --jq '.[] | select(.tags | index("roar"))'destroy removes only resources belonging to this project's logicalId — other
projects in the account are untouched.
- Don't set
playwrightConfigPathincheckly.config.tsfor a MultiStepCheck-only project — it additionally creates an implicit Playwright-suite check that runs all your specs a second time. checkly testruns in the cloud: the target API must be reachable from public internet (or use a Checkly private location).- Secret env vars print empty in
env ls— that's expected, not a failed write.
Sentry hosts an official MCP server. In Claude Code:
claude mcp add --transport http sentry https://mcp.sentry.dev/mcp
# then run /mcp inside the session and complete the browser OAuthThe tools you'll actually use in the loop:
get_sentry_resource(resourceType='event', resourceId=<event id>)— event → issue, stack frames, trace idsearch_events(dataset='errors', query='trace:<your trace id>')— the verify query: everything one check run causedsearch_issue_events(issueId=…, query='timestamp:>…')— events in a windowget_issue_details(eventId=…)— per-event message / URL / trace ids
Two caveats we hit: the MCP does not expose request headers on events (join by trace id instead), and identical root causes can fingerprint into different issues depending on the route's error wrapping — match on message + in-app frame + your trace id, never on issue membership.
Any log store with an HTTP query API works; there is also an official Grafana MCP if
you prefer. We used a service-account token and the Loki query_range endpoint
directly:
START=$(date -v-15M +%s)000000000; END=$(date +%s)000000000
curl -s -G -H "Authorization: Bearer $GRAFANA_TOKEN" \
"https://<your-stack>.grafana.net/api/datasources/proxy/uid/grafanacloud-logs/loki/api/v1/query_range" \
--data-urlencode 'query={app="your-api", env="prod"} |= "roar_run"' \
-d limit=100 -d "start=$START" -d "end=$END"Check what your request logs actually contain before you need them: ours had
method/path/query-string/status/duration but no trace id and no headers. That's
why the template duplicates the correlation ids into query params (roar_run=…) on
the routes that tolerate unknown params (strict input validation rejects them on
others — probe per route), and why the fallback join is path resource id + account +
timestamp.
- Pick one real error-tracker issue with a request path in it.
- Read the in-app frames + their git history before designing the probe (the trigger is often already written down in an old PR).
- Adapt the template, run
checkly test --record, and require: repro check red, control check green. - Take the
traceIdfrom theROAR_REPRO_CONTEXToutput line and query your error tracker fortrace:<id>. Only call it reproduced when the message + frame match the original issue. - Fix it; put the check ids, run links and event table in the PR description; deploy the same project to staging as the gate; re-run after the fix deploys and verify by error message, not by check color.
The full phase-by-phase procedure, including the diagnosis and PR conventions, is in
CASE-STUDY.md and LESSONS.md.
Once the manual loop works, the automation is mostly prompt engineering: we condensed
the procedure into a reusable agent skill (a markdown playbook the agent loads when an
alert comes in) that encodes phases, tool calls, and the guardrails from
LESSONS.md — deploy repro checks with retries off, always ship a control, never
claim "reproduced" without the trace-id match, verify per message. Start there before
building anything heavier; the expensive part is the judgment in CORRELATE and
DIAGNOSE, and a well-briefed agent with these three integrations covers it.