Skip to content
HostStack Docs

Error Tracking

Uptime checks tell you a service stopped answering. Error tracking tells you what threw, where, on which release, and how many people hit it. Your application posts exceptions to an ingest endpoint, HostStack groups them into issues, alerts you when a new one appears or a fixed one comes back — and because HostStack also runs your dev box with the repository checked out, any issue can become an agent task with one click.

Set it up

Two steps: mint a key, then paste a handler. There is no SDK to install and nothing to keep up to date — reporting is one HTTP POST.

  1. Open your service, go to the Errors tab, and press New key. Copy it — it is shown once and stored only as a hash.
  2. Put the endpoint in your service’s environment as HOSTSTACK_ERRORS_URL:
    https://hoststack.dev/api/ingest/errors/<your-key>
  3. Paste the handler for your runtime below and deploy.

Node / Bun

javascript
const HOSTSTACK_ERRORS = process.env.HOSTSTACK_ERRORS_URL;

async function report(err, context = {}) {
  try {
    await fetch(HOSTSTACK_ERRORS, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        events: [{
          type: err.name ?? 'Error',
          value: err.message ?? String(err),
          stack: err.stack,
          level: 'error',
          context,
        }],
      }),
    });
  } catch {
    // Never let the reporter take down the process it is reporting for.
  }
}

process.on('uncaughtException', (err) => void report(err));
process.on('unhandledRejection', (err) => void report(err));

Python

python
import json, os, sys, traceback, urllib.request

HOSTSTACK_ERRORS = os.environ["HOSTSTACK_ERRORS_URL"]

def report(exc_type, exc, tb):
    payload = {"events": [{
        "type": exc_type.__name__,
        "value": str(exc),
        "stack": "".join(traceback.format_exception(exc_type, exc, tb)),
        "level": "error",
    }]}
    req = urllib.request.Request(
        HOSTSTACK_ERRORS,
        data=json.dumps(payload).encode(),
        headers={"content-type": "application/json"},
    )
    try:
        urllib.request.urlopen(req, timeout=3).read()
    except Exception:
        pass  # never let the reporter raise

sys.excepthook = report

The wire format

Everything except type is optional. Batch up to 100 events per request — a crashing process emits in bursts, and one request per exception turns your incident into a second one.

bash
curl -X POST https://hoststack.dev/api/ingest/errors/ing_your_key_here \
  -H 'content-type: application/json' \
  -d '{
    "release": "9f3c1ab",
    "environment": "production",
    "events": [{
      "type": "TypeError",
      "value": "cart.total is not a function",
      "stack": "    at checkout (/app/src/checkout.ts:12:9)",
      "level": "error",
      "requestId": "req_9f3c",
      "context": { "url": "/checkout", "method": "POST" },
      "user": { "id": "customer-777" }
    }]
  }'

release

Commit SHA. Omitted, it defaults to the commit of the service’s live deploy — which is right far more often than not, and is what makes “resolved in X, came back in Y” work without you doing anything.

user.id

Hashed with a per-team salt before it is stored, so “how many people hit this” is answered without us keeping who they are. The identifier itself is never written to disk.

fingerprint

An escape hatch. Send one and it overrides our grouping entirely — useful when a generic wrapper error collapses several distinct bugs into one issue.

context

Any JSON. Values under keys like password, token or authorization are redacted before anything is stored — but do not put secrets in it.

How errors are grouped

An issue is one distinct problem, not one event. The fingerprint is computed once at ingest from three things:

  • The exception class — a TypeError and a RangeError from the same line are different bugs.
  • The message with its variables removed — so user 41 not found and user 9002 not found are one issue rather than two hundred. Numbers, UUIDs, paths, URLs, emails, timestamps and quoted strings are all normalised.
  • The topmost stack frame in your own code — not the framework’s. Grouping on the top frame overall would file every error in your router; grouping on your frame files it at the line that needs editing.

A fingerprint is never recomputed. If we change how grouping works, it applies to new events — your existing issues are not silently rewritten underneath you.

Counts are exact, samples are not

An issue that fired 40,000 times says 40,000. Behind it we keep a bounded number of full occurrences — the stack, the request, the release — rather than all 40,000, because a table that keeps everything is a table that eventually takes the platform down with it. Occurrences age out after 30 days; the issue and its counts stay.

Each service can ingest 10,000 events per hour. Over that we keep counting and stop storing, and the issue says so — droppedCount is how many were counted but not kept. You are never quietly told a smaller number than the truth.

Alerts

Two events go through your existing Slack, Discord and email channels:

  • error.issue_new — an exception this service has never reported before. Not critical by default: new issues are common and frequently trivial. Capped at five notifications per service per hour, because a deploy that breaks a shared module creates fifty new issues in a minute and fifty messages is how a channel gets muted.
  • error.issue_regressed — an issue you marked resolved has happened again. Critical by default: that is a statement about a shipped fix, not about a new bug.

Resolving an issue records the release it was resolved in, which is what makes a regression legible. Ignoring one keeps counting and stops telling you — an ignored issue never reopens itself, so use it for noise you have decided to live with, not for something you intend to fix.

Fix it in a dev box

This is the part a hosted error tracker cannot do. HostStack already runs your dev box with the repository checked out, so an issue can become a briefed agent task: the exception, the stack with your frames marked, the release it happened on, how often and to how many users, and a real request context — plus hard boundaries. The agent works on a branch, is told not to deploy, not to delete tests, not to silence the error instead of fixing it, and to stop and say so if the cause turns out not to be in your repository.

The button appears only for issues we can attribute to your code. When every frame is inside a dependency, when the browser sent nothing but Script error., or when a browser extension threw it, you get an explanation instead — an agent told to fix something it cannot reach does not refuse, it writes a confident and useless diff.

It writes the task; it does not start the agent. Running spends your own agent tokens, and that is a separate decision made in the box.

From the CLI

bash
# Mint the write-only key your app reports with (shown once)
hoststack errors keys new 48

# What is broken right now
hoststack errors list --service 48

# Read one issue, with the stack traces behind it
hoststack errors show 12

# Hand it to a coding agent in this project's dev box
hoststack errors fix 12

# Fixed it? Resolve it — you get told if it comes back
hoststack errors resolve 12

The same surface exists in the SDK (client.errors) and over MCP (list_error_issues, get_error_issue, update_error_issue, fix_error_in_dev_box), so an agent can triage production errors without a human relaying them.

About the ingest key

The ingest key is not an API key and must not be treated like one in either direction. It can create error events for exactly one service and can do nothing else — it cannot read the issues it created, list your services, or touch anything else on your team. That is why shipping it inside your application, including a browser bundle where it is world-readable, is expected rather than a mistake.

It is stored as a SHA-256 hash, so it is shown exactly once. Rotating one has no downtime: create a second key, deploy with it, then revoke the first. A revoked key stops working immediately. A service can hold up to five.

Deleting a service deletes its issues and occurrences with it.

Essential cookies only — for login sessions. No tracking. Details