Docs
Send a crash, get a root cause back.
Everything below is the real interface — the same one the dashboard and the live demo use, not a simplified rewrite of it. Sending a crash means sending its error and stack trace to a third-party AI model — see what's collected and who sees it before you send anything sensitive.
Install
SDK for Go
The Go SDK ships as the chronexis-go module (currently v0.2.2) and wraps automatic panic-recovery middleware around your existing handler.
go get github.com/Alraies97/chronexis-go
import chronexis "github.com/Alraies97/chronexis-go"
handler := chronexis.Middleware(
"<YOUR_CHRONEXIS_API_KEY>", // find this on Settings after you sign up
"https://chronexis.dedyn.io/v1/traces",
)(yourExistingHandler)
http.ListenAndServe(":8080", handler)
Any panic inside yourExistingHandler is now caught and
reported. The request it happened on still fails with a 500,
exactly as it did before — the middleware reports the panic, it
doesn't swallow it.
Flush before your process exits
Reports are sent in the background, so a process that exits immediately loses whatever is still queued. Hold the service and flush it during shutdown.
service := chronexis.New(apiKey, ingestURL)
handler := chronexis.Middleware(apiKey, ingestURL)(yourExistingHandler)
// ... on SIGTERM, after srv.Shutdown(ctx):
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
service.Flush(ctx)
As of v0.2.1, Flush waits for reports to be
actually delivered, not merely dequeued. It only waits on what was
captured before the call, so it terminates even while your service is
still handling traffic.
Background jobs and goroutines
Cron tasks, queue workers and goroutines have no incoming request for the middleware to wrap. Initialize once, then report errors directly.
chronexis.Init(apiKey, ingestURL) // once, at startup
func runJob() {
if err := doWork(); err != nil {
chronexis.CaptureError(err)
}
}
CaptureError attaches the current stack trace itself.
Called before Init or Middleware has run, it
logs a warning locally and drops the report rather than panicking.
SDK for Python
The Python SDK isn't on PyPI yet — install it straight from the
repo. Unlike the Go middleware, it doesn't hook into a web framework
automatically: call capture_error yourself wherever you
already catch exceptions.
pip install git+https://github.com/Alraies97/chronexis-python.git
from chronexis import Chronexis
sdk = Chronexis(
api_key="<YOUR_CHRONEXIS_API_KEY>", # find this on Settings after you sign up
endpoint="https://chronexis.dedyn.io/v1/traces",
)
try:
handle_request()
except Exception as exc:
sdk.capture_error(str(exc), path=request.path, method=request.method)
raise
capture_error only buffers — it returns immediately
and a background thread does the sending. Shut the SDK down once, when
your process is stopping:
sdk.close() # flushes what's buffered, then stops the worker thread
How delivery works
Both SDKs are built to sit in a live request path, which means the reporting call itself must never become the second incident. Capturing a crash never blocks your code — it appends to a bounded in-memory buffer and returns. Everything below happens on background workers.
| Go | Python | |
|---|---|---|
| Sending | 4 worker goroutines | 1 background thread, 0.2s tick |
| Buffer | 256 pending reports | 1000 pending reports |
| When the buffer is full | drops the newest, logs locally | drops the newest, calls on_drop |
| Retries | 2 attempts, 250ms backoff, doubling | none — a failed send is dropped |
| HTTP timeout | 5s | 5s |
On 429 | pause, dropping what the workers pick up meanwhile | pause, holding queued reports until it elapses |
A 429 is never retried in place — the request that
just happened is precisely what the server asked us to stop doing, so
retrying it would amplify the problem rather than recover from it. The
Go SDK reads Retry-After as integer seconds, then as an
HTTP-date (the form a fronting proxy emits), then as a
retry_after field in the JSON body, falling back to 60
seconds — the length of the server's own rate-limit window. The
two SDKs differ on what happens to the backlog: Go drops each report a
worker picks up while paused, trading those reports for a guarantee it
won't queue without bound; Python holds them and sends them when the
pause lifts. Both reject an implausible Retry-After rather
than clamping it — a server claiming "wait a week" is
malfunctioning, not informing you — and both draw that line at 24
hours (max_retry_delay in Python), which is what a
daily-quota 429 needs when it pauses you until UTC midnight.
If nothing parses, both fall back to a 60-second pause, matching the
minute window.
Tuning the Go defaults
| Option | Default | Purpose |
|---|---|---|
| WithBufferSize(n) | 256 | Pending reports held before new ones are dropped. |
| WithWorkers(n) | 4 | Goroutines draining the buffer. |
| WithMaxRetries(n) | 2 | Send attempts before giving up. |
| WithBaseRetryDelay(d) | 250ms | First backoff delay; doubles each retry. |
| WithTimeout(d) | 5s | Per-request HTTP timeout. |
| WithOnDrop(fn) | none | Called when a report is dropped, with the reason. |
What the Python SDK will not catch
HTTP API
Every route below takes your API key in an X-API-Key header. A trace that doesn't exist and a trace that belongs to someone else return the identical 404 — the response never confirms or denies which.
| Route | Notes |
|---|---|
| POST /v1/traces | Ingest a crash. 202 on success, 401 bad key, 402 trial expired, 429 rate limited. |
| GET /v1/traces | List your traces, paginated (limit, default 50, max 200; offset; status; since, RFC3339). Returns {traces, total, limit, offset}. |
| GET /v1/traces/usage | Read-only rate-limit snapshot — checking it never consumes quota. {plan_type, trial_ends_at, minute:{...}, day:{...}}. |
| GET /trace/{id} | The raw captured record for one trace. |
| GET /trace/{id}/analysis | The stored diagnosis. Reads what's already there — does not trigger a new AI call. Three possible shapes, see below. |
What a trace contains
The error and stack trace are kept exactly as sent — nothing sampled or summarized before the AI sees it. Once diagnosed, GET /trace/{id}/analysis adds six fields:
| error_summary | Plain-language description of what went wrong. |
| root_cause_analysis | Why it actually happened, not just what the stack trace shows. |
| file_and_line | Exact source location. |
| remediation_patch | A suggested fix. |
| prevention_strategy | How to stop the same class of bug recurring. |
| model | Which model produced the diagnosis. |
The three analysis states
GET /trace/{id}/analysis is always one of exactly three shapes — there's no fourth "something went wrong, try again" case to handle separately.
| Diagnosed | The six fields above, plus model. |
| Pending | {"detail": "diagnosis not yet available"} — still processing. |
| Failed | {"status": "analysis_failed", "error": ..., "raw_content": ..., "model": ...} — the model returned something that couldn't be parsed as a diagnosis. raw_content and model may each be absent. |
Rate limits
| Plan | Per minute | Per day |
|---|---|---|
| Trial | 60 | 5,000 |
| Standard | 600 | 500,000 |
| Pro | 600 | 500,000 |
| Max | 600 | 500,000 |
| Enterprise | 6,000 | unlimited |
Standard, Pro and Max share the same ingest limits on purpose —
those tiers differ by how many diagnoses you get and which model runs
them, not by how fast you can send crashes. Exceeding either window
returns 429 with a Retry-After; the official
SDKs honor it for you (see how delivery works).
Full plan details, including diagnosis caps and pricing, are on the pricing section.