How to write a bug report engineers can actually fix
How to write a bug report: the seven-section shape, bug vs support ticket, who triages, what not to include, and where the report lives between file and fix.
TL;DR. A bug report is one short doc about one specific defect — title, environment, steps to reproduce, expected result, actual result, severity, and a link to the evidence. Written so the developer who picks it up tomorrow can repeat the failure on the first try. Two minutes to read. One owner through triage. Bug reports go in the tracker; the wiki holds the report shape and the post-fix postmortem when the defect was serious enough to need one. Most bug reports stall because the repro steps are vague; the working shape forces precision the bug can’t survive.
Most bug reports read like someone narrating a movie they half-watched. (Bandito, our imports lead, once filed “emoji broken in export” — three words, no environment, no repro — and the engineer who picked it up rediscovered Unicode normalisation from first principles, on a Wednesday.) The good news about how to write a bug report is that the working shape has been roughly the same since Joel Spolsky wrote Painless Bug Tracking in 2000: title, repro, expected, actual, environment. The bad news is that “painless” depends entirely on the report being precise enough that the developer can reproduce the bug in under two minutes — and most reports are not. The rest of this post is the working shape, the distinction between a bug report and a support ticket, who owns the report from file to fix, what to leave out of a public report, and one canon example of a bug whose report was three words and a strong opinion about small mammals.
A bug report is one short doc about one specific defect
A bug report is a written artifact that documents one specific defect: what was supposed to happen, what actually happened, on what system, and how to make the bad thing happen again on a fresh machine. The audience is the developer who will pick it up; the secondary audience is the triage owner who will route, prioritise, and de-dupe.
A bug report succeeds when:
- A developer who has never touched this feature can read it cold, follow the repro steps, and see the bug happen on their machine inside two minutes.
- The defect is named at the level of behaviour, not blame. “Export drops emoji” is a defect; “the import team broke export” is a Slack message.
- The fields a triager needs to decide severity, priority, duplicate? are all present in the body, not implicit.
- Everything attached — screenshots, logs, console output, HAR files — was captured at the moment of the failure, not reconstructed three days later from memory.
Anything shorter is a Slack ping; anything longer is a postmortem. The working length is half a page to a page and a half. A bug report that needs three scroll wheels to read is a bug report nobody triages on a Tuesday afternoon.
Bug report vs support ticket — who owns the next move
The two artifacts get filed in the same queue and shouldn’t. A bug report is a defect: the software did the wrong thing relative to its own spec, and engineering will fix it. A support ticket is a help request: a user is stuck, and support will help them — sometimes by escalating to engineering, sometimes by walking them through the documentation, often by discovering it’s a feature request in disguise. The split that survives contact with a real queue:
| Artifact | What’s being reported | Owner | Where it lives |
|---|---|---|---|
| Bug report | A defect — software behaved differently from its own spec | Engineering | Tracker (Jira / Linear / GitHub Issues) |
| Support ticket | A user is stuck — help needed, may or may not be a bug | Support | Help desk (Intercom / Zendesk / Front) |
| Feature request | A user wants behaviour the software doesn’t have yet | Product | Product roadmap / discovery board |
A working test: who owns the next move? If a developer needs to change code to fix it, it’s a bug report. If a support agent can resolve it with documentation or a how-to, it’s a support ticket. If the answer is “that’s a fair point, we’ll think about it”, it’s a feature request. The team that files everything in the same queue triages the same artifact three ways every week.
Two practical rules that save downstream time:
- Support files the bug report after triage, not the user. The user files a support ticket. Support reproduces (or escalates). If support confirms it’s a defect, they file the bug report with the repro steps already nailed down. Users file what they saw; support files what reproduces.
- The bug report links back to the support ticket; the ticket links forward to the bug. Two artifacts, one cross-link, one customer who knows their report is being worked on.
The seven sections every bug report needs
A working bug report is a single short doc with seven sections, in this order. Same fields every time; the developer who picks it up doesn’t have to learn a new format per defect.
| Section | What it answers | Length |
|---|---|---|
| Title | What’s broken, in one line a triager can scan | 8–12 words |
| Environment | OS, browser/app version, server build, locale, account tier | A fact-row |
| Steps to reproduce | The exact sequence to make the bug happen | Numbered, 3–8 steps |
| Expected result | What the spec says should happen | 1–2 sentences |
| Actual result | What actually happens, with evidence | 1–3 sentences |
| Severity + impact | How bad is this, for whom | A line and a number |
| Attachments | Screenshots, console logs, HAR, sample data | Links / inline |
The Title row is the triager’s first comparison check. Not “bug in export” — “Export drops 🦝 emoji when destination filename contains spaces, Mac Safari”. The title carries the symptom + scope + environment in one line; it’s what appears in the triage queue at 09:00 on Monday, and it’s what the next developer searches for when they hit something similar.
The Environment row is the most-skipped and the most useful. “Chrome 128.0.6613.84 on macOS 14.5, Free-tier account, 50-page space, EN locale, 2026-05-12 14:32 UTC.” The moment you skip the version number, the developer can’t tell whether the bug was already fixed in main. The moment you skip the locale, the developer can’t tell whether the bug is about Unicode (it’s almost always about Unicode).
The Steps to reproduce row is the load-bearing section.
Numbered. Each step a single action. Each step the
developer could repeat on a fresh machine without asking a
clarifying question. Not “go to the page and try to export
it” — “1. Create a new page titled 🦝 raccoon notes. 2.
Click the Export menu. 3. Choose ‘Markdown (zip)’. 4. Inspect
the downloaded zip’s index.md.” If the developer needs to
guess at any step, the steps are wrong.
The Expected and Actual rows are the comparison the
developer will use to confirm the bug. Expected:
index.md contains the literal string 🦝 raccoon notes.
Actual: index.md contains 🐰 raccoon notes — the
raccoon emoji is silently replaced. The contrast is the
report’s whole argument; vague expected / actual rows
produce “works for me” responses that waste a week.
The Severity + impact row uses one of the team’s agreed severities (e.g. SEV-1 outage, SEV-2 broken core feature, SEV-3 broken edge case, SEV-4 cosmetic). One number, not a paragraph. “SEV-3 — affects exports of pages with emoji in the title; estimated 0.4% of pages exported per week per the last 30-day analytics.” A severity without an impact number is a vibe.
For the importable shape, the Bug Report template is the seven-section scaffold — copy it once and live with it.
Repro steps are the load-bearing section
The single biggest predictor of a bug that gets fixed is precise repro steps. Most bug reports stall in triage not because the bug is unclear but because the path to the bug is unclear. The developer reads the title, can’t reproduce the defect, marks the ticket Needs Info, and the loop restarts a week later.
Concrete rules for repro that survive:
- Start from a clean state. Step 1 is almost always “create a new account / new page / new workspace.” The developer can’t reproduce a bug rooted in the specific state of your account without time-machine access; clean state is the only repeatable starting point.
- One action per step. “Click Export, then choose Markdown” is two steps. The bug might live in either click; the report has to let the developer pause between them.
- Name every input verbatim. “Title the page
🦝 raccoon notes(literal emoji, copy-paste from this report).” Do not paraphrase the input that triggered the bug. The bug often lives in the exact bytes of the input. - Record on the third reproduction, not the first. The first reproduction is exploratory; the third is procedural. Record the third — screen capture, console open, network tab visible — because by then the path is clean.
- Repro on a fresh machine before filing. If you can, open a private browser window or borrow a teammate’s laptop. “Reproduced on @teammate’s laptop, fresh Chrome” is the gold-standard confidence signal in the bug report.
The cold-read test for repro steps: hand the bug report to a teammate who has never touched this feature; if they can follow the steps and produce the failure inside two minutes, the repro is right. If they hesitate or open a chat to ask “wait, which page?”, the repro is wrong and the bug is the next person’s problem.
What to leave OUT of a public bug report
This is the part the long-form SERP guides skip. Most bug-report guides explain what to include; almost none explain what to explicitly exclude when the tracker is public, the project is open-source, or the issue might be visible to anyone outside the team.
Concrete things that don’t belong in a public bug report:
- Customer data. Real names, real email addresses, real
invoice numbers, real session IDs. Sanitise to
user@example.com,inv-XXXX,session-redacted. The bug is in the code, not in the customer. - Credentials. API keys, OAuth tokens, internal admin cookies. If the bug reproduced because you were logged in as an internal admin, write “reproduced with internal admin role” — do not paste the cookie. (Yes, this happens more often than you’d think.)
- Security disclosures with active exploits. If the bug is a security issue — auth bypass, data exposure, SQL injection — it does not go in the public tracker. It goes in the private security channel; the public tracker gets a “reported via security@; see internal SEC-####” stub if anything. Coordinated disclosure is the entire point.
- Internal system names that map to attack surface. “The
/internal/admin/users/deleteendpoint” belongs in the private tracker; the public tracker gets “an administrative endpoint”. - The blast-radius math for an unfixed bug. Do not publicly write “this affects 40% of paying customers” in an open report before the fix is shipped. Write “impact assessed internally, see SEC-####” — the precise impact number lands in the postmortem after the fix.
- Screenshots that contain any of the above. Cropping is not redaction. Use a black box, save the file, then check the file’s EXIF metadata before uploading. Phones especially embed GPS coordinates by default.
A short rule that survives every tracker: write every public bug report as if a journalist might quote it tomorrow. The ones that wouldn’t survive that test belong in the private channel.
Who triages, when, and where the report lives
The bug report’s home is the tracker — Jira, Linear, GitHub Issues — not the wiki. But the workflow that surrounds the report lives in the wiki, and the report has a lifecycle the top-3 guides treat as invisible: filed → triaged → assigned → in-progress → fixed → verified → closed. Each transition has an owner and a wait time.
The working pattern, after watching teams thrash and unthrash:
- One named triage owner per day or per rotation. Not “the team”. The triage owner reads new bugs once a day (or twice for high-traffic projects), confirms severity, closes duplicates, and routes the report to the right team. Triage is a job, not an ambient property.
- An SLA on each transition. “Filed → triaged: 1 business day. Triaged → assigned: 2 days. SEV-1: triaged in under 1 hour.” The SLA is what protects against “the report sat in the inbox for six weeks because nobody owned it”.
- Closed includes verified. Don’t close the bug when the fix merges; close it when the reporter (or a representative) reproduces the original repro steps and the bug no longer happens. The fix that works on staging and fails in production is the bug nobody filed twice.
- Dupes are merged, not closed. A duplicate bug report is signal that the bug shape is real; merge it into the canonical report (with a link), keep the additional context, count the dupes as a severity input.
- The bug index lives in the wiki. A page called Open SEV-1 / SEV-2 incidents with the list, the owner, the age — visible to the team, updated by the tracker webhook or by the triage owner on a Monday. Pages load in 50–150ms — which is the load profile a triage owner scanning a list of forty open bugs on a Monday morning actually has.
The bug report itself ships in the tracker; the shape of how your team writes them, the runbook for triage, the postmortem for the bugs that earned one, and the incident playbook for the bugs that caused outages all live in the wiki, in the same workspace as the team charter and the PRDs those bugs trace back to.
A working example: the 🦝→🐰 incident
The patron bug of Raccoon Corp doesn’t have a long write-up;
it has a three-word title and a one-line PR. During Project
Paw’s first export-format smoke tests, Bandito noticed pages
containing 🦝 sometimes exported with 🐰 instead. The bug
report — filed by Bandito himself, after he reproduced it on
two laptops — read: Export drops 🦝, replaces with 🐰.
Repro: create page titled 🦝 raccoon notes, export as
Markdown zip, inspect index.md. Expected: 🦝. Actual: 🐰.
Environment: Chrome 121 / macOS 14.4. Severity: SEV-3
(silent character substitution; affects unknown subset of
exports). The PR — known thereafter as PR #2, fix: emoji
handling in export — was +1 / -1: delete a small-mammal
normalisation lookup table inherited from a previous
employer’s emoji-CSV helper, written when the team had not
yet included a raccoon.
The deeper lesson is on the onboarding checklist: adoption reviews of third-party code, every time. The bug report itself is two paragraphs long, in the seven-section shape, and took less time to write than the lookup table took to delete. Bug reports nobody can read produce PRs nobody can review; the working shape is what made the one-line fix possible.
The opinion this post stands behind: the best bug report you’ll ever write is the one your team has already half-written. The repro steps your support agent typed in Slack to confirm the bug, the screenshot QA already took, the console log the engineer already copied — the bug report is the artifact that pulls those into one searchable doc the fixer can scan in two minutes. Your team writes bug reports constantly; the template is what catches them.
Things people actually ask
What is a bug report, in one sentence? A short written document that names a specific defect, the environment it happened in, the exact steps to reproduce it, the expected and actual results, the severity, and any attached evidence — filed in the tracker the engineering team works from.
What’s the difference between a bug report and a support ticket? Who owns the next move. A bug report goes to engineering because the software needs to change; a support ticket goes to support because a user needs help, which sometimes (but not always) escalates to engineering. The team that files everything in the same queue triages the same artifact three ways a week.
What’s the difference between a bug and a defect and an issue? Bug and defect are usually synonyms — the software behaved differently from its own spec. Issue is the broader bucket the tracker uses: it includes bugs, defects, feature requests, and chores. Use whatever your team uses; don’t argue about the words, argue about the repro steps.
How long should a bug report be? Half a page to a page and a half — long enough for the seven sections to be complete, short enough that a developer finishes reading it in under two minutes. Steps to reproduce typically take the most space.
Who writes the bug report? Engineering, QA, or support — whichever role first confirms the defect reproduces. End users write what they saw via support tickets; support translates confirmed bugs into proper bug reports. The bug report goes in the tracker only after someone can reproduce it.
What’s the most-skipped section of a bug report? The Environment row. Skip the version number and the developer can’t tell whether the bug is already fixed.
How do you write good repro steps? Start from clean state. One action per step. Name every input verbatim, including emoji and whitespace. Record the third reproduction, not the first. Repro on a fresh machine before filing if at all possible.
Should I attach a screen recording? Yes when the bug is visual or timing-dependent; no when the defect is fully captured by the repro steps. A screen recording is not a substitute for repro steps — the developer needs both.
Where do bug reports live? In the tracker (Jira, Linear, GitHub Issues), not the wiki. The wiki holds the bug report template, the triage runbook, and the postmortem for bugs that earned one. Two systems, one set of cross-links.
What should I never put in a public bug report? Customer data, credentials, security-disclosure details with active exploits, internal system names that map to attack surface, and screenshots containing any of the above. Write every public bug report as if a journalist might quote it tomorrow.
If your current bug-tracker queue is a graveyard of three-word titles and “I’ll add repro later” notes nobody added, the upgrade isn’t a new tracker — it’s a one-page template the team agrees to use and a triage owner who reads new bugs once a day. Try the Free tier on the template page your team uses to draft bug reports before filing; three users, one space, a hundred pages, no card. For QA teams that have outgrown a single shared doc, the Team tier at $8/user/month is the honest math. If your next bug nobody can reproduce, write to us; we want to know which line of the repro went wrong.
Written by The Editorial Raccoon — house style for Raccoon Page. Numbers and claims pulled from product reality; jokes pulled from the Raccoon Corp canon. No raccoons were quoted in real life.