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:

  1. Title that names the symptom and the place: "Checkout returns 500 when coupon has 0% discount" rather than "Checkout broken".
  2. Environment: versions, platform, account type, feature flags, region. Whatever makes your setup different from the default.
  3. Steps to reproduce, numbered, starting from a known state.
  4. Expected result: what should have happened, and why you think so.
  5. Actual result: what happened, quoted exactly.
  6. Evidence: error text, log lines, request IDs, screenshots or a recording, timestamps with time zone.
  7. 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