Add prompts and scripts for automatic crash repro and fix (#49063)

These prompts can be used to automatically diagnose and fix crashes
report in Sentry.

Usage:
1. Find a crash in Sentry. It will have an ID like ZED-123
2. In an agent, do a prompt like `Follow the instructions in
@investigate.md to investigate ZED-123`
3. Once the agent finds a repro, fix it in a new thread by saying
`Follow the instructions in @fix.md`

Release Notes:
- N/A

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
This commit is contained in:
Eric Holk
2026-02-12 12:42:41 -08:00
committed by GitHub
parent 45cd96182f
commit 889d0db8e1
3 changed files with 530 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
# Crash Fix
You are fixing a crash that has been analyzed and has a reproduction test case. Your goal is to implement a minimal, correct fix that resolves the root cause and makes the reproduction test pass.
## Inputs
Before starting, you should have:
1. **ANALYSIS.md** — the crash analysis from the investigation phase. Read it thoroughly.
2. **A failing test** — a reproduction test that triggers the crash. Run it first to confirm it fails as expected.
If either is missing, ask the user to provide them or run the investigation phase first (`/prompt crash/investigate`).
## Workflow
### Step 1: Confirm the Failing Test
Run the reproduction test and verify it fails with the expected crash:
```
cargo test -p <crate> <test_name>
```
Read the failure output. Confirm the panic message and stack trace match what ANALYSIS.md describes. If the test doesn't fail, or fails differently than expected, stop and reassess before proceeding.
### Step 2: Understand the Fix
Read the "Suggested Fix" section of ANALYSIS.md and the relevant source code. Before writing any code, be clear on:
1. **What invariant is being violated** — what property of the data does the crashing code assume?
2. **Where the invariant breaks** — which function produces the bad state?
### Step 3: Implement the Fix
Apply the minimal change needed to resolve the root cause. Guidelines:
- **Fix the root cause, not the symptom.** Don't just catch the panic with a bounds check if the real problem is an incorrect offset calculation. Fix the calculation.
- **Preserve existing behavior** for all non-crashing cases. The fix should only change what happens in the scenario that was previously crashing.
- **Don't add unnecessary changes.** No drive-by improvements, keep the diff focused.
- **Add a comment only if the fix is non-obvious.** If a reader might wonder "why is this check here?", a brief comment explaining the crash scenario is appropriate.
- **Consider long term maintainability** Please make a targeted fix while being sure to consider the long term maintainability and reliability of the codebase
### Step 4: Verify the Fix
Run the reproduction test and confirm it passes:
```
cargo test -p <crate> <test_name>
```
Then run the full test suite for the affected crate to check for regressions:
```
cargo test -p <crate>
```
If any tests fail, determine whether the fix introduced a regression. Fix regressions before proceeding.
### Step 5: Run Clippy
```
./script/clippy
```
Address any new warnings introduced by your change.
### Step 6: Summarize
Write a brief summary of the fix for use in a PR description. Include:
- **What was the bug** — one sentence on the root cause.
- **What the fix does** — one sentence on the change.
- **How it was verified** — note that the reproduction test now passes.
- **Sentry issue link** — if available from ANALYSIS.md.
We use the following template for pull request descriptions. Please add information to answer the relevant sections, especially for release notes.
```
<Description of change, what the issue was and the fix.>
Release Notes:
- N/A *or* Added/Fixed/Improved ...
```
+89
View File
@@ -0,0 +1,89 @@
# Crash Investigation
You are investigating a crash that was observed in the wild. Your goal is to understand the root cause and produce a minimal reproduction test case that triggers the same crash. This test will be used to verify a fix and prevent regressions.
## Workflow
### Step 1: Get the Crash Report
If given a Sentry issue ID (like `ZED-4VS` or a numeric ID), there are several ways to fetch the crash data:
**Option A: Sentry MCP server (preferred if available)**
If the Sentry MCP server is configured as a context server, use its tools directly (e.g., `get_sentry_issue`) to fetch the issue details and stack trace. This is the simplest path — no tokens or scripts needed.
**Option B: Fetch script**
Run the fetch script from the terminal:
```
script/sentry-fetch <issue-id>
```
This reads authentication from `~/.sentryclirc` (set up via `sentry-cli login`) or the `SENTRY_AUTH_TOKEN` environment variable.
**Option C: Crash report provided directly**
If the crash report was provided inline or as a file, read it carefully before proceeding.
### Step 2: Analyze the Stack Trace
Read the stack trace bottom-to-top (from crash site upward) and identify:
1. **The crash site** — the exact function and line where the panic/abort occurs.
2. **The immediate cause** — what operation failed (e.g., slice indexing on a non-char-boundary, out-of-bounds access, unwrap on None).
3. **The relevant application frames** — filter out crash handler, signal handler, parking_lot, and stdlib frames. Focus on frames marked "(In app)".
4. **The data flow** — trace how the invalid data reached the crash site. What computed the bad index, the None value, etc.?
Find the relevant source files in the repository and read them. Pay close attention to:
- The crashing function and its callers
- How inputs to the crashing operation are computed
- Any assumptions the code makes about its inputs (string encoding, array lengths, option values)
### Step 3: Identify the Root Cause
Work backwards from the crash site to determine **what sequence of events or data conditions** produces the invalid state.
Ask yourself: *What user action or sequence of actions could lead to this state?* The crash came from a real user, so there is some natural usage pattern that triggers it.
### Step 4: Write a Reproduction Test
Write a minimal test case that:
1. **Mimics user actions** rather than constructing corrupt state directly. Work from the top down: what does the user do (open a file, type text, trigger a completion, etc.) that eventually causes the internal state to become invalid?
2. **Exercises the same code path** as the crash. The test should fail in the same function with the same kind of error (e.g., same panic message pattern).
3. **Is minimal** — include only what's necessary to trigger the crash. Remove anything that isn't load-bearing.
4. **Lives in the right place** — add the test to the existing test module of the crate where the bug lives. Follow the existing test patterns in that module.
5. **Avoid overly verbose comments** - the test should be self-explanatory and concise. More detailed descriptions of the test can go in ANALYSIS.md (see the next section).
When the test fails, its stack trace should share the key application frames from the original crash report. The outermost frames (crash handler, signal handling) will differ since we're in a test environment — that's expected.
If you can't reproduce the exact crash but can demonstrate the same class of bug (e.g., same function panicking with a similar invalid input), that is still valuable. Note the difference in your analysis.
### Step 5: Write the Analysis
Create an `ANALYSIS.md` file (in the working directory root, or wherever instructed) with these sections:
```markdown
# Crash Analysis: <short description>
## Crash Summary
- **Sentry Issue:** <ID and link if available>
- **Error:** <the panic/error message>
- **Crash Site:** <function name and file>
## Root Cause
<Explain what goes wrong and why. Be specific about the data flow.>
## Reproduction
<Describe what the test does and how it triggers the same crash.
Include the exact command to run the test, e.g.:
`cargo test -p <crate> <test_name>`>
## Suggested Fix
<Describe the fix approach. Be specific: which function, what check to add,
what computation to change. If there are multiple options, list them with tradeoffs.>
```
## Guidelines
- **Don't guess.** If you're unsure about a code path, read the source. Use `grep` to find relevant functions, types, and call sites.
- **Check the git history.** If the crash appeared in a specific version, `git log` on the relevant files may reveal a recent change that introduced the bug.
- **Look at existing tests.** The crate likely has tests that show how to set up the relevant subsystem. Follow those patterns rather than inventing new test infrastructure.
+357
View File
@@ -0,0 +1,357 @@
#!/usr/bin/env python3
"""Fetch a crash report from Sentry and output formatted markdown.
Usage:
script/sentry-fetch <issue-short-id-or-numeric-id>
script/sentry-fetch ZED-4VS
script/sentry-fetch 7243282041
Authentication (checked in order):
1. SENTRY_AUTH_TOKEN environment variable
2. Token from ~/.sentryclirc (written by `sentry-cli login`)
If neither is found, the script will print setup instructions and exit.
"""
import argparse
import configparser
import json
import os
import sys
import urllib.error
import urllib.request
SENTRY_BASE_URL = "https://sentry.io/api/0"
DEFAULT_SENTRY_ORG = "zed-dev"
def main():
parser = argparse.ArgumentParser(
description="Fetch a crash report from Sentry and output formatted markdown."
)
parser.add_argument(
"issue",
help="Sentry issue short ID (e.g. ZED-4VS) or numeric issue ID",
)
args = parser.parse_args()
token = find_auth_token()
if not token:
print(
"Error: No Sentry auth token found.",
file=sys.stderr,
)
print(
"\nSet up authentication using one of these methods:\n"
" 1. Run `sentry-cli login` (stores token in ~/.sentryclirc)\n"
" 2. Set the SENTRY_AUTH_TOKEN environment variable\n"
"\nGet a token at https://sentry.io/settings/auth-tokens/",
file=sys.stderr,
)
sys.exit(1)
try:
issue_id, short_id, issue = resolve_issue(args.issue, token)
event = fetch_latest_event(issue_id, token)
except FetchError as err:
print(f"Error: {err}", file=sys.stderr)
sys.exit(1)
markdown = format_crash_report(issue, event, short_id)
print(markdown)
class FetchError(Exception):
pass
def find_auth_token():
"""Find a Sentry auth token from environment or ~/.sentryclirc.
Checks in order:
1. SENTRY_AUTH_TOKEN environment variable
2. auth.token in ~/.sentryclirc (INI format, written by `sentry-cli login`)
"""
token = os.environ.get("SENTRY_AUTH_TOKEN")
if token:
return token
sentryclirc_path = os.path.expanduser("~/.sentryclirc")
if os.path.isfile(sentryclirc_path):
config = configparser.ConfigParser()
try:
config.read(sentryclirc_path)
token = config.get("auth", "token", fallback=None)
if token:
return token
except configparser.Error:
pass
return None
def api_get(path, token):
"""Make an authenticated GET request to the Sentry API."""
url = f"{SENTRY_BASE_URL}{path}"
req = urllib.request.Request(url)
req.add_header("Authorization", f"Bearer {token}")
req.add_header("Accept", "application/json")
try:
with urllib.request.urlopen(req) as response:
return json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as err:
body = err.read().decode("utf-8", errors="replace")
try:
detail = json.loads(body).get("detail", body)
except (json.JSONDecodeError, AttributeError):
detail = body
raise FetchError(f"Sentry API returned HTTP {err.code} for {path}: {detail}")
except urllib.error.URLError as err:
raise FetchError(f"Failed to connect to Sentry API: {err.reason}")
def resolve_issue(identifier, token):
"""Resolve a Sentry issue by short ID or numeric ID.
Returns (issue_id, short_id, issue_data).
"""
if identifier.isdigit():
issue = api_get(f"/issues/{identifier}/", token)
return identifier, issue.get("shortId", identifier), issue
result = api_get(f"/organizations/{DEFAULT_SENTRY_ORG}/shortids/{identifier}/", token)
group_id = str(result["groupId"])
issue = api_get(f"/issues/{group_id}/", token)
return group_id, identifier, issue
def fetch_latest_event(issue_id, token):
"""Fetch the latest event for an issue."""
return api_get(f"/issues/{issue_id}/events/latest/", token)
def format_crash_report(issue, event, short_id):
"""Format a Sentry issue and event as a markdown crash report."""
lines = []
title = issue.get("title", "Unknown Crash")
lines.append(f"# {title}")
lines.append("")
issue_id = issue.get("id", "unknown")
project = issue.get("project", {})
project_slug = (
project.get("slug", "unknown") if isinstance(project, dict) else str(project)
)
first_seen = issue.get("firstSeen", "unknown")
last_seen = issue.get("lastSeen", "unknown")
count = issue.get("count", "unknown")
sentry_url = f"https://sentry.io/organizations/{DEFAULT_SENTRY_ORG}/issues/{issue_id}/"
lines.append(f"**Short ID:** {short_id}")
lines.append(f"**Issue ID:** {issue_id}")
lines.append(f"**Project:** {project_slug}")
lines.append(f"**Sentry URL:** {sentry_url}")
lines.append(f"**First Seen:** {first_seen}")
lines.append(f"**Last Seen:** {last_seen}")
lines.append(f"**Event Count:** {count}")
lines.append("")
format_tags(lines, event)
format_entries(lines, event)
return "\n".join(lines)
def format_tags(lines, event):
"""Extract and format tags from the event."""
tags = event.get("tags", [])
if not tags:
return
lines.append("## Tags")
lines.append("")
for tag in tags:
key = tag.get("key", "") if isinstance(tag, dict) else ""
value = tag.get("value", "") if isinstance(tag, dict) else ""
if key:
lines.append(f"- **{key}:** {value}")
lines.append("")
def format_entries(lines, event):
"""Format exception and thread entries from the event."""
entries = event.get("entries", [])
for entry in entries:
entry_type = entry.get("type", "")
if entry_type == "exception":
format_exceptions(lines, entry)
elif entry_type == "threads":
format_threads(lines, entry)
def format_exceptions(lines, entry):
"""Format exception entries."""
exceptions = entry.get("data", {}).get("values", [])
if not exceptions:
return
lines.append("## Exceptions")
lines.append("")
for i, exc in enumerate(exceptions):
exc_type = exc.get("type", "Unknown")
exc_value = exc.get("value", "")
mechanism = exc.get("mechanism", {})
lines.append(f"### Exception {i + 1}")
lines.append(f"**Type:** {exc_type}")
if exc_value:
lines.append(f"**Value:** {exc_value}")
if mechanism:
mech_type = mechanism.get("type", "unknown")
handled = mechanism.get("handled")
if handled is not None:
lines.append(f"**Mechanism:** {mech_type} (handled: {handled})")
else:
lines.append(f"**Mechanism:** {mech_type}")
lines.append("")
stacktrace = exc.get("stacktrace")
if stacktrace:
frames = stacktrace.get("frames", [])
lines.append("#### Stacktrace")
lines.append("")
lines.append("```")
lines.append(format_frames(frames))
lines.append("```")
lines.append("")
def format_threads(lines, entry):
"""Format thread entries, focusing on crashed and current threads."""
threads = entry.get("data", {}).get("values", [])
if not threads:
return
crashed_threads = [t for t in threads if t.get("crashed", False)]
current_threads = [
t for t in threads if t.get("current", False) and not t.get("crashed", False)
]
other_threads = [
t
for t in threads
if not t.get("crashed", False) and not t.get("current", False)
]
lines.append("## Threads")
lines.append("")
for thread in crashed_threads + current_threads:
format_single_thread(lines, thread, show_frames=True)
if other_threads:
lines.append(f"*({len(other_threads)} other threads omitted)*")
lines.append("")
def format_single_thread(lines, thread, show_frames=False):
"""Format a single thread entry."""
thread_id = thread.get("id", "?")
thread_name = thread.get("name", "unnamed")
crashed = thread.get("crashed", False)
current = thread.get("current", False)
markers = []
if crashed:
markers.append("CRASHED")
if current:
markers.append("current")
marker_str = f" ({', '.join(markers)})" if markers else ""
lines.append(f"### Thread {thread_id}: {thread_name}{marker_str}")
lines.append("")
if not show_frames:
return
stacktrace = thread.get("stacktrace")
if not stacktrace:
return
frames = stacktrace.get("frames", [])
if frames:
lines.append("```")
lines.append(format_frames(frames))
lines.append("```")
lines.append("")
def format_frames(frames):
"""Format stack trace frames for display.
Sentry provides frames from outermost caller to innermost callee,
so we reverse them to show the most recent (crashing) call first,
matching the convention used in most crash report displays.
"""
output_lines = []
for frame in reversed(frames):
func = frame.get("function") or frame.get("symbol") or "unknown"
filename = (
frame.get("filename")
or frame.get("absPath")
or frame.get("abs_path")
or "unknown file"
)
line_no = frame.get("lineNo") or frame.get("lineno")
in_app = frame.get("inApp", frame.get("in_app", False))
app_marker = "(In app)" if in_app else "(Not in app)"
line_info = f"Line {line_no}" if line_no else "Line null"
output_lines.append(f" {func} in {filename} [{line_info}] {app_marker}")
context_lines = build_context_lines(frame, line_no)
output_lines.extend(context_lines)
return "\n".join(output_lines)
def build_context_lines(frame, suspect_line_no):
"""Build context code lines for a single frame.
Handles both Sentry response formats:
- preContext/contextLine/postContext (separate fields)
- context as an array of [line_no, code] tuples
"""
output = []
pre_context = frame.get("preContext") or frame.get("pre_context") or []
context_line = frame.get("contextLine") or frame.get("context_line")
post_context = frame.get("postContext") or frame.get("post_context") or []
if context_line is not None or pre_context or post_context:
for code_line in pre_context:
output.append(f" {code_line}")
if context_line is not None:
output.append(f" {context_line} <-- SUSPECT LINE")
for code_line in post_context:
output.append(f" {code_line}")
return output
context = frame.get("context") or []
for ctx_entry in context:
if isinstance(ctx_entry, list) and len(ctx_entry) >= 2:
ctx_line_no = ctx_entry[0]
ctx_code = ctx_entry[1]
suspect = " <-- SUSPECT LINE" if ctx_line_no == suspect_line_no else ""
output.append(f" {ctx_code}{suspect}")
return output
if __name__ == "__main__":
main()