Bug Report Forms That Developers Love
"It doesn't work" is the most useless bug report ever written. Developers need specifics: what happened, what should have happened, how to reproduce it, and what environment it occurred in. A well-designed bug report form extracts this information without requiring users to think like engineers.
The goal isn't just collecting reports - it's collecting actionable reports. Every field should earn its place by helping developers understand and fix the issue faster.
Basic Issue Information
Start with what the user noticed. A clear title, severity level, and category help triage and route issues to the right team.
const issueSection = form.addSubform('issue', {
title: 'Issue Details'
});
issueSection.addRow(row => {
row.addTextbox('title', {
label: 'Bug Title',
isRequired: true,
placeholder: 'Brief description of the issue',
maxLength: 100
});
});
issueSection.addRow(row => {
row.addDropdown('severity', {
label: 'Severity',
isRequired: true,
options: [
{ id: 'critical', name: 'Critical - System unusable' },
{ id: 'high', name: 'High - Major feature broken' },
{ id: 'medium', name: 'Medium - Feature impaired' },
{ id: 'low', name: 'Low - Minor issue' }
]
});
row.addDropdown('category', {
label: 'Category',
isRequired: true,
options: [
{ id: 'ui', name: 'UI/Visual' },
{ id: 'functionality', name: 'Functionality' },
{ id: 'performance', name: 'Performance' },
{ id: 'security', name: 'Security' },
{ id: 'data', name: 'Data/Content' },
{ id: 'other', name: 'Other' }
]
});
});The severity dropdown uses plain language: "System unusable" is clearer than "Severity 1" for non-technical reporters. Categories help developers filter and prioritize their backlog.
Pro tip
Put severity descriptions in the options themselves. "Critical - System unusable" helps users self-classify correctly. Without guidance, everything becomes "critical".
Steps to Reproduce
This is the most important section. Developers can't fix what they can't reproduce. Three fields capture the complete picture: what to do, what should happen, what actually happens.
const stepsSection = form.addSubform('steps', {
title: 'Steps to Reproduce'
});
stepsSection.addRow(row => {
row.addTextarea('stepsToReproduce', {
label: 'Steps to Reproduce',
isRequired: true,
placeholder: '1. Go to...\n2. Click on...\n3. Observe...',
rows: 5
});
});
stepsSection.addRow(row => {
row.addTextarea('expectedBehavior', {
label: 'Expected Behavior',
isRequired: true,
placeholder: 'What should happen?',
rows: 3
});
});
stepsSection.addRow(row => {
row.addTextarea('actualBehavior', {
label: 'Actual Behavior',
isRequired: true,
placeholder: 'What actually happens?',
rows: 3
});
});The placeholder text in "Steps to Reproduce" shows the expected format. Users who see "1. Go to... 2. Click on... 3. Observe..." naturally follow that pattern. Without it, you get paragraphs of stream-of-consciousness text.
Reproducibility
How often does the bug happen? A bug that occurs every time is usually easier to fix than one that appears randomly.
issueSection.addRow(row => {
row.addRadioButton('reproducibility', {
label: 'How often does this occur?',
isRequired: true,
options: [
{ id: 'always', name: 'Always (100%)' },
{ id: 'often', name: 'Often (50-99%)' },
{ id: 'sometimes', name: 'Sometimes (10-49%)' },
{ id: 'rarely', name: 'Rarely (<10%)' },
{ id: 'once', name: 'Happened once' }
],
orientation: 'vertical'
});
});Percentage ranges help users classify without overthinking. "Always" vs "sometimes" is clear. Exact percentages ("happens 37% of the time") would be false precision.
Environment Information
Many bugs are environment-specific. What works in Chrome might break in Safari. What works on Windows might fail on macOS.
const envSection = form.addSubform('environment', {
title: 'Environment'
});
envSection.addRow(row => {
row.addDropdown('browser', {
label: 'Browser',
options: [
{ id: 'chrome', name: 'Chrome' },
{ id: 'firefox', name: 'Firefox' },
{ id: 'safari', name: 'Safari' },
{ id: 'edge', name: 'Edge' },
{ id: 'other', name: 'Other' }
]
});
row.addDropdown('os', {
label: 'Operating System',
options: [
{ id: 'windows', name: 'Windows' },
{ id: 'macos', name: 'macOS' },
{ id: 'linux', name: 'Linux' },
{ id: 'ios', name: 'iOS' },
{ id: 'android', name: 'Android' }
]
});
});
envSection.addRow(row => {
row.addTextbox('browserVersion', {
label: 'Browser Version',
placeholder: 'e.g., 120.0.6099.109'
});
row.addTextbox('appVersion', {
label: 'App Version',
placeholder: 'e.g., 2.4.1'
});
});Dropdowns for browser and OS are faster than text fields and give consistent data. Version fields stay as textboxes since the format varies and users can copy-paste.
See more form examples in our gallery.
Conditional Fields
Not every bug needs every field. Security issues need impact assessment. Non-critical issues can have workarounds. Show fields only when relevant.
const severity = issueSection.dropdown('severity');
const category = issueSection.dropdown('category');
// Show security-specific fields for security bugs
issueSection.addRow(row => {
row.addCheckboxList('securityImpact', {
label: 'Security Impact',
isVisible: () => category?.value() === 'security',
isRequired: () => category?.value() === 'security',
options: [
{ id: 'data-exposure', name: 'Data Exposure' },
{ id: 'unauthorized-access', name: 'Unauthorized Access' },
{ id: 'injection', name: 'Injection Vulnerability' },
{ id: 'auth-bypass', name: 'Authentication Bypass' },
{ id: 'other', name: 'Other' }
]
});
});
// Show workaround field for non-critical issues
stepsSection.addRow(row => {
row.addTextarea('workaround', {
label: 'Known Workaround',
isVisible: () => severity?.value() !== 'critical',
placeholder: 'Is there a way to work around this issue?',
rows: 2
});
});Security bugs get a mandatory "Security Impact" field with specific vulnerability types. Regular bugs get an optional "Workaround" field - if users know a workaround, that's valuable triage information.
Error Details
Error messages and console output are gold for debugging. Make it easy to provide them, but don't require them - not every bug shows an error.
const errorSection = form.addSubform('error', {
title: 'Error Details',
isCollapsible: true
});
errorSection.addRow(row => {
row.addTextarea('errorMessage', {
label: 'Error Message',
placeholder: 'Copy the exact error message if shown',
rows: 3
});
});
errorSection.addRow(row => {
row.addTextarea('consoleOutput', {
label: 'Console Output',
placeholder: 'Paste any relevant console errors (F12 > Console)',
rows: 4
});
});
errorSection.addRow(row => {
row.addTextbox('errorUrl', {
label: 'URL Where Error Occurred',
placeholder: 'https://...'
});
});Making this section collapsible keeps the form manageable. Technical users can expand and provide details. Non-technical users can skip it without feeling overwhelmed.
Attachments
Screenshots and screen recordings often explain issues better than words. Since direct file upload adds complexity, URL fields for hosted images/videos work well.
const attachSection = form.addSubform('attachments', {
title: 'Attachments'
});
attachSection.addRow(row => {
row.addTextbox('screenshotUrl', {
label: 'Screenshot URL',
placeholder: 'Link to screenshot (Imgur, Dropbox, etc.)'
});
});
attachSection.addRow(row => {
row.addTextbox('videoUrl', {
label: 'Screen Recording URL',
placeholder: 'Link to video (Loom, YouTube, etc.)'
});
});
attachSection.addRow(row => {
row.addTextarea('additionalContext', {
label: 'Additional Context',
placeholder: 'Any other information that might help...',
rows: 3
});
});Suggesting specific services (Imgur, Loom) in placeholders helps users who don't know where to host files. The "Additional Context" field catches anything that doesn't fit elsewhere.
Smart Priority Suggestions
You can compute a suggested priority based on severity and reproducibility. Critical bugs that always happen are P0. Low-severity bugs that rarely occur go to the backlog.
// Auto-suggest priority based on severity and reproducibility
const severity = issueSection.dropdown('severity');
const reproducibility = issueSection.radioButton('reproducibility');
const suggestedPriority = form.computedValue(() => {
const sev = severity?.value();
const repro = reproducibility?.value();
if (sev === 'critical') return 'P0 - Immediate';
if (sev === 'high' && repro === 'always') return 'P1 - High';
if (sev === 'high') return 'P2 - Medium';
if (sev === 'medium' && repro === 'always') return 'P2 - Medium';
if (sev === 'medium') return 'P3 - Low';
return 'P4 - Backlog';
});
issueSection.addRow(row => {
row.addTextPanel('prioritySuggestion', {
label: 'Suggested Priority',
computedValue: () => suggestedPriority(),
isVisible: () => !!severity?.value() && !!reproducibility?.value()
});
});This is a suggestion, not a mandate. Your team still makes the final call, but seeing the suggested priority helps reporters understand how their issue might be triaged.
Reporter Information
Sometimes you need to follow up for more details. Collect contact info and account identifiers to investigate user-specific issues.
const userSection = form.addSubform('user', {
title: 'Your Information'
});
userSection.addRow(row => {
row.addEmail('email', {
label: 'Email',
isRequired: true,
placeholder: 'For follow-up questions'
});
});
userSection.addRow(row => {
row.addTextbox('accountId', {
label: 'Account ID / Username',
placeholder: 'Helps us check your account state'
});
});
userSection.addRow(row => {
row.addCheckbox('canContact', {
label: 'You can contact me for more details',
defaultValue: true
});
});The "can contact me" checkbox sets expectations. Users who check it know they might get follow-up questions. Users who don't can still report bugs without committing to a conversation.
What Makes Bug Forms Work
A few principles make the difference between forms that get useful reports and forms that get noise:
Structure over prose: Separate fields for steps, expected, and actual behavior beat a single "describe the bug" textarea. Structure guides users to provide complete information.
Show don't tell: Placeholder text with examples ("1. Go to...") teaches the format better than instructions.
Required vs optional: Require the minimum needed to understand the issue. Make everything else optional. A partially-completed report is better than an abandoned form.
Plain language: "System unusable" beats "Severity 1". "How often does this occur?" beats "Reproducibility rate". Write for humans, not JIRA.
Conditional complexity: Show security fields only for security bugs. Show workaround fields only for non-critical issues. Keep the common case simple.
After the Form
A good bug report form is just the start. Consider what happens next:
- Send confirmation emails so reporters know their issue was received
- Route critical bugs to on-call teams automatically
- Include reporter email in the ticket for follow-up
- Generate a reference number users can track
The form captures information. Your workflow determines what happens with it.
Common Questions
Should I require screenshots?
No. Many bugs are behavioral or data-related and don't show visually. Make screenshots easy to provide but optional. If you require them, users will submit random screenshots just to pass validation, which doesn't help anyone.
How do I handle bugs from non-technical users?
Keep language simple, use dropdowns instead of free text where possible, and provide placeholder examples. The form should guide them to provide useful information without requiring technical knowledge. The 'steps to reproduce' format works for anyone.
Should I let users set priority themselves?
Showing a suggested priority based on their inputs is fine. Letting them override it usually isn't - everyone thinks their bug is high priority. Compute the suggestion, show it for transparency, but let your team make the final call.
How do I prevent duplicate bug reports?
You can't fully prevent duplicates with a form alone. Some teams show known issues before the form, others dedupe on the backend. At minimum, good search in your issue tracker helps users find existing reports before creating new ones.