How to document a bug report developers will actually fix

What separates a bug report that gets fixed in an hour from one that sits in the backlog for a month. The fields that matter, the ones that don't, and how to capture them without typing every step by hand.

Most bug reports don't get fixed because they can't be reproduced. They sit in a queue, get triaged as “cannot reproduce” or “needs more info,” and quietly die. The reporter blames the developer. The developer blames the ticket. Both are right and both are wrong.

The real culprit is almost always missing context. The reporter saw something specific — a button in a specific state on a specific page after a specific sequence of clicks — and described it generically: “The save button doesn't work.” The developer reads that, opens the app, clicks save, watches it work, and closes the ticket.

This post is a practical guide to writing bug reports that actually get fixed. The non-negotiable fields, the ones that get cut, what a good repro looks like, and where the act of writing the report can stop being the slowest part of finding the bug.

The five fields a developer needs

Every guide on bug reporting eventually lists about fifteen fields. In practice, five matter. The rest are nice-to-have or auto-fillable.

1. A specific title

Not “Login broken.” Not “Bug on dashboard.” A specific title is the one a developer can read in their queue and form a mental model before they open the ticket.

Bad: “Save button doesn't work”

Good: “Save fails silently on /invoices/edit when total is > $9,999.99 (no error toast)”

The rule of thumb: include the page, the trigger, and the observable failure. If a developer scanning their queue can't tell what file they'll be opening, the title isn't specific enough yet.

2. Steps to reproduce, numbered

Numbered list, every step a single action, no skipped steps. The test of a good repro is whether someone who has never used your app can follow it without asking questions.

The trap most reporters fall into is implicit context. “Go to the invoice page” assumes the developer knows where the invoice page is. “Open the side menu and click Invoices” doesn't. The literal URL — copy-pasted from your address bar — beats both.

Bad:

  1. Go to invoices
  2. Open one
  3. Try to save

Good:

  1. Open https://app.example.com/invoices while logged in as a billing admin.
  2. Click the row for any invoice with status = “Draft.”
  3. In the Total field, change the value to 10000.
  4. Click the “Save” button at the bottom right.

3. Expected vs actual

Two short sentences. What you thought would happen, and what happened instead.

Expected: The invoice saves and a green “Saved” toast appears.

Actual: The button greys out for half a second, then returns to normal. No toast, no error. The total reverts to its previous value when you reload the page.

Without expected vs actual, the developer has to guess at the correct behaviour. Sometimes their guess matches yours; often it doesn't, and you end up “fixing” a bug that the reporter still considers broken.

4. Environment

Browser and version. OS. Screen size if it's a layout bug. Account type or feature flags if your app has them. Time of day and timezone if the bug is timing-related.

The shortest useful environment block:

  • Browser: Chrome 142 on macOS 16
  • Account: billing-admin@acme.com
  • URL at failure: https://app.example.com/invoices/edit/4912
  • Timestamp: 2026-05-06 14:22 PT

The timestamp is the underrated one. If your stack has any kind of logging, “the bug happened at 14:22 PT” lets a developer pull the request log for that user at that minute and see the failure mode in twenty seconds.

5. Visual evidence

A screenshot at minimum. Better: a sequence of screenshots, one per step, with the URL bar visible. Better still: a short recording, but not a 4-minute Loom for a 3-step bug.

Visual evidence does two things at once. It proves the bug exists, and it short-circuits the “cannot reproduce” triage. A developer who can see the broken state on their screen is much more willing to spend ten minutes hunting for it than one who can't.

Fields you can usually cut

Templates pile these on. Most are noise.

  • Severity and priority chosen by the reporter. The reporter is biased; the engineering team will re-triage anyway. Skip unless your process really needs it.
  • Workarounds. Useful if you have one. Optional if you don't. Don't invent one to fill the field.
  • Long “background” sections. The ticket is not a place for a story. Steps + expected + actual is the story.
  • Attachments of the entire console log. Trim to the failing request and the surrounding ten lines. A 4MB paste of network noise gets ignored.

The reproduction problem nobody talks about

Even with all five fields, bug reports decay between writing and reading. Three common ways:

The repro depends on hidden state. The bug only happens when the user has six invoices open in adjacent tabs, or when they've just clicked a notification, or when their feature flag is on. The reporter doesn't mention it because they don't know it's relevant.

The reporter rewrites the steps from memory. They saw the bug, closed the tab, opened the bug tracker, and now type out what they think they did. Memory is a compression algorithm. Detail gets lossy.

The screenshot is from a different moment than the steps. The reporter took the screenshot at the failing state, then wrote the steps, but the steps describe the successful path that should have led there, with one variable changed mentally.

The shared cause: the report is reconstructed after the fact. The fix is to capture as you go, not after.

Capturing the report while the bug is happening

This is where a tool like UIHike earns its place in the QA stack. The desktop app and Chrome extension record while you use the product. Each step it captures is, by default, the kind of thing a bug report wants:

  • The screenshot — every click and navigation emits one, automatically.
  • The URL — the active page at capture time, stored as a separate field.
  • The clicked element — CSS selector, tag, visible text, associated label, and the value typed (passwords masked). That's the data you need to write“clicked the ‘Save’ button next to the Total field” instead of “clicked the button.”
  • The timestamp — UTC, on every step.
  • The page metadata — title, headings, Open Graph data; useful when the bug only repros on certain page types.

The repro doesn't get reconstructed. It's the recording. You hand the developer a link (go.uihike.com/published/{UUID}) or an exported Markdown file, and they get the canonical sequence with screenshots and URLs and clicked elements per step.

Redaction matters here too. Real bug reports often contain customer data — an email in a contact card, an account number in the URL. Drag a redaction box over the field, the original PNG is preserved, and the exported version masks the sensitive bits. You don't have to re-record because you forgot to blur something.

A template that actually gets used

The shortest bug report template I've seen survive contact with a real engineering team:

Title: [page] [trigger] [observable failure]

Steps:

  1. ...
  2. ...
  3. ...

Expected: [one sentence]
Actual: [one sentence]

Environment: [browser/OS], [account], [URL], [timestamp]

Evidence: [screenshots or walkthrough link]

That's it. Don't add a section for “additional context” or “business impact” until you have evidence the engineering team reads them. They almost never do.

What a good bug report looks like in the wild

A real example, lightly anonymised:

Title: /invoices/edit save fails silently when total > $9,999.99 (no toast, no error, value reverts on reload)

Steps:

  1. Log in as a billing admin (test account billing-admin@acme.com).
  2. Open app.example.com/invoices.
  3. Click any draft invoice. URL becomes app.example.com/invoices/edit/[id].
  4. In the Total field, type 10000 and tab out.
  5. Click the Save button (bottom right).

Expected: A green “Saved” toast appears, the page stays on the edit view, and the new total persists on reload.

Actual: Button greys out for ~500ms, no toast. Reloading the page reverts the total to its previous value. The Network tab shows a 400 response from PUT /api/invoices/[id] with body {"error": "total_exceeds_limit"}", but nothing surfaces in the UI.

Environment: Chrome 142 on macOS 16.0, billing-admin role, timestamp 2026-05-06 14:22:11 PT.

Evidence: go.uihike.com/published/[UUID] — five-step walkthrough with screenshots, network tab capture on step 5.

That report gets fixed. The developer has the URL, the steps, the response code, the failure mode, and the visual evidence tied together. The total time from bug-found to fix-deployed, for a report like this, is usually under a day.

The next bug you find

Try this once: when you see the next real bug, instead of switching to the bug tracker and writing it from memory, hit record first and reproduce it. Then publish the recording and paste the link into the ticket. The five fields above either fill themselves (URL, steps, environment, evidence) or shrink to one short sentence each (expected, actual).

Try UIHike on your next bug. The difference between a report that takes ten minutes to write and one that takes one is also, usually, the difference between a fix this week and a closed-as-stale six months from now.

— The UIHike team