# Collect beta tester feedback build by build

> A beta feedback form with a personal link per tester and build, two rating scales and a weekly summary that shows whether each build got better.

Published 2026-10-25 · https://ikiform.com/guides/feedback-form/beta-tester-feedback

Give each tester a personal link that carries an opaque tester code and the build number, so the form knows who and which build without asking. Ask what they used the build for, two 1 to 5 ratings for stability and usefulness, and what stopped them. Then compare builds, not individual answers, to see whether each release moved the numbers.

## What's different about beta feedback

Public feedback comes from strangers, once. Beta feedback comes from the same small group of people, again and again, about software that changes every week. A single answer matters less than the trend: did build 1.4.0 feel more stable than 1.3.0 to the same people? That needs two things a normal feedback form doesn't have: who answered, and which build they were on.

Asking testers to type their email and version number fails in practice. They forget the version, mistype it, or write "latest". So both go in the link you send, and the page fills them in.

Diagram: The weekly beta feedback loop, from a new build to the next one

1. A new build goes out to the beta group.
2. A script makes one link per tester with their code and the build number, and you send them by email.
3. The tester opens the link. The page fills in the hidden tester and build fields and shows which build it's asking about.
4. A summary script reads every answer through the API and averages the scores per build.
5. The blockers from the lowest scores become the fix list for the next build.

Compare columns, not rows. One tester's bad week is noise; a drop across the group is a regression.

## The fields

Form: Beta feedback

- **Tester code** (`tester`, text, required)
- **Build** (`build`, text, required)
- **What did you use this build for?** (`tried`, textarea, required)
- **How stable did it feel?** (`stability`, rating, required)
- **How useful was it?** (`usefulness`, rating, required)
- **If you could no longer use it, how would you feel?** (`would_miss`, radio, required)
- **Anything that stopped you?** (`blockers`, textarea)

`tester` and `build` are hidden inputs filled from the link. Their patterns reject anything that didn't come from a real invite.

| Field | Why |
| --- | --- |
| `tester` | A random 10-character code. Lets you follow one person across builds without putting their email in a URL |
| `build` | Ties every score to a release. The pattern only accepts versions like `1.4.0` |
| `tried` | What they actually did. Scores without this are hard to interpret |
| `stability`, `usefulness` | Two `rating` fields, 1 to 5. Separate, because a stable build of the wrong thing is still the wrong thing |
| `would_miss` | How disappointed they'd be without it. A rising share of "Very" is the strongest sign the beta is working |
| `blockers` | Optional, and where the fix list comes from |

## Make one link per tester

Keep a plain list of tester emails in `testers.txt`. This script gives each a stable code the first time it sees them, stores the mapping in `testers.json`, and writes one invite link per tester for the build you name. Start `testers.json` as `{}`.

`links.mjs`:

```js
import { randomBytes } from "node:crypto";
import { readFileSync, writeFileSync } from "node:fs";

const BUILD = process.argv[2];
if (!/^\d+\.\d+\.\d+$/.test(BUILD ?? "")) throw new Error("Usage: node links.mjs 1.4.0");

const known = JSON.parse(readFileSync("testers.json", "utf8"));
const emails = readFileSync("testers.txt", "utf8").split(/\r?\n/).map((line) => line.trim().toLowerCase()).filter(Boolean);

for (const email of emails) {
  known[email] ??= randomBytes(5).toString("hex");
}
writeFileSync("testers.json", JSON.stringify(known, null, 2));

const rows = emails.map((email) => {
  const link = new URL("https://example.com/beta/feedback");
  link.searchParams.set("t", known[email]);
  link.searchParams.set("build", BUILD);
  return `${email},${link}`;
});
writeFileSync(`invites-${BUILD}.csv`, ["email,link", ...rows].join("\n"));
console.log(`${rows.length} links for build ${BUILD}`);
```

Send the CSV through your email tool's mail merge, or paste the links by hand for a small group. Keep `testers.json` private: it is the only place a code maps back to a person.

## The feedback page

Put this on a page at `/beta/feedback` on your site. It reads the code and build from the link, refuses to show the form without them, and posts the plain way. Set **Redirect after submit** to a thank-you page. It works on any static host or builder that allows a script.

`beta/feedback.html`:

```html
<p id="beta-missing" hidden>This page needs the link from your invite email.</p>

<form id="beta" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post" hidden>
  <h2>Feedback for build <span id="beta-build"></span></h2>
  <input type="hidden" name="tester" />
  <input type="hidden" name="build" />
  <label for="tried">What did you use this build for?</label>
  <textarea id="tried" name="tried" rows="4" required minlength="15"></textarea>
  <fieldset>
    <legend>How stable did it feel?</legend>
    <label><input type="radio" name="stability" value="1" required /> 1, it crashed a lot</label>
    <label><input type="radio" name="stability" value="2" /> 2</label>
    <label><input type="radio" name="stability" value="3" /> 3</label>
    <label><input type="radio" name="stability" value="4" /> 4</label>
    <label><input type="radio" name="stability" value="5" /> 5, no problems</label>
  </fieldset>
  <fieldset>
    <legend>How useful was it?</legend>
    <label><input type="radio" name="usefulness" value="1" required /> 1, not at all</label>
    <label><input type="radio" name="usefulness" value="2" /> 2</label>
    <label><input type="radio" name="usefulness" value="3" /> 3</label>
    <label><input type="radio" name="usefulness" value="4" /> 4</label>
    <label><input type="radio" name="usefulness" value="5" /> 5, I rely on it</label>
  </fieldset>
  <fieldset>
    <legend>If you could no longer use it, how would you feel?</legend>
    <label><input type="radio" name="would_miss" value="very" required /> Very disappointed</label>
    <label><input type="radio" name="would_miss" value="somewhat" /> Somewhat disappointed</label>
    <label><input type="radio" name="would_miss" value="not" /> Not disappointed</label>
  </fieldset>
  <label for="blockers">Anything that stopped you?</label>
  <textarea id="blockers" name="blockers" rows="3"></textarea>
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Send this week's feedback</button>
</form>

<script>
  const params = new URLSearchParams(location.search);
  const tester = params.get("t") ?? "";
  const build = params.get("build") ?? "";
  const form = document.getElementById("beta");
  if (/^[a-z0-9]{10}$/.test(tester) && /^\d+\.\d+\.\d+$/.test(build)) {
    form.elements.tester.value = tester;
    form.elements.build.value = build;
    document.getElementById("beta-build").textContent = build;
    form.hidden = false;
  } else {
    document.getElementById("beta-missing").hidden = false;
  }
</script>
```

> Anyone with a tester's link can answer as that tester. That's acceptable for a beta: the codes are random, the group is small, and an odd answer stands out. Don't use the code to grant access to anything.

## Compare builds

The dashboard shows individual answers. To compare builds, read them all with the SDK and group by `build`. Run this with a secret key in `IKIFORM_SECRET_KEY`:

`summary.mjs`:

```js
import { createClient } from "@ikiform/sdk";

const ikiform = createClient({ apiKey: process.env.IKIFORM_SECRET_KEY });
const builds = new Map();

for await (const submission of ikiform.submissions.iterate({ formId: "frm_xxxxxxxxxxxxxxxx", status: "received" })) {
  const { build, tester, stability, usefulness, would_miss } = submission.fields;
  const row = builds.get(build) ?? { testers: new Set(), stability: 0, usefulness: 0, very: 0, answers: 0 };
  row.testers.add(tester);
  row.stability += stability;
  row.usefulness += usefulness;
  row.very += would_miss === "very" ? 1 : 0;
  row.answers += 1;
  builds.set(build, row);
}

console.table(
  [...builds].map(([build, row]) => ({
    build,
    testers: row.testers.size,
    stability: (row.stability / row.answers).toFixed(1),
    usefulness: (row.usefulness / row.answers).toFixed(1),
    "very disappointed": `${Math.round((row.very / row.answers) * 100)}%`,
  })),
);
```

Ratings arrive as numbers, so the averages need no parsing. Count testers next to each average: a build with three answers can't be compared fairly with one that has thirty.

## Check it works

1. Run `node links.mjs 1.4.0` and open one of the links. The form shows "Feedback for build 1.4.0".
2. Open the page without the query string. Only the invite message shows.
3. Submit an answer, then change `build` to `latest` in the browser's dev tools and submit again. Ikiform answers `422` with "Use a version like 1.4.0".
4. Run `summary.mjs`. The table shows build 1.4.0 with one tester and your two scores.

## Gotchas

- Beta groups often test from one office, which shares an IP. The Standard limit counts them together, so choose Relaxed, 20 a minute per visitor for this form.
- Mobile betas on TestFlight already let testers send screenshot feedback to App Store Connect. Use that for screen-level bugs and this form for the weekly questions.
- Ask the same questions every week. Changing the wording breaks the comparison between builds.
- Testers answer less as the beta goes on. Keep the form to a couple of minutes, and thank people by name in release notes.
- Ikiform doesn't merge repeat answers. If a tester answers twice for one build, keep the later one in your summary.

## Troubleshooting

| Problem | What to do |
| --- | --- |
| `422` with "Open the link from your invite email" | The `tester` value doesn't match the pattern, usually because the link was cut off in an email client. Resend the link. |
| `429 rate_limited` | One visitor sent more than the form's rate limit allows. Standard allows 5 a minute per visitor. Wait for `Retry-After`, or choose Relaxed or Custom for forms that post from one shared IP. |
| `403 origin_not_allowed`, or every post lands in spam | The 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

### How do I know which beta tester sent feedback?

Give each tester a random code and put it in their personal link. The page copies it into a hidden field, and only your private mapping file connects the code to an email.

### How do I track feedback per build?

Put the build number in the link too, validate it with a pattern, and group answers by build. The SDK's submissions.iterate reads every answer for a summary.

### What questions should a beta feedback form ask?

What they used the build for, how stable and how useful it felt on a 1 to 5 scale, how disappointed they'd be without it, and what stopped them. Keep the same questions every week.

### Should beta testers report bugs in the same form?

Keep a separate bug report form for anything that needs reproducing. The weekly form is for how the build feels overall; mixing the two makes both harder to read.

## More guides

- [Feedback form for a website or app](https://ikiform.com/guides/feedback-form.md): Put a feedback form on any page or inside your app: a rating, a comment, the page it came from, spam filtering, and alerts when a score is low.
- [Bug report form](https://ikiform.com/guides/feedback-form/bug-report.md): 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.
- [NPS survey](https://ikiform.com/guides/feedback-form/nps-survey.md): Run an NPS survey with a 0 to 10 score in the email, a follow-up question on your own page that changes with the score, and a script that calculates NPS.
- [Feature request form](https://ikiform.com/guides/feedback-form/feature-request.md): A feature request form that asks for the problem instead of the solution, stores requests by theme, counts people not rows, and emails them when it ships.
- [React feedback widget](https://ikiform.com/guides/feedback-form/react-feedback-widget.md): A floating feedback button for a React app that opens a small panel, sends the screen, app version and account with each answer, and never loses a draft.
- [Cancellation survey](https://ikiform.com/guides/feedback-form/cancellation-survey.md): Ask why customers cancel inside your SaaS cancel flow, keep Skip one click away, save the reason to Stripe and Ikiform, and never delay the cancel.
