Feedback form for a website or app

Feedback form for a website or app6 min read

Add a bug report form that captures the page, browser and steps

A bug report form that asks people for steps and severity, and lets the page add the URL, browser, viewport and recent errors on its own.

Preet SutharFounder, Ikiform

Ask the reporter only for what they know: what went wrong, what they did before, and how much it blocks them. Let the page fill in the rest at submit time: the page URL, the browser's user agent, the viewport and the last few JavaScript errors. Post it to a form backend and forward each report to your issue tracker by webhook.

What's different about a bug report

A bug report is only useful if you can reproduce it, and reporters rarely know the details you need. Ask someone which browser they use and you get "Chrome" or "the normal one". Ask for the URL and you get the name of the page. So split the form in two: questions only the reporter can answer, and context the page already knows.

Which parts of a bug report the person types and which the page addsthe reporter typesthe page adds at submitsummaryone line, 8 to140 charactersstepswhat they didright beforeseverityblocker, majoror minorscreenshotlinkoptional, aURL not a filepage_urlorigin andpath, no queryuser_agentbrowser and OSviewportsize and pixelratiorecent_errorslast five,hosts strippedWhich parts of a bug report the person types and which the page addsthe reporter typesthe page adds at submitsummaryone line, 8 to 140 charactersstepswhat they did right beforeseverityblocker, major or minorscreenshot linkoptional, a URL not a filepage_urlorigin and path, no queryuser_agentbrowser and OSviewportsize and pixel ratiorecent_errorslast five, hosts stripped
  1. The reporter writes a one-line summary.
  2. They describe what they did right before it broke.
  3. They pick how badly it blocks them.
  4. They can paste a link to a screenshot or recording.
  5. At submit, the page adds its own address without the query string.
  6. It adds the browser's user agent.
  7. It adds the window size and pixel ratio.
  8. It adds the last few JavaScript errors it saw, with hostnames removed.
The top row is all the reporter sees. The bottom row is hidden inputs, filled in the moment they press Send.

Three constraints shape the code:

  • The referrer isn't the page. Browsers send only your origin on a cross-site post by default, so meta.referrer won't tell you the path. Send page_url yourself.
  • A server post hides the browser. Ikiform records the user agent of whoever makes the request. From a server action, that's your server. Forward the visitor's user-agent header as a field.
  • Stack traces are full of links. Each frame has a script URL, and three or more links add 2 to the spam score. Strip the hosts before you send errors, or a report with a pasted screenshot link can tip into spam.

The fields

Preview

Runs Ikiform's validator in your browser. Nothing is sent.

How much does it block you?
The last five fields are hidden inputs in the real form. With them defined, Ikiform checks their length and type like any other field.

Severity is phrased as what it does to the reporter, not as a priority label. People pick "I can't continue" accurately; they pick "Critical" for everything. Steps has a minimum of 20 characters, which turns "it's broken" into a sentence you can act on.

Capture the context

The browser fills the hidden fields in the submit handler, after the person is done typing, so the error list includes anything that went wrong while they wrote the report.

The form posts the plain way; the script only fills the hidden inputs during submit, before the browser builds the request. Set data-app-version on the <html> element at build time. The browser posts directly, so Ikiform's own meta.user_agent matches the user_agent field.

report-bug.html
<form id="bug" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <label for="summary">What went wrong?</label>
  <input id="summary" name="summary" required minlength="8" maxlength="140" />
  <label for="steps">What did you do right before?</label>
  <textarea id="steps" name="steps" rows="5" required minlength="20"></textarea>
  <label for="expected">What did you expect to happen?</label>
  <textarea id="expected" name="expected" rows="3"></textarea>
  <fieldset>
    <legend>How much does it block you?</legend>
    <label><input type="radio" name="severity" value="blocker" required /> I can't continue</label>
    <label><input type="radio" name="severity" value="major" /> I found a workaround</label>
    <label><input type="radio" name="severity" value="minor" /> It's cosmetic</label>
  </fieldset>
  <label for="screenshot_url">Link to a screenshot or recording</label>
  <input id="screenshot_url" name="screenshot_url" type="url" />
  <label for="email">Email for follow-up questions</label>
  <input id="email" name="email" type="email" />
  <input type="hidden" name="page_url" />
  <input type="hidden" name="user_agent" />
  <input type="hidden" name="viewport" />
  <input type="hidden" name="app_version" />
  <input type="hidden" name="recent_errors" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Send report</button>
</form>

<script>
  const recent = [];
  function remember(text) {
    recent.push(String(text).replace(/https?:\/\/[^/\s]+/g, "").slice(0, 400));
    if (recent.length > 5) recent.shift();
  }
  window.addEventListener("error", (event) => remember(`${event.message} at ${event.filename}:${event.lineno}`));
  window.addEventListener("unhandledrejection", (event) => remember(`Unhandled: ${event.reason?.stack ?? event.reason}`));

  document.getElementById("bug").addEventListener("submit", (event) => {
    const fields = event.currentTarget.elements;
    fields.page_url.value = location.origin + location.pathname;
    fields.user_agent.value = navigator.userAgent;
    fields.viewport.value = `${innerWidth}x${innerHeight} @${devicePixelRatio}x`;
    fields.app_version.value = document.documentElement.dataset.appVersion ?? "";
    fields.recent_errors.value = recent.join("\n---\n");
  });
</script>
The server-side versions post with a secret key, so Ikiform skips Allowed domains and its human check. Every post then comes from your server's IP, and the Standard limit of 5 a minute per visitor counts all your visitors together. Choose the Relaxed or Custom rate limit for a form you post through a server, and keep IKIFORM_SECRET_KEY in server-only environment variables.

The success message shows the submission id. When the reporter writes to support later, that id finds the report in one search. The bug report template has a shorter version without the captured context.

Into your issue tracker

Reports belong where the fix will be tracked. Add a webhook that points at a relay which creates an issue: summary as the title, the steps and context as the body, severity as a label. The Linear and GitHub Issues guide has that relay, with signature checks and retries. The webhooks docs list every meta value the payload carries. Put the captured fields in a collapsed block at the end of the issue body so the steps stay readable.

FieldIn the issue
summaryTitle
steps, expectedBody, first
severityA label, so blockers can be filtered
page_url, user_agent, viewport, app_versionA details block under the steps
recent_errorsA code block, last

Check it works

  1. Open the page in a desktop browser, run undefinedFunction() in the console, then send a report.
  2. Open the submission in the dashboard. recent_errors contains the ReferenceError with no https:// host in it.
  3. user_agent shows your browser, not node, in the server versions too.
  4. page_url has the path but not the query string.
  5. Send a report with a 10-character steps. Ikiform answers 422 naming steps, and the form shows the message.

Gotchas

  • File uploads aren't supported yet. A post with a file gets 422 files_not_supported. Ask for a link to a screenshot, or upload the image to your own storage first and submit its URL.
  • Each value can be up to 16 KB. Five errors cut to 400 characters stay far below that; a full console dump wouldn't.
  • Chromium browsers freeze parts of the user agent string, such as the macOS version, which always reads 10_15_7. Use the browser name and major version, not the OS version, when you triage.
  • Errors from scripts on another origin show only Script error. unless that script is served with CORS headers and loaded with crossorigin.
  • Don't capture form contents, local storage or cookies into the report. They can hold passwords and tokens, and you'd store them in plain text.

Troubleshooting

ProblemWhat to do
422 validation_failedThe post doesn't match the form's field definition. error.fields names each field and the reason. Match the input name to the field id.
422 files_not_supportedThe form has an <input type="file"> with a file chosen. File uploads aren't supported yet; remove the input or ask for a link instead.
Reports with screenshot links land in spamCount the links across all fields. Three or more add to the score; with a repeat of the same report it reaches the threshold. Strip hosts from errors and mark the report as not spam.
403 origin_not_allowed, or every post lands in spamThe page's domain isn't in the form's Allowed domains. Add it, plus localhost for local tests. Subdomains of a listed domain are allowed.

Questions

Stuck on a step, or want a guide for your situation? Email hello@ikiform.com.