ウェブサイトやアプリのフィードバックフォーム

ウェブサイトやアプリのフィードバックフォーム2 分で読めます

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

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

Preet SutharIkiform 創業者

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

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

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

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

新しいビルドから次のビルドまでの、週次のベータ版フィードバックのループビルド1.4.0をリリースベータグループへ招待メールテスターごとに1リンクテスターが回答コードとビルドは入力済みビルドごとのサマリー平均、テスター数、失望度ビルド1.5.0上位の障害を修正?t=…&build=API新しいビルドから次のビルドまでの、週次のベータ版フィードバックのループビルド 1.4.0 をリリースベータグループへ招待メールテスターごとに 1 リンクテスターが回答コードとビルドは入力済みビルドごとのサマリー平均、テスター数、失望度ビルド 1.5.0上位の障害を修正?t=…&build=API
  1. 新しいビルドをベータグループに配布します。
  2. スクリプトがテスターごとにコードとビルド番号入りのリンクを 1 つずつ作り、それをメールで送ります。
  3. テスターがリンクを開きます。ページが非表示のテスターとビルドのフィールドを埋め、どのビルドについての質問かを表示します。
  4. サマリースクリプトが API ですべての回答を読み込み、ビルドごとにスコアを平均します。
  5. 最も低いスコアの回答にあった障害が、次のビルドの修正リストになります。
行ではなく列を比べてください。1 人のテスターの調子が悪い週はノイズです。グループ全体で下がっていればリグレッションです。

フィールド

プレビュー

Ikiform のバリデーターをブラウザーで実行します。何も送信されません。

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

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

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

よくある質問

手順で詰まったときや、あなたの状況に合うガイドがほしいときはメールでどうぞ: hello@ikiform.com