# ベータテスターのフィードバックをビルドごとに集める

> テスターとビルドごとの個別リンク、2 つの評価スケール、各ビルドが良くなったかがわかる週次サマリーを備えたベータ版フィードバックフォームです。

公開日 2026-10-25 · https://ikiform.com/ja/guides/feedback-form/beta-tester-feedback

各テスターに、不透明なテスターコードとビルド番号を含む個別リンクを渡します。こうすればフォームは尋ねなくても誰がどのビルドかわかります。そのビルドで何をしたか、安定性と有用性の 1〜5 の評価 2 つ、そして何に妨げられたかを尋ねます。そのうえで個々の回答ではなくビルド同士を比べ、リリースごとに数字が動いたかを確認します。

## ベータ版のフィードバックはどこが違うのか

公開のフィードバックは、見知らぬ人から 1 回だけ届きます。ベータ版のフィードバックは、毎週変わるソフトウェアについて、同じ少人数のグループから何度も届きます。1 件の回答より傾向のほうが重要です。同じ人たちにとって、ビルド 1.4.0 は 1.3.0 より安定していると感じられたか。それには、通常のフィードバックフォームにない 2 つの情報が必要です。誰が回答したかと、どのビルドを使っていたかです。

テスターにメールアドレスとバージョン番号を入力してもらう方法は、実際にはうまくいきません。バージョンを忘れたり、打ち間違えたり、「latest」と書いたりします。そこで両方を送るリンクに含め、ページがそれを埋めます。

図: 新しいビルドから次のビルドまでの、週次のベータ版フィードバックのループ

1. 新しいビルドをベータグループに配布します。
2. スクリプトがテスターごとにコードとビルド番号入りのリンクを 1 つずつ作り、それをメールで送ります。
3. テスターがリンクを開きます。ページが非表示のテスターとビルドのフィールドを埋め、どのビルドについての質問かを表示します。
4. サマリースクリプトが API ですべての回答を読み込み、ビルドごとにスコアを平均します。
5. 最も低いスコアの回答にあった障害が、次のビルドの修正リストになります。

行ではなく列を比べてください。1 人のテスターの調子が悪い週はノイズです。グループ全体で下がっていればリグレッションです。

## フィールド

フォーム: Beta feedback

- **テスターコード** (`tester`, text, 必須)
- **ビルド** (`build`, text, 必須)
- **このビルドで何をしましたか？** (`tried`, textarea, 必須)
- **どのくらい安定していると感じましたか？** (`stability`, rating, 必須)
- **どのくらい役に立ちましたか？** (`usefulness`, rating, 必須)
- **もう使えなくなったら、どう感じますか？** (`would_miss`, radio, 必須)
- **妨げになったことはありますか？** (`blockers`, textarea)

`tester` と `build` はリンクから埋められる非表示の入力欄です。そのパターンにより、本物の招待から来ていないものは拒否されます。

| フィールド | 理由 |
| --- | --- |
| `tester` | ランダムな 10 文字のコード。メールアドレスを URL に入れずに、同じ人をビルドをまたいで追えます |
| `build` | すべてのスコアをリリースに結び付けます。パターンは `1.4.0` のようなバージョンだけを受け付けます |
| `tried` | 実際に何をしたか。これがないとスコアを解釈しにくくなります |
| `stability`, `usefulness` | 1〜5 の `rating` フィールドが 2 つ。分けているのは、間違ったものを作った安定したビルドは、やはり間違っているからです |
| `would_miss` | それがなくなったらどれだけ残念か。「とても」の割合が増えていれば、ベータ版がうまくいっている何よりの兆候です |
| `blockers` | 任意。修正リストはここから生まれます |

## テスターごとにリンクを 1 つ作る

テスターのメールアドレスを `testers.txt` にそのまま並べておきます。このスクリプトは、初めて見たテスターに固定のコードを割り当て、その対応を `testers.json` に保存し、指定したビルドについてテスターごとに招待リンクを 1 つ書き出します。`testers.json` は `{}` から始めてください。

`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}`);
```

CSV はメールツールの差し込み機能で送るか、少人数ならリンクを手で貼り付けます。`testers.json` は非公開にしてください。コードを人に結び付けられる唯一の場所です。

## フィードバックページ

これをサイトの `/beta/feedback` のページに置きます。リンクからコードとビルドを読み取り、それがなければフォームを表示せず、プレーンな方法で送信します。**Redirect after submit** にはサンクスページを設定してください。スクリプトを使える静的ホストやサイトビルダーならどこでも動きます。

`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>
```

> テスターのリンクを持っていれば、誰でもそのテスターとして回答できます。ベータ版ならそれで構いません。コードはランダムで、グループは小さく、不自然な回答は目立ちます。コードを何かへのアクセス権付与には使わないでください。

## ビルドを比較する

ダッシュボードには個々の回答が表示されます。ビルドを比較するには、SDK ですべての回答を読み込み、`build` でグループ化します。`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)}%`,
  })),
);
```

評価は数値として届くので、平均を出すのにパースは不要です。各平均の横にテスター数を数えておきましょう。回答が 3 件のビルドと 30 件のビルドは公平に比較できません。

## 動作を確認する

1. `node links.mjs 1.4.0` を実行し、リンクを 1 つ開きます。フォームに「Feedback for build 1.4.0」と表示されます。
2. クエリ文字列なしでページを開きます。招待のメッセージだけが表示されます。
3. 回答を送信したら、ブラウザーの開発者ツールで `build` を `latest` に変えて、もう一度送信します。Ikiform は「Use a version like 1.4.0」と共に `422` を返します。
4. `summary.mjs` を実行します。表にはビルド 1.4.0 がテスター 1 人と自分の 2 つのスコアで表示されます。

## ハマりどころ

- ベータグループは 1 つのオフィスからテストすることが多く、その場合は IP を共有します。Standard の制限はそれらをまとめて数えるので、このフォームでは訪問者ごとに 1 分あたり 20 件の Relaxed を選んでください。
- TestFlight のモバイルベータでは、テスターがすでにスクリーンショット付きのフィードバックを App Store Connect に送れます。画面単位のバグにはそれを使い、週次の質問にはこのフォームを使ってください。
- 毎週同じ質問をしてください。言い回しを変えると、ビルド間の比較が崩れます。
- ベータ版が進むにつれて、テスターの回答は減っていきます。フォームは数分で終わるようにし、リリースノートで名前を挙げて感謝を伝えましょう。
- Ikiform は重複する回答をまとめません。テスターが 1 つのビルドに 2 回回答した場合は、サマリーでは後の回答を残してください。

## トラブルシューティング

| 問題 | 対処法 |
| --- | --- |
| 「Open the link from your invite email」と共に `422` | `tester` の値がパターンに合っていません。たいていはメールクライアントでリンクが途中で切れたためです。リンクを送り直してください。 |
| `429 rate_limited` | 1 人の訪問者がフォームのレート制限を超えて送信しました。Standard では訪問者ごとに 1 分あたり 5 件までです。`Retry-After` まで待つか、共有の IP 1 つから送信するフォームには Relaxed か Custom を選んでください。 |
| `403 origin_not_allowed` が返る、またはすべての送信がスパムになる | ページのドメインがフォームの **Allowed domains** にありません。ドメインと、ローカルテスト用の `localhost` を追加してください。登録したドメインのサブドメインは許可されます。 |

## よくある質問

### どのベータテスターがフィードバックを送ったかを知るには？

各テスターにランダムなコードを割り当て、個別リンクに含めます。ページがそれを非表示のフィールドにコピーし、コードとメールアドレスを結び付けるのは自分の非公開の対応ファイルだけです。

### ビルドごとにフィードバックを追跡するには？

ビルド番号もリンクに含め、パターンで検証し、回答をビルドごとにグループ化します。SDK の submissions.iterate でサマリー用にすべての回答を読み込めます。

### ベータ版のフィードバックフォームでは何を尋ねるべきですか？

そのビルドで何をしたか、安定性と有用性を 1〜5 でどう感じたか、なくなったらどれだけ残念か、何に妨げられたか。毎週同じ質問を続けてください。

### ベータテスターにはバグも同じフォームで報告してもらうべきですか？

再現が必要なものには、別のバグ報告フォームを用意してください。週次のフォームはビルド全体の使用感を聞くためのものです。両者を混ぜると、どちらも読みにくくなります。

## その他のガイド

- [ウェブサイトやアプリのフィードバックフォーム](https://ikiform.com/ja/guides/feedback-form.md): どのページにもアプリ内にもフィードバックフォームを設置。評価、コメント、送信元ページ、スパム対策、低評価時の通知まで。
- [バグ報告フォーム](https://ikiform.com/ja/guides/feedback-form/bug-report.md): 手順と深刻度は本人に尋ね、URL、ブラウザー、ビューポート、直近のエラーはページが自動で追加するバグ報告フォームです。
- [NPS アンケート](https://ikiform.com/ja/guides/feedback-form/nps-survey.md): メールに 0〜10 のスコアを載せ、スコアに応じて変わる追加質問を自分のページで尋ね、スクリプトで NPS を計算する NPS アンケートを実施します。
- [機能要望フォーム](https://ikiform.com/ja/guides/feedback-form/feature-request.md): 解決策ではなく課題を尋ね、要望をテーマ別に保存し、件数ではなく人数で数え、リリース時にメールで知らせる機能要望フォームです。
- [React フィードバックウィジェット](https://ikiform.com/ja/guides/feedback-form/react-feedback-widget.md): React アプリ用のフローティングのフィードバックボタン。小さなパネルを開き、回答ごとに画面、アプリのバージョン、アカウントを送り、下書きを失いません。
- [解約アンケート](https://ikiform.com/ja/guides/feedback-form/cancellation-survey.md): SaaS の解約フローで解約理由を尋ね、スキップはワンクリックのまま、理由を Stripe と Ikiform に保存し、解約を決して遅らせない方法。
