top of page

Bug Report Template: What Makes One Useful

  • Shawn West
  • Mar 18
  • 9 min read

Updated: Aug 10

A tester filed a clean-looking report against the checkout flow: title, steps, expected result, actual result, a screenshot of the 500 error. It read like a good report. The engineer who picked it up followed the steps exactly, added the same item to the cart, entered an address, clicked pay — and got a receipt. No error. They tried twice more, shrugged, and closed the ticket "Cannot reproduce."

A week later the same 500 hit real customers during a checkout spike. The postmortem found the missing detail in about four minutes: the failure only fired when the billing address contained an apostrophe — O'Brien, D'Angelo. The tester's own test data happened to include one. The engineer's didn't. Every field in that report was filled in. The one input that actually triggered the bug was never written down, because to the tester it wasn't special — it was just the name they always used.

That is the whole game. A bug report is not a description of a problem. It is a set of instructions for recreating a specific failure on demand. The single test of whether it works: hand it to someone who has never seen the bug, and see if they can make it happen. If they can't, the report didn't fail to be thorough — it failed to be reproducible, and an irreproducible bug is one nobody can fix and everybody eventually re-discovers in production.

This piece is about the artifact — the template — and the one property that makes it earn its place: reproducibility. Getting a reproducible report actually prioritized and worked is a different skill, covered in the sibling piece Writing Bug Reports That Actually Get Fixed. Here we stay on the mechanics of the repro itself.

The test that defines a useful report

Most advice about bug reports optimizes for completeness: fill in every field, attach logs, be detailed. Completeness is easy to fake. The report above was complete and still useless.

Replace "is it complete?" with a harder, more honest question — the acceptance bar for every report you file:

Could a person who has never seen this bug, working only from what I wrote, reproduce the failure on their first try?

This is a discovery move, not a formatting rule. Before you submit, mentally hand the report to a stranger — a new hire, a developer on another team, yourself in three weeks with no memory of the session — and walk their path. The moment you catch yourself thinking "well, they'd also need to know that I was logged in as a trial account" or "obviously you'd use a real credit card," you have found a field that is in your head and not on the page. That silent assumption is exactly where "cannot reproduce" comes from.

Run the test now: take your last bug report and ask whether a stranger could follow it cold. If you had to add a caveat out loud to make it work, that caveat is a missing field.

What breaks reproduction, field by field

An engineer trying to reproduce a bug is running a small experiment. For the experiment to produce the same result twice, three things must match: the conditions (environment and state), the procedure (the exact steps and inputs), and the observation (what counts as the failure). A report fails when it leaves one of those three ambiguous.

The table below maps each template field to the thing it locks down and the specific reproduction that collapses without it. This is the reasoning behind the template — not a checklist to fill for its own sake.

Field

What it unlocks

The repro that fails without it

Build / version

Which code the bug lives in

Engineer tests on today's build; the bug was fixed yesterday — or exists only in the release branch. "Works for me" is really "works on a different build."

Environment (OS / browser / device)

The conditions the failure needs

A Safari-only date-picker bug is invisible to an engineer on Chrome. Reproduction never even starts.

Account / user state

The data and permissions in play

Bug fires only for accounts with an expired card on file or a specific role. A fresh test account can't trigger it.

Preconditions

The starting state before step 1

"Cart already has 3 items" or "feature flag X on." Start from a clean slate and the sequence does nothing.

Exact steps, numbered

The procedure, executable cold

"Go to settings and update your profile" hides which of six fields, in what order. The stranger guesses wrong and sees nothing.

Exact input values

The specific data that triggers it

The apostrophe address; the user+tag@ email; the amount 1000000. Generic "enter an address" uses safe data and never fires the bug.

Expected result

What should have happened

Without it the engineer can't tell a bug from intended behavior — is the 500 wrong, or is the input supposed to be rejected?

Actual result

The observable failure signal

"It's broken" doesn't say what to look for. A 500? A wrong total? A silent no-op? Different failures, different searches.

Frequency

Whether one attempt is enough

An intermittent bug marked as nothing looks "not reproduced" after one clean run, and gets closed.

Notice the pattern: the fields that most often get dropped are the ones the reporter treats as obvious. Build number, because "it's the current one." Input values, because "any address works" — until one doesn't. The reporter's familiarity is precisely what makes the report unreproducible for someone without it.

Takeaway: for your next report, look specifically at build, account state, and the literal input you typed. Those three are the usual suspects behind "cannot reproduce" — confirm each is written down, not assumed.

The template

Here is the working artifact. Copy it, keep it in a snippet or issue template, fill every field. Empty fields are decisions, not omissions — "Frequency: unknown" tells the engineer something; a blank tells them nothing.

**Title:** [symptom] + [specific condition]

**Environment**
- Build / version:
- OS / browser / device:
- Account / user (role, relevant state):
- Environment (prod / staging / local):

**Preconditions**
- [State that must be true before step 1: flags, existing data, session]

**Steps to Reproduce**
1.
2.
3.

**Test data / exact inputs**
- [The literal values used — the string, the email, the amount]

**Expected result:**
[What should have happened]

**Actual result:**
[What happened instead — the observable failure]

**Frequency:** Always / Sometimes (___ of ___) / Once

**Additional info**
- Screenshots / video:
- Logs / request ID / error message:
- When it started (build, date):
- Workaround (if any):
- Related tickets:

The section that carries the most weight and gets the least care is Steps to Reproduce paired with exact inputs. Everything else describes context; those two are the experiment. Write them as if the reader is a script that will do exactly what you say and nothing you imply.

Takeaway: put this template where filing a bug happens — a repository issue template, a saved snippet, a pinned note. A template you have to go find is a template you skip at 5pm.

Rebuilding a vague report into a reproducible one

(Developed example — composite scenario.)

Start with a report a real tester might file after hitting a genuine bug. It is not lazy — it is written by someone who saw the failure clearly and assumed the reader shares their context.

The vague version:

Title: Signup is broken Steps: Try to sign up with an email. It errors. Expected: Account created. Actual: Error.

An engineer picks this up, goes to the signup page, types test@example.com, sets a password, submits — account created, confirmation email sent. Works perfectly. They mark it "Cannot reproduce" and move on. The reporter, who can trigger it every single time, is baffled. Both are right. They are running different experiments.

Now rebuild it, one field at a time, watching for the field that actually unlocks the failure.

Add environment and build. The reporter is on the current staging build, 2.14.0-rc3, in Chrome. The engineer was on the same build, so this doesn't crack it yet — but it rules out "already fixed," which is where the engineer would otherwise waste an hour.

Add account state. The reporter was signing up a new account, not logged in. Same as the engineer. Still no repro. Good — we've now eliminated two common culprits, which narrows the search.

Add the exact input. Here it is. The reporter wasn't using test@example.com. They were using their real work address with a plus tag — dana+staging@company.com — the way testers route disposable signups to one inbox. The + in the local part hits a validation regex that accepts the email at the form layer but rejects it at the account-creation service, which throws a 500 instead of a clean validation error.

That one field — the literal input value — is the entire bug. Every other field was context that helped the engineer stop guessing. This field made the failure happen.

Add expected vs. actual, precisely. Not "error" but: Expected — either the account is created, or the form shows a validation message. Actual — the page shows a generic "Something went wrong," and the server logs a 500 from POST /accounts with ValidationError: local part contains '+'. Now the engineer knows it's a real defect (a 500 is never the intended response to bad input) and where to look. Precise expected-vs-actual is the same discipline as a Then clause in acceptance criteria: it states the observable outcome, not a vibe.

The reproducible version:

Title: Signup returns 500 when email local part contains a + (e.g. dana+staging@company.com) Environment Preconditions: None — clean signup flow. Steps to Reproduce Expected: Account created, or a field-level validation error if + is unsupported. Actual: Page shows "Something went wrong." Server returns 500 from POST /accounts; log: ValidationError: local part contains '+'. Frequency: Always (5 of 5). Additional info: Plain addresses (dana@company.com) succeed. Started in 2.14.0-rc1 when signup validation moved to the new service.
  • Build: 2.14.0-rc3 (staging)

  • Browser: Chrome 141, macOS

  • Account: none (new signup, logged out)

  1. Go to /signup.

  2. Email: dana+staging@company.com

  3. Password: any valid password (Test1234!)

  4. Click Create account.

Hand this to a stranger and they reproduce it on the first try, because the one input that matters is on the page instead of in the reporter's habits. The rebuild didn't make the report longer for its own sake — it added exactly the fields that a stranger's cold run would have stumbled on.

Takeaway: when a report of yours gets "cannot reproduce," don't re-explain it — diff it against this one. The missing detail is almost always a specific input value or a piece of account state you never thought to write down.

Minimal, reliable, and honest about frequency

Two more properties separate a report an engineer can act on from one they fight.

Minimal. After you can reproduce a bug, strip the path to the fewest steps that still trigger it. If the 500 fires whether or not you first browse three product pages, cut the browsing. Extra steps aren't safety margin — each one is a place the reader can diverge from you, and a place the real trigger might be hiding. A five-step repro that always works beats a fifteen-step one that sometimes does. This is the same instinct as isolating a finding during exploratory testing: once you've found the failure, narrow it until only the essential cause remains.

Honest about frequency. If a bug fires four times out of ten, say Sometimes (4 of 10) — never round it up to "always" to sound convincing, and never hide it. An intermittent bug reported as reproducible gets closed the moment the engineer's first attempt happens to pass. Reported as intermittent, it tells them to run the steps several times and to suspect timing, ordering, or shared state. The frequency field is you handing over your actual observation, not your confidence.

Takeaway: before filing, run your own steps one more time from a clean state, deleting any that don't change the outcome, and record how many of your attempts actually failed. If you can't reproduce your own bug, write that plainly — an honest "seen twice, can't reliably trigger" is worth more than a confident repro that doesn't hold.

From reproducible report to permanent guardrail

A reproducible report has a second life the vague one never earns: it converts directly into a regression test. The steps become the test procedure, the exact input becomes the test data, and expected-vs-actual becomes the assertion. The apostrophe address and the plus-tag email don't just fix one ticket — captured as a maintainable test case, they become permanent checks that the same failure can't return unnoticed. This is why reproduction discipline pays twice: once when the bug gets fixed, and again every time the test stops it from coming back.

Takeaway: for any bug worth fixing, copy its exact input and steps into a regression test before you close the ticket. The reproducible report already did the hard part — you're just moving the repro from prose into code.

What to do next

Pick your three most recent bug reports and run each through the stranger test: could someone with none of your context reproduce it cold? For every "no," find the field that was in your head — usually the build, the account state, or the literal input — and add it. Then adopt the template above as an issue template so the fields are prompts, not afterthoughts.

Reproducibility is the property that makes a report an artifact instead of a note. Once your reports clear that bar, the next question is getting them prioritized against everything else in the backlog — which is exactly what the sibling piece, Writing Bug Reports That Actually Get Fixed, takes up.

Sources

  • The signup and checkout failures are a composite scenario built from common, real reproduction failures (plus-tagged emails and apostrophes in address fields hitting layered validation). It is illustrative, not a documented incident, and contains no invented statistics.

Keep learning. This article is part of the Software Testing Foundations path in the ShiftQuality Learning Center.

bottom of page