How to Write a Bug Report That Actually Gets Fixed
Two bugs land in the same sprint. One ships by Wednesday. The other sits in triage for a week — the developer asks for reproduction steps, gets a screenshot, asks for logs, gets nothing, and closes the ticket “cannot reproduce” on Friday.
The difference isn’t the bug. It’s the report.
This guide covers six fields that separate reports that get fixed from reports that get abandoned. It comes from patterns I’ve seen filing and triaging thousands of bugs. If you skip anything, at least read Step 4 — that’s where most guides fall silent, and where the fastest fixes come from.
What you need before writing
Have the following before you open the ticket:
- Access to the system where the bug happened. You’ll need to check timestamps and pull logs.
- A way to reproduce it, or clear evidence of a single failure. Intermittent bugs need more evidence than one-shot bugs — be explicit about which one you have.
- Access to your team’s log system and dashboards. Logs, error tracking (Sentry, Rollbar), and metrics dashboards (Grafana, Datadog).
- Knowledge of the expected behavior. From the product spec, the docs, or your own understanding of the feature.
If you’re missing any of these, spend five minutes filling the gap before writing. A report built on incomplete information wastes more time than it saves.
Step 1: Write a title that carries the fail
The title sits in someone’s ticket queue next to fifty others. Yours has three seconds to earn a click.
Two rules:
- Pass/fail signal at the top. Whoever’s triaging should know from the title alone: is this system-broken, feature-broken, or cosmetic?
- Component + symptom. State the component (checkout, auth, dashboard) and what went wrong (500 error, wrong value shown, button unresponsive).
Bad titles that waste triage time:
- “Bug in checkout” → what kind of bug? What’s broken?
- “This is broken” → what is “this”?
- “Payment doesn’t work sometimes” → “sometimes” doesn’t survive a queue
Better:
- “Checkout:
POST /ordersreturns 500 when applying valid coupon” - “Dashboard: revenue widget shows $0 for orgs with UTF-8 names”
- “Login: SSO redirect drops session on Safari 17.4+”
The reader should know whether to prioritize this ticket before opening it.
Step 2: Reproduction with real values
Reproduction steps are the second most-skipped field. They’re also why bugs land in the “cannot reproduce” pile and reopen weeks later when someone else works out the path.
Format:
- Start from a known state (“logged in as user X in org Y with role Z”)
- Numbered actions with concrete values — never “sample data” or
[email protected] - The exact action that triggers the failure (“clicked Apply at 15:32:04 UTC”)
Bad:
1. Log in
2. Go to checkout
3. Apply a coupon
4. Get error
Good:
1. Log in as [email protected] (org: qa-sandbox, role: standard)
2. Navigate to /checkout with a $47.50 cart containing SKUs WGT-100 and WGT-200
3. Enter coupon code SAVE20 in the promo field
4. Click Apply — POST /api/v2/orders/apply-coupon returns 500 at
2026-07-02T15:32:04Z
Include timing where relevant. “Sometimes fails” is not a report; “fails on 3 out of 5 attempts within a 30-second window” is.
Step 3: Expected vs actual, in matching detail
State both outcomes at the same level of specificity. A common reason for “cannot reproduce” closures is a vague “expected” — the developer’s guess doesn’t match what the reporter meant.
Bad:
Expected: It should work.
Actual: It's broken.
Good:
Expected: POST /api/v2/orders/apply-coupon returns 200 with body
{ "discount": 9.50, "total": 38.00 }. The checkout summary updates to
show "SAVE20 applied" and the total drops to $38.00.
Actual: The endpoint returns 500 with body
{ "error": "internal_server_error", "trace_id": "abc123" }. The UI
shows "Something went wrong. Try again." The cart total remains $47.50.
If a screenshot helps, attach two: one of the expected state (from staging, docs, or a passing test) and one of the actual state. Screenshots on their own — with no accompanying text — are worse than no screenshot at all.
Step 4: Observability details (the field most guides skip)
Good bug reports separate from great ones here. And this is where public guides go silent.
Include:
- Timestamp with timezone. ISO 8601 preferred:
2026-07-02T15:32:04Z. Without this, correlating with logs is guesswork. - Error code and trace ID. From the response body, from the UI, or from your error-tracking tool.
trace_id: abc123lets the developer pull the exact request in seconds. - Log excerpts, linked. Copy the 5–10 relevant lines into the report AND link to the log system’s view of the incident (Datadog, Splunk, CloudWatch — whatever you use). Don’t screenshot log text; it’s not searchable.
- Dashboard callouts. If a Grafana panel or metric dashboard shows the failure spike, link it with the time range:
grafana.company.com/d/abc123?from=1751470324000&to=1751470924000. - Video, if available. For UI bugs, a 30-second Loom or QuickTime capture is worth more than any amount of prose. Especially for bugs involving state, timing, or user interaction that doesn’t reproduce on the first try.
If your team doesn’t have observability instrumented, that’s a bigger problem than this bug report. Log it separately.
Step 5: Environment and versions
Enough to reproduce, not more. Include:
- Browser + version (for web bugs):
Chrome 128.0.6613.84orSafari 17.4.1 - OS + version (for native or Electron apps):
macOS 14.5,Windows 11 23H2 - Backend service versions: application version + any relevant service versions from
/healthor your deploy dashboard - Environment:
production,staging,local-dev— be explicit - Device (for mobile-specific bugs): model + OS
Skip:
- Full useragent string dumps unless they matter
- Every service version in your stack — only the ones touching this bug’s path
- Screenshots of the “About” page — the version strings alone are searchable
Step 6: Severity vs priority — and why they’re different
Severity measures technical impact. Priority measures business impact. They don’t always move together.
- A severity: critical bug — say, a database write failing under load — can be priority: low if it only affects a feature nobody uses.
- A severity: minor bug — say, the checkout button being slightly misaligned — can be priority: highest if it drops conversion measurably on your main product.
Set severity based on what the system is doing wrong. Data loss, security exposure, and outages are always severity: critical. Wrong values displayed are severity: high. Cosmetic issues are severity: low.
Set priority based on customer impact and blast radius. Two questions decide it:
- How many customers are affected, and who are they? A bug affecting your three biggest accounts outweighs a bug affecting all users of a small feature.
- Is there a workaround, and how painful is it? If a customer can dodge the bug by refreshing the page, priority is lower. If they have to call support to complete a purchase, priority is highest.
File severity and priority separately, both explicit. Don’t collapse them into a single “urgency” field — it makes triage worse, not better.
Common pitfalls
These are the patterns that turn a well-intentioned report into a triage bottleneck.
Screenshot without detail. A screenshot proves the bug happened but tells the developer nothing about how or why. It’s a starting point, not a report. Every screenshot needs the steps that produced it and the observability context around it.
Vague reproduction. “Sometimes fails” or “works on my machine” turns triage into guesswork. If the bug is intermittent, quantify: how often, under what load, at what time of day.
Missing steps. Reports that jump from “I opened the page” to “everything crashed” skip the actions in between. The developer needs the full sequence — even the boring parts.
Suggested fixes without root-cause understanding. This one hurts more than it helps. When a reporter writes “the bug is in the coupon-validation service — the fix is to add a null check,” the developer anchors to that theory and can waste hours proving it wrong.
A suggested fix is only useful if the reporter has three pieces of context: observable data (they’ve read the logs), source-code understanding (they’ve traced the path), and product intent (they know what the feature is supposed to do). Without all three, the suggestion becomes a red herring. If you have less than that, describe the symptoms and let the developer diagnose.
No traceability. “This is broken” with no way for the developer to correlate the failure to logs, metrics, or a specific request wastes hours. Trace IDs and timestamps aren’t optional.
The six-field checklist
Before you file, confirm all six fields are populated:
- Title — pass/fail signal + component + symptom
- Reproduction — numbered steps with real values and timing
- Expected vs actual — both stated at matching specificity
- Observability — timestamp, error code / trace ID, log link, dashboard link, video if applicable
- Environment — browser/OS/service versions and env
- Severity + priority — set independently, with brief reasoning
If any field is blank, either fill it or explicitly note why it’s blank (“no logs available — team lacks Sentry integration”). A blank field with no reason is a red flag to whoever triages.
Related reading
- Defect Density: Formula, Examples, and Benchmarks — how bug volume relates to overall code quality, and where bug reports feed the metric
- More QA metrics and tools coming to the tools index as they ship