Quality assurance
How to write a bug report a developer can act on
A defect report is a handover of an unfinished investigation. Most of them fail in the same three places, and none of the repairs involve a longer template.
There is one test of a defect report, and it is not whether every field is filled. It is whether a person who has never seen the problem can put the product into the failing state without asking the reporter a single question. A great many reports fail that test while looking immaculate in the tracker.
When reports keep bouncing back, the usual institutional response is to lengthen the template. That almost never helps, because the missing information was never a field: it was the state the tester was in before they started clicking. What follows is the standard we hold ourselves to on manual test passes, written as advice to whoever is filing.
Three people read it, and they want different things
The triager wants to know how bad this is and whether it is already known, and will decide in under a minute. The engineer wants to arrive at the broken state as cheaply as possible and then stay there while they poke at it. The person verifying the fix in a fortnight wants to repeat exactly what you did, on a build that no longer resembles yours.
Those three needs are served by different parts of the report, which is why a wall of undifferentiated prose fails all of them at once. It is also why the report is not a place to narrate. Nobody needs to know that you were checking something else at the time.
A defect report is finished when a stranger can produce the failure on demand, not when the description is complete.
The state before step one
This is where most reports break, and the symptom is a developer replying that it works for them. Steps that begin at the checkout button omit the fact that the account was created three years ago, sits on a plan that is no longer sold, and has an address record with no postcode in it. All three of those turn out to matter, and none appeared in the ticket.
Write down what was true before you began:
- Which account, in which role, with which permissions. "As an admin" is not the same as "as the account owner", and products routinely treat them differently.
- The relevant data. Not the whole database, just the rows the flow touches and anything unusual about them.
- The build. A commit hash or a version string, because a report that only says "on staging" becomes unverifiable the moment staging moves on.
- Browser, operating system, device and viewport where any of them could plausibly matter — and say when you have only tried one.
- Feature flags and configuration. A flag set differently between two environments is the single most common reason a defect refuses to travel.
Steps that reproduce, and the work of proving they do
Writing the steps is not the same as verifying them. The sequence is short and almost nobody does it:
- 01Write the steps out as instructions to somebody else, in the imperative, one action per line.
- 02Reset to the starting state and follow your own steps literally, doing nothing you did not write down. Roughly a third of the time this alone exposes a missing precondition.
- 03Remove steps one at a time until the failure stops happening, then restore the last one you took away. What remains is the minimal reproduction, and it is worth considerably more than a faithful transcript of everything you did.
- 04Record how often it happens. Nine attempts out of ten, or two out of twenty, is data. Silence on the point invites the assumption that it is deterministic.
Intermittent defects are still worth filing. The mistake is filing them as though they were reliable, which produces a closure of "cannot reproduce" and a bug that returns in the next release with nothing learned. Say plainly that it is intermittent, say what you were doing when it happened, and attach whatever the system logged at that moment.
Expected, actual, and where the expectation came from
Everyone writes expected and actual. Very few people write the third line, which is the source of the expectation: an acceptance criterion, a design file, the behaviour of the previous release, a documented API contract, or the reporter’s own judgement.
That line is what stops the ticket becoming an argument. A surprising share of first-cycle findings are not faults at all but disagreements about intended behaviour that nobody knew existed, and naming the source resolves them in one exchange rather than four. If the source really is your judgement, write that. An honest "this seems wrong to me and no requirement covers it" is a useful ticket. A guess dressed as a specification is not.
Severity is not priority, and neither is a mood
Severity describes the damage if the defect occurs: data loss, a blocked purchase, a cosmetic misalignment. Priority describes when it gets fixed relative to everything else competing for the same week. They are set by different people for different reasons, and a tracker with one field for both quietly turns every triage meeting into a negotiation.
Two rules make this workable. The reporter sets severity and never revises it to reflect a scheduling decision, because a severity that moves to match the plan stops carrying information. And each level on the scale needs a written anchor with an example attached, otherwise everyone applies a private definition and the numbers cannot be aggregated across a release.
Evidence, including what not to attach
- A screenshot taken at the moment of failure, with the URL visible, beats a six-minute screen recording that the engineer has to scrub through.
- Console output and the failing network request. A request identifier and a timestamp with its time zone let somebody find the corresponding server log, which is where the actual cause usually lives.
- Redact before you attach. Screenshots and network captures carry real names, tokens and addresses into a tracker that far more people can read than can reach the database, which is one of the quieter ways test work leaks personal data.
- Skip the annotated arrows and circles on a screenshot of a single field. If the location needs explaining, the steps were not specific enough.
Four reports not worth filing
- Five defects in one ticket. They will be fixed at different times by different people, and the ticket cannot be closed until the least important of them is done.
- A performance complaint with no number in it. "Slow" is not reproducible; a measured duration, on a named connection, against a named build, is.
- Anything reported only in a chat message. It will be read, agreed with, and lost by Friday.
- A duplicate you did not search for. Thirty seconds of searching the tracker is cheaper than a triage cycle, and if you do find a match, add your reproduction to it rather than opening a second thread.
The standard worth holding
One rule covers almost all of this: no follow-up question should be necessary. A team that adopts it finds its reports get shorter rather than longer, because the ceremony falls away and the preconditions arrive. It also makes the reports reusable — a defect written to this standard converts directly into a regression case with no rewriting, which is how a bug you paid to find stops coming back.
The remaining half of the problem is organisational rather than editorial. If reports arrive well written and still sit untouched for three weeks, the writing was never the constraint, and the triage path is what needs attention instead.
Quick answers
Common questions
What should a bug report always contain?
Preconditions, minimal reproduction steps that have been re-run from a clean state, expected versus actual behaviour with the source of the expectation named, the build identifier, and severity with its reasoning. Everything else is optional. The test is whether a developer can reach the failing state without asking you anything.
What is the difference between severity and priority?
Severity is the damage the defect does if it happens; priority is when it gets fixed relative to other work. The reporter sets severity from evidence and does not revise it afterwards, while product or engineering sets priority from the schedule. Merging the two into one field turns triage into a negotiation and destroys the ability to compare defects across releases.
Should intermittent bugs be reported?
Yes, but labelled honestly. State how many attempts produced the failure, what you were doing at the time, and attach logs from that moment. Filing an intermittent defect as though it were deterministic gets it closed as unreproducible, and it returns in a later release with nothing learned in between.
Does a longer bug report template improve report quality?
Rarely. The information that is usually missing is not a field anyone forgot to add, it is the state of the account, data and configuration before the first step. Adding fields tends to increase compliance and reduce reading, whereas insisting on a verified minimal reproduction improves both at once.