7 fields
Bug report form
A bug report form with a summary, steps to reproduce, severity and the page URL, ready to forward to Linear or GitHub Issues by webhook.
Preview
Runs Ikiform's validator in your browser. Nothing is sent.
Copy the code
Every version below is generated from the same field definition. Replace frm_xxxxxxxxxxxxxxxx with your form id, or enter it in the Form ID box and the code updates.
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
<label for="summary">Summary</label>
<input id="summary" name="summary" type="text" placeholder="Export button does nothing on Safari" minlength="8" maxlength="140" required />
<label for="steps">Steps to reproduce</label>
<textarea id="steps" name="steps" rows="6" placeholder="Open the dashboard, click Export, nothing downloads" minlength="20" maxlength="8000" required></textarea>
<label for="expected">What did you expect?</label>
<textarea id="expected" name="expected" rows="3" maxlength="4000"></textarea>
<fieldset>
<legend>How bad is it?</legend>
<label><input type="radio" name="severity" value="blocker" required /> I can't continue</label>
<label><input type="radio" name="severity" value="major" required /> Something important is broken</label>
<label><input type="radio" name="severity" value="minor" required /> Small or cosmetic</label>
</fieldset>
<label for="page_url">Page where it happened</label>
<input id="page_url" name="page_url" type="url" />
<label for="screenshot_url">Screenshot or recording link</label>
<input id="screenshot_url" name="screenshot_url" type="url" aria-describedby="screenshot_url-help" />
<p id="screenshot_url-help">Optional. A link to an image or video, for example from a screenshot tool.</p>
<label for="email">Email for follow-up questions</label>
<input id="email" name="email" type="email" autocomplete="email" />
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
<button>Report bug</button>
</form>- HTML needs no JavaScript. After a successful post the browser is redirected to the form's redirect URL, or to Ikiform's hosted thank-you page.
- React uses the SDK:
npm install @ikiform/sdk. Field errors from the server appear under each field. - JavaScript enhances the HTML form with
fetch, so the visitor stays on the page. - The hidden
_gotchainput is the honeypot. Leave it empty and hidden.
challenge(FORM_ID) from @ikiform/sdk and pass it to submit as turnstileToken, or add it to the fetch body as cf-turnstile-response. Without it the post is refused with 403 challenge_required. See the Next.js guide and Spam protection.The fields
Paste this definition on the form's Fields tab in the dashboard. Ikiform then checks every post against it on the server, refuses bad data with 422 validation_failed, refuses any field that isn't listed, and stores typed values.
| Field id | Type | Required | Accepts |
|---|---|---|---|
summary | text | Yes | 8 to 140 characters |
steps | textarea | Yes | 20 to 8,000 characters |
expected | textarea | No | Up to 4,000 characters |
severity | radio | Yes | One of: blocker, major, minor |
page_url | url | No | An http: or https: URL |
screenshot_url | url | No | An http: or https: URL |
email | email | No | One valid address |
{
"version": 1,
"title": "Bug report",
"fields": [
{
"id": "summary",
"type": "text",
"label": "Summary",
"required": true,
"placeholder": "Export button does nothing on Safari",
"validation": {
"min_length": 8,
"max_length": 140
}
},
{
"id": "steps",
"type": "textarea",
"label": "Steps to reproduce",
"required": true,
"rows": 6,
"placeholder": "Open the dashboard, click Export, nothing downloads",
"validation": {
"min_length": 20,
"max_length": 8000
}
},
{
"id": "expected",
"type": "textarea",
"label": "What did you expect?",
"rows": 3,
"validation": {
"max_length": 4000
}
},
{
"id": "severity",
"type": "radio",
"label": "How bad is it?",
"required": true,
"options": [
{
"value": "blocker",
"label": "I can't continue"
},
{
"value": "major",
"label": "Something important is broken"
},
{
"value": "minor",
"label": "Small or cosmetic"
}
]
},
{
"id": "page_url",
"type": "url",
"label": "Page where it happened"
},
{
"id": "screenshot_url",
"type": "url",
"label": "Screenshot or recording link",
"help_text": "Optional. A link to an image or video, for example from a screenshot tool."
},
{
"id": "email",
"type": "email",
"label": "Email for follow-up questions"
}
],
"settings": {
"submit_label": "Report bug",
"success_message": "Thanks. We'll look into it."
}
}Next.js server action
To keep the form working without JavaScript in a Next.js app, post from a server action with a secret key. Create the key under API keys with the submissions:write scope and keep it out of the browser.
IKIFORM_SECRET_KEY=ik_sk_your_secret_key"use server";
export interface BugReportFormState {
ok: boolean;
message: string;
fields?: Record<string, string[]>;
values?: Record<string, string[]>;
}
export async function sendBugReportForm(_previous: BugReportFormState, formData: FormData): Promise<BugReportFormState> {
const body = new URLSearchParams();
for (const [name, value] of formData) {
if (typeof value === "string" && !name.startsWith("$ACTION")) body.append(name, value);
}
const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
method: "POST",
headers: {
Accept: "application/json",
Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
},
body,
});
const result = await response.json();
if (result.ok) return { ok: true, message: "Thanks. We'll look into it." };
const values = Object.fromEntries([...new Set(body.keys())].map((name) => [name, body.getAll(name)]));
return { ok: false, message: result.error.message, fields: result.error.fields, values };
}"use client";
import { useActionState } from "react";
import { type BugReportFormState, sendBugReportForm } from "./actions";
const initial: BugReportFormState = { ok: false, message: "" };
export function BugReportForm() {
const [state, action, pending] = useActionState(sendBugReportForm, initial);
if (state.ok) return <p>{state.message}</p>;
return (
<form action={action}>
<label htmlFor="summary">Summary</label>
<input id="summary" name="summary" type="text" placeholder="Export button does nothing on Safari" minLength={8} maxLength={140} required defaultValue={state.values?.summary?.[0]} />
{state.fields?.summary ? <p>{state.fields.summary[0]}</p> : null}
<label htmlFor="steps">Steps to reproduce</label>
<textarea id="steps" name="steps" rows={6} placeholder="Open the dashboard, click Export, nothing downloads" minLength={20} maxLength={8000} required defaultValue={state.values?.steps?.[0]} />
{state.fields?.steps ? <p>{state.fields.steps[0]}</p> : null}
<label htmlFor="expected">What did you expect?</label>
<textarea id="expected" name="expected" rows={3} maxLength={4000} defaultValue={state.values?.expected?.[0]} />
{state.fields?.expected ? <p>{state.fields.expected[0]}</p> : null}
<fieldset>
<legend>How bad is it?</legend>
<label><input type="radio" name="severity" value="blocker" required defaultChecked={state.values?.severity?.includes("blocker")} /> I can't continue</label>
<label><input type="radio" name="severity" value="major" required defaultChecked={state.values?.severity?.includes("major")} /> Something important is broken</label>
<label><input type="radio" name="severity" value="minor" required defaultChecked={state.values?.severity?.includes("minor")} /> Small or cosmetic</label>
</fieldset>
{state.fields?.severity ? <p>{state.fields.severity[0]}</p> : null}
<label htmlFor="page_url">Page where it happened</label>
<input id="page_url" name="page_url" type="url" defaultValue={state.values?.page_url?.[0]} />
{state.fields?.page_url ? <p>{state.fields.page_url[0]}</p> : null}
<label htmlFor="screenshot_url">Screenshot or recording link</label>
<input id="screenshot_url" name="screenshot_url" type="url" aria-describedby="screenshot_url-help" defaultValue={state.values?.screenshot_url?.[0]} />
<p id="screenshot_url-help">Optional. A link to an image or video, for example from a screenshot tool.</p>
{state.fields?.screenshot_url ? <p>{state.fields.screenshot_url[0]}</p> : null}
<label htmlFor="email">Email for follow-up questions</label>
<input id="email" name="email" type="email" autoComplete="email" defaultValue={state.values?.email?.[0]} />
{state.fields?.email ? <p>{state.fields.email[0]}</p> : null}
<input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
{state.message && !state.fields ? <p role="alert">{state.message}</p> : null}
<button disabled={pending}>{pending ? "Sending" : "Report bug"}</button>
</form>
);
}Set it up
- Create a form in the dashboard and copy its id.
- Paste the field definition on the Fields tab.
- Add your site under Allowed domains, plus
localhostwhile you develop. - Set the rate limit to Standard, 5 a minute per visitor if visitors post from the browser. Testers sometimes file several bugs in a row. Standard allows that while stopping floods. With the server action, every post comes from your server's IP, so choose Relaxed or Custom instead.
- Paste the code, send a test and check it arrives in the dashboard.
The browser is recorded for you
Every submission records the visitor's user agent, origin, referrer and country alongside the fields. Webhooks include them under data.meta, so you don't need hidden fields for the page or the browser. The user agent tells you the browser and operating system, which is what most bug report forms ask for by hand.
Screenshots
Ikiform doesn't accept file uploads yet. A post that contains a real file is refused with 422 files_not_supported; an empty file input is ignored. Ask for a link, as this form does with screenshot_url, or upload the image to your own storage first and submit its URL. The file upload form shows the pattern.
Into your issue tracker
Point a webhook at a small relay that creates an issue from each submission: summary becomes the title, steps and expected the body, and severity a label. The Linear and GitHub Issues guide has the full relay with signature checks.
Links and the spam score
This form can already send two links, page_url and screenshot_url. Three or more links across all fields add 2 to the spam score, so when both are filled in, one more link in the steps to reproduce is enough. With an identical repeat (+3) that reaches the threshold of 5, and the report is stored as spam without an error. Spam sends no webhooks or emails, so review spam submissions in the dashboard. Marking one as not spam sends its webhooks then.
Sending extra context from your app
An in-app reporter can add details like the app version. With fields defined, any key that isn't in the definition is refused as Unknown field, so add a field for it first, for example a text field with the id app_version. Field ids are lowercase snake_case, up to 64 characters.
Where to send it
Every submission can go to a signed webhook. These guides cover the tools that suit this form, with the relay code:
- Create Linear or GitHub issues from form submissions
- Send form submissions to Slack
- Send form submissions to Discord
- Send form submissions to a Notion database
Questions
Yes, through a webhook to a small relay that calls the Linear or GitHub API. Ikiform signs every delivery and retries failed ones for about 3 days.
Not as files; Ikiform refuses posts with files. Ask for a link to the screenshot, or upload it to your own storage first and submit the URL.
No. Ikiform records the user agent with every submission and includes it in webhooks.