{"title":"Runheed, Error Tracking My Agent Can Read","html_url":"https://deallocatedobjects.com/posts/runheed-error-tracking-my-agent-can-read","source_url":"","markdown_content":"A lot of the code running in my apps now was written by an agent. When that code throws in production, the usual path back to a fix goes through me: an alert email, a dashboard, a stack trace I copy into a terminal, and then an explanation of what I think happened. The agent that's about to fix it is the one that needs the details, and it's the one furthest from them.\n\nSo this week I launched [Runheed](https://runheed.com). It's error tracking built so your coding agent can read it. Your app reports exceptions to it, and Claude Code, Cursor or any other MCP client can ask \"what broke in production since yesterday's deploy?\", read the full context, fix the bug and mark it resolved.\n\n### How the Loop Works\n\nThere are four steps, and only the first one is your app's job.\n\n1. **Your app throws.** The SDK sends the exception with its stack trace, the breadcrumbs leading up to it, request details and, if you set it, the agent session that wrote the code.\n2. **Runheed groups it.** Repeats fold into one issue, so the agent sees \"TypeError in checkout.ts:42, 318 events\" instead of 318 separate reports.\n3. **Your agent reads it.** It searches errors and pulls the full context through MCP.\n4. **Your agent closes it.** Once the fix ships, the agent marks the error resolved with a note about the fix. If the same error comes back, it reopens on its own.\n\nThat last part is a small rule in the ingest path. When an event arrives for a group that was marked resolved, the group goes back to unresolved. A regression shows up the next time the agent checks, with no one needing to remember the old issue.\n\n### Grouping\n\nGrouping is the part that decides whether an error tracker is pleasant or exhausting, and for an agent it matters even more. Every duplicate it has to read costs context.\n\nRunheed fingerprints each event from the error type, the message and the first stack frame that belongs to your own code:\n\n```ts title=\"src/app/api/ingest/route.ts\"\nfunction generateFingerprint(event: IngestEvent): string {\n  const parts: string[] = [event.errorType, event.errorMessage];\n\n  // Add first in-app stack frame if available\n  if (event.stacktrace && event.stacktrace.length > 0) {\n    const inAppFrame = event.stacktrace.find((frame) => frame.inApp !== false) || event.stacktrace[0];\n    if (inAppFrame.filename) parts.push(inAppFrame.filename);\n    if (inAppFrame.function) parts.push(inAppFrame.function);\n    if (inAppFrame.lineno !== undefined) parts.push(String(inAppFrame.lineno));\n  }\n  // ...hashed into the group's fingerprint\n}\n```\n\nUsing the first in-app frame matters. Plenty of errors surface deep inside a framework or a library, and grouping on the top frame would lump unrelated bugs together under some line in `node_modules`. The frame in your code is the one the agent needs to open anyway. If you know better for a particular error, you can pass your own fingerprint array and Runheed hashes that instead.\n\n### What the Agent Gets\n\nMost error trackers are designed for a person looking at a dashboard. I picked the fields an agent needs to go from report to fix without asking me anything:\n\n- The stack trace with file, function, line and column for each frame, with your own code marked so the agent opens the right file first.\n- Agent context: which agent and version wrote the code, the session id, and which tool or action was running when it failed.\n- Breadcrumbs, the steps your code recorded before the failure, like requests, queries and log lines.\n- Environment and release, so \"since yesterday's deploy\" means something.\n\nOn the MCP side there are nine tools, all prefixed `runheed_`: search errors, get one error, get the most recent ones, get trends over 24 hours, 7 days or 30 days, get the full context for an error, resolve, ignore, unignore, and a project health check. The health tool gives a score from 0 to 100 based on unresolved issues and the last 24 hours of errors, plus a few recommendations, which makes it a nice thing for an agent to call right after a deploy.\n\nRecording which agent wrote the code is one line in the SDK:\n\n```ts title=\"instrument.ts\"\nimport runheed from 'runheed';\n\nrunheed.init({ dsn: process.env.RUNHEED_DSN, environment: process.env.NODE_ENV, release: process.env.GIT_SHA });\nrunheed.setAgent({ name: 'claude-code', sessionId: process.env.AGENT_SESSION_ID });\n```\n\nThere's a Python SDK with FastAPI, Django and Flask hooks, an Express error handler for Node, and a plain JSON endpoint for any other language.\n\n### Setup Your Agent Can Do\n\nMy favorite file on the site is [runheed.com/SETUP.txt](https://runheed.com/SETUP.txt). It's setup instructions written for a coding agent. You point your agent at it and it does almost everything: it looks at the repo to decide between Node, Python or the HTTP endpoint, installs the SDK, adds the init call to the earliest-loaded server file, wires up the framework hook, triggers a test error to confirm it shows up, removes the test code, and then connects itself over MCP.\n\nThe only things it asks you for are the parts that need a person: signing in, creating a project and pasting it the DSN, and creating an agent key in settings. The npm and PyPI releases aren't out yet, so for now the SDK and MCP server install straight from runheed.com, and the setup file has the exact commands.\n\n### Being Careful With What Comes In\n\nError reports tend to collect things that shouldn't be stored. Authorization, cookie and API key headers are redacted when an event arrives, before anything is written. Agent keys are hashed in the database and shown to you once when you create them. Events older than your plan's history window are deleted by a daily job, so old reports don't pile up forever.\n\n### Where It Fits\n\nRunheed is for people who already let agents write and ship a good share of their code. The fix loop gets shorter when the agent can read production errors itself: you ask what's new, it reads the grouped errors with their context, and it can come back with a fix and a test. If you want your agent to see what its code is doing out in the world, give [Runheed](https://runheed.com) a try.","html_content":"<p>A lot of the code running in my apps now was written by an agent. When that code throws in production, the usual path back to a fix goes through me: an alert email, a dashboard, a stack trace I copy into a terminal, and then an explanation of what I think happened. The agent that’s about to fix it is the one that needs the details, and it’s the one furthest from them.</p>\n<p>So this week I launched <a href=\"https://runheed.com\">Runheed</a>. It’s error tracking built so your coding agent can read it. Your app reports exceptions to it, and Claude Code, Cursor or any other MCP client can ask “what broke in production since yesterday’s deploy?”, read the full context, fix the bug and mark it resolved.</p>\n<h2 id=\"how-the-loop-works\" data-level=\"3\">How the Loop Works</h2>\n<p>There are four steps, and only the first one is your app’s job.</p>\n<ol>\n<li><strong>Your app throws.</strong> The SDK sends the exception with its stack trace, the breadcrumbs leading up to it, request details and, if you set it, the agent session that wrote the code.</li>\n<li><strong>Runheed groups it.</strong> Repeats fold into one issue, so the agent sees “TypeError in checkout.ts:42, 318 events” instead of 318 separate reports.</li>\n<li><strong>Your agent reads it.</strong> It searches errors and pulls the full context through MCP.</li>\n<li><strong>Your agent closes it.</strong> Once the fix ships, the agent marks the error resolved with a note about the fix. If the same error comes back, it reopens on its own.</li>\n</ol>\n<p>That last part is a small rule in the ingest path. When an event arrives for a group that was marked resolved, the group goes back to unresolved. A regression shows up the next time the agent checks, with no one needing to remember the old issue.</p>\n<h2 id=\"grouping\" data-level=\"3\">Grouping</h2>\n<p>Grouping is the part that decides whether an error tracker is pleasant or exhausting, and for an agent it matters even more. Every duplicate it has to read costs context.</p>\n<p>Runheed fingerprints each event from the error type, the message and the first stack frame that belongs to your own code:</p>\n<figure class=\"code\" data-lang=\"typescript\"><figcaption><span class=\"file\">src/app/api/ingest/route.ts</span><span class=\"lang\">TypeScript</span></figcaption><pre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">function</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> generateFingerprint</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"--shiki-light:#E36209;--shiki-dark:#FFAB70\">event</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> IngestEvent</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> string</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> parts</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> string</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">[] </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> [event.errorType, event.errorMessage];</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#6A737D;--shiki-dark:#6A737D\">  // Add first in-app stack frame if available</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> (event.stacktrace </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> event.stacktrace.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> ></span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> inAppFrame</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> event.stacktrace.</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">find</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"--shiki-light:#E36209;--shiki-dark:#FFAB70\">frame</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> frame.inApp </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">!==</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> event.stacktrace[</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">];</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> (inAppFrame.filename) parts.</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">push</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(inAppFrame.filename);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> (inAppFrame.function) parts.</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">push</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(inAppFrame.function);</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> (inAppFrame.lineno </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">!==</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> undefined</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">) parts.</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">push</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">String</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(inAppFrame.lineno));</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">  }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#6A737D;--shiki-dark:#6A737D\">  // ...hashed into the group's fingerprint</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">}</span></span></code></pre></figure><p>Using the first in-app frame matters. Plenty of errors surface deep inside a framework or a library, and grouping on the top frame would lump unrelated bugs together under some line in <code>node_modules</code>. The frame in your code is the one the agent needs to open anyway. If you know better for a particular error, you can pass your own fingerprint array and Runheed hashes that instead.</p>\n<h2 id=\"what-the-agent-gets\" data-level=\"3\">What the Agent Gets</h2>\n<p>Most error trackers are designed for a person looking at a dashboard. I picked the fields an agent needs to go from report to fix without asking me anything:</p>\n<ul>\n<li>The stack trace with file, function, line and column for each frame, with your own code marked so the agent opens the right file first.</li>\n<li>Agent context: which agent and version wrote the code, the session id, and which tool or action was running when it failed.</li>\n<li>Breadcrumbs, the steps your code recorded before the failure, like requests, queries and log lines.</li>\n<li>Environment and release, so “since yesterday’s deploy” means something.</li>\n</ul>\n<p>On the MCP side there are nine tools, all prefixed <code>runheed_</code>: search errors, get one error, get the most recent ones, get trends over 24 hours, 7 days or 30 days, get the full context for an error, resolve, ignore, unignore, and a project health check. The health tool gives a score from 0 to 100 based on unresolved issues and the last 24 hours of errors, plus a few recommendations, which makes it a nice thing for an agent to call right after a deploy.</p>\n<p>Recording which agent wrote the code is one line in the SDK:</p>\n<figure class=\"code\" data-lang=\"typescript\"><figcaption><span class=\"file\">instrument.ts</span><span class=\"lang\">TypeScript</span></figcaption><pre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> runheed </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> 'runheed'</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">runheed.</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">init</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">({ dsn: process.env.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">RUNHEED_DSN</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">, environment: process.env.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">NODE_ENV</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">, release: process.env.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">GIT_SHA</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> });</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">runheed.</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">setAgent</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">({ name: </span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">'claude-code'</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">, sessionId: process.env.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">AGENT_SESSION_ID</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> });</span></span></code></pre></figure><p>There’s a Python SDK with FastAPI, Django and Flask hooks, an Express error handler for Node, and a plain JSON endpoint for any other language.</p>\n<h2 id=\"setup-your-agent-can-do\" data-level=\"3\">Setup Your Agent Can Do</h2>\n<p>My favorite file on the site is <a href=\"https://runheed.com/SETUP.txt\">runheed.com/SETUP.txt</a>. It’s setup instructions written for a coding agent. You point your agent at it and it does almost everything: it looks at the repo to decide between Node, Python or the HTTP endpoint, installs the SDK, adds the init call to the earliest-loaded server file, wires up the framework hook, triggers a test error to confirm it shows up, removes the test code, and then connects itself over MCP.</p>\n<p>The only things it asks you for are the parts that need a person: signing in, creating a project and pasting it the DSN, and creating an agent key in settings. The npm and PyPI releases aren’t out yet, so for now the SDK and MCP server install straight from runheed.com, and the setup file has the exact commands.</p>\n<h2 id=\"being-careful-with-what-comes-in\" data-level=\"3\">Being Careful With What Comes In</h2>\n<p>Error reports tend to collect things that shouldn’t be stored. Authorization, cookie and API key headers are redacted when an event arrives, before anything is written. Agent keys are hashed in the database and shown to you once when you create them. Events older than your plan’s history window are deleted by a daily job, so old reports don’t pile up forever.</p>\n<h2 id=\"where-it-fits\" data-level=\"3\">Where It Fits</h2>\n<p>Runheed is for people who already let agents write and ship a good share of their code. The fix loop gets shorter when the agent can read production errors itself: you ask what’s new, it reads the grouped errors with their context, and it can come back with a fix and a test. If you want your agent to see what its code is doing out in the world, give <a href=\"https://runheed.com\">Runheed</a> a try.</p>","created_at":"2026-10-10T13:00:00.000Z","updated_at":"2026-10-10T13:00:00.000Z"}