Last year we measured how long issues in our tracker waited between "opened" and "someone started working on it". The median was under a day for issues with clear steps and a log excerpt, and nine days for issues that said some version of "checkout is broken". The extra eight days were not spent fixing. They were spent asking questions, waiting for answers, and trying to reproduce something nobody had described precisely.
This guide is the structure we now use, whether the reporter is a developer, a support agent or a customer filling in a form. The test of a good report is simple: could an engineer who joined last week, who has never seen your screen and cannot message you, reproduce the bug from the report alone? By the end you will have a template, a worked example and a GitHub issue form you can adapt.
The anatomy of a fixable report
A report that passes the stranger test has seven parts:
- Title that names the symptom and the place: "Checkout returns 500 when coupon has 0% discount" rather than "Checkout broken".
- Environment: versions, platform, account type, feature flags, region. Whatever makes your setup different from the default.
- Steps to reproduce, numbered, starting from a known state.
- Expected result: what should have happened, and why you think so.
- Actual result: what happened, quoted exactly.
- Evidence: error text, log lines, request IDs, screenshots or a recording, timestamps with time zone.
- Impact: who is affected and how badly, which feeds severity.
Optional but valuable: frequency ("3 of 10 attempts"), first-seen time, and anything you already ruled out.
A worked example: before and after
This is a real report from our support channel, lightly edited:
Title: Export not working
The export doesn't work for my customer. They tried a few times. Can someone look? It's urgent.
Every engineer who picked this up had to ask the same five questions. Here is the same bug after the support agent used the template:
**Title:** CSV export of invoices returns an empty file when the date range spans a DST change
**Environment**
- Web app, production, EU region
- Account: acct_demo_3315 (Business plan), account time zone Europe/Berlin
- Browser: Firefox 135 on Windows 11 (also reproduced on Chrome 133, macOS 15)
**Steps to reproduce**
1. Log in as an admin of an account whose time zone is Europe/Berlin.
2. Go to Invoices, then Export.
3. Set the range from 2024-10-20 to 2024-10-31 and click Export CSV.
**Expected:** a CSV with the 212 invoices visible in the list for that range.
**Actual:** a CSV containing only the header row. No error is shown.
**Evidence**
- Request ID from the response header: `req_7d2e0c91`
- Time: 2025-02-19 14:06 UTC
- Ranges that do not include 2024-10-27 export correctly (tried 10-01 to 10-26).
**Impact:** This customer files monthly VAT returns from the export; deadline is 2025-02-28.
Two other EU accounts reported the same in the last week.
The engineer who picked it up searched logs by request ID, saw that the range bounds were derived with same-zone datetime arithmetic that ignores the DST shift, and had a fix in review the same afternoon. The report did most of the work.
Environment: capture what makes you different
"Latest Chrome" is not an environment. Different products need different fields, but the principle is the same: record what a stranger would otherwise guess.
| Product type | Fields worth capturing |
|---|---|
| Web app | Browser and version, OS, account or tenant, plan, feature flags, region, extensions if relevant |
| Mobile app | App version and build number, OS version, device model, network type, install source |
| API or SDK | SDK version, language runtime, request ID, endpoint, request body with secrets removed |
| CLI or library | Tool version (--version output), OS, install method, config file excerpt |
| Infrastructure | Cloud region, instance type, image or chart version, recent deploy IDs |
Let tools collect it where possible. Our web app has a "Copy diagnostics" button in the help menu
that copies app version, browser, time zone, locale and the last five request IDs. CLIs can do the
same with a doctor or --version --verbose command.
Steps: start from a known state, one action per line
Good steps are boring. They start from something anyone can reach ("logged out, fresh browser profile", "new project from the default template") and contain one action per line. Avoid "the usual way" and "then do the thing with the settings". If the bug depends on data, say how to create that data, or attach a sanitised sample.
Check your steps by following them yourself, literally, before submitting. A surprising number of reports skip the one step the reporter does by habit, such as switching the account's time zone.
Expected versus actual: quote, do not paraphrase
"It errors" hides the most useful information in the report. Copy error text exactly, including codes and IDs. Prefer text over screenshots for errors and logs, because text is searchable and can be pasted into a log query. Use a screenshot or short recording when the bug is visual or depends on timing.
For the expected result, say why you expect it: "the list shows 212 invoices for that range" or "the docs for this endpoint say it returns 404 for unknown IDs". Sometimes writing that sentence reveals that the behaviour is intended, which saves everyone time.
Minimal reproduction: the strongest evidence
For developer-facing bugs, a minimal reproduction beats any amount of description: the smallest program, config or dataset that still shows the bug. Building one usually takes a few rounds of deletion: remove a dependency, check the bug is still there, repeat.
# repro.py: smallest script that shows the export bug, no app code required
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
tz = ZoneInfo("Europe/Berlin")
start = datetime(2024, 10, 20, tzinfo=tz)
end = datetime(2024, 10, 31, 23, 59, 59, tzinfo=tz)
same_zone = (end - start).total_seconds() / 3600 # same tzinfo: wall-clock difference
utc = (end.astimezone(timezone.utc) - start.astimezone(timezone.utc)).total_seconds() / 3600
print(round(same_zone, 2), round(utc, 2)) # 288.0 289.0: the DST hour is invisible to same-zone math
Even when a repro is not feasible, describing what you removed or ruled out ("happens without browser extensions", "not on staging") narrows the search.
Severity versus priority
Teams lose a lot of time arguing about labels because one field is used for two different questions. Keep them separate:
- Severity describes the impact of the bug itself. The reporter can usually assess it.
- Priority describes when the team will fix it, given everything else. The owning team sets it.
| Severity | Definition we use | Example |
|---|---|---|
| S1 Critical | Data loss, security exposure, or core flow down for many users with no workaround | Payments failing for all cards |
| S2 Major | Core flow broken for some users, or a workaround that is costly | Export empty for EU accounts across DST |
| S3 Minor | Non-core feature wrong, easy workaround | Sort order wrong on a secondary list |
| S4 Cosmetic | Visual or wording issue, no functional impact | Misaligned icon in settings |
A cosmetic bug on the pricing page during a launch can be high priority with S4 severity; an S2 bug in a feature being retired next week can reasonably wait.
Make the right thing the easy thing
People write the report the form asks for. On GitHub, issue forms let you require fields and provide dropdowns instead of free text:
# .github/ISSUE_TEMPLATE/bug_report.yml
name: Bug report
description: Something is not working as documented
labels: ["bug", "needs-triage"]
body:
- type: input
id: version
attributes:
label: App or package version
placeholder: "e.g. 4.12.1 (copy from Settings > About)"
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
value: |
1.
2.
3.
validations:
required: true
- type: textarea
id: expected_actual
attributes:
label: Expected result and actual result
validations:
required: true
- type: textarea
id: logs
attributes:
label: Error text or logs (remove tokens and personal data)
render: text
- type: dropdown
id: severity
attributes:
label: Impact
options:
- Data loss, security, or core flow down
- Core flow broken for some users
- Minor feature wrong, workaround exists
- Cosmetic
validations:
required: true
Redact before you paste
Logs, HAR files and screenshots routinely contain session cookies, API keys, email addresses and
customer data. Before attaching evidence, remove Authorization and Cookie headers, replace
tokens with placeholders such as sk_test_xxx, and crop screenshots to the relevant area. If the
tracker is public, assume everything in it is permanent.
Common mistakes
| Mistake | Why it hurts | Do this instead |
|---|---|---|
| Vague title like "Broken" | Cannot be triaged or found later | Symptom plus location |
| Paraphrased error | Unsearchable, loses codes | Paste exact text and request IDs |
| Missing starting state | Steps work only on the reporter's setup | Start from a state anyone can reach |
| Several bugs in one issue | Partial fixes close the whole issue | One bug per report |
| "Urgent" instead of impact | Priority inflation, no information | Describe who is affected and how |
Key takeaways
- Write for a stranger who cannot ask you questions.
- Environment, numbered steps, expected and actual, evidence and impact are the core.
- Exact error text and request IDs are worth more than any description.
- A minimal reproduction is the fastest path to a fix.
- Keep severity (impact) separate from priority (scheduling).
- Use forms and diagnostics tools so good reports are the default, and redact secrets first.
References
- Simon Tatham: How to Report Bugs Effectively — https://www.chiark.greenend.org.uk/~sgtatham/bugs.html
- GitHub Docs: Syntax for issue forms — https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-issue-forms
- Mozilla Firefox source docs: Bug types and triage — https://firefox-source-docs.mozilla.org/bug-mgmt/guides/bug-types.html
- Python documentation:
zoneinfo— https://docs.python.org/3/library/zoneinfo.html
Comments
0No comments yet. Be the first to comment!
Sign In to post a comment.