ウェブサイトやアプリのフィードバックフォーム2 分で読めます
ベータテスターのフィードバックをビルドごとに集める
テスターとビルドごとの個別リンク、2 つの評価スケール、各ビルドが良くなったかがわかる週次サマリーを備えたベータ版フィードバックフォームです。
各テスターに、不透明なテスターコードとビルド番号を含む個別リンクを渡します。こうすればフォームは尋ねなくても誰がどのビルドかわかります。そのビルドで何をしたか、安定性と有用性の 1〜5 の評価 2 つ、そして何に妨げられたかを尋ねます。そのうえで個々の回答ではなくビルド同士を比べ、リリースごとに数字が動いたかを確認します。
ベータ版のフィードバックはどこが違うのか
公開のフィードバックは、見知らぬ人から 1 回だけ届きます。ベータ版のフィードバックは、毎週変わるソフトウェアについて、同じ少人数のグループから何度も届きます。1 件の回答より傾向のほうが重要です。同じ人たちにとって、ビルド 1.4.0 は 1.3.0 より安定していると感じられたか。それには、通常のフィードバックフォームにない 2 つの情報が必要です。誰が回答したかと、どのビルドを使っていたかです。
テスターにメールアドレスとバージョン番号を入力してもらう方法は、実際にはうまくいきません。バージョンを忘れたり、打ち間違えたり、「latest」と書いたりします。そこで両方を送るリンクに含め、ページがそれを埋めます。
- 新しいビルドをベータグループに配布します。
- スクリプトがテスターごとにコードとビルド番号入りのリンクを 1 つずつ作り、それをメールで送ります。
- テスターがリンクを開きます。ページが非表示のテスターとビルドのフィールドを埋め、どのビルドについての質問かを表示します。
- サマリースクリプトが API ですべての回答を読み込み、ビルドごとにスコアを平均します。
- 最も低いスコアの回答にあった障害が、次のビルドの修正リストになります。
フィールド
プレビュー
Ikiform のバリデーターをブラウザーで実行します。何も送信されません。
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 は {} から始めてください。
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 にはサンクスページを設定してください。スクリプトを使える静的ホストやサイトビルダーならどこでも動きます。
<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 にシークレットキーを設定して実行してください。
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 件のビルドは公平に比較できません。
動作を確認する
node links.mjs 1.4.0を実行し、リンクを 1 つ開きます。フォームに「Feedback for build 1.4.0」と表示されます。- クエリ文字列なしでページを開きます。招待のメッセージだけが表示されます。
- 回答を送信したら、ブラウザーの開発者ツールで
buildをlatestに変えて、もう一度送信します。Ikiform は「Use a version like 1.4.0」と共に422を返します。 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 でどう感じたか、なくなったらどれだけ残念か、何に妨げられたか。毎週同じ質問を続けてください。
再現が必要なものには、別のバグ報告フォームを用意してください。週次のフォームはビルド全体の使用感を聞くためのものです。両者を混ぜると、どちらも読みにくくなります。
手順で詰まったときや、あなたの状況に合うガイドがほしいときはメールでどうぞ: hello@ikiform.com