HTMLをリンクで共有するPlopinoを開発しています。アクセス解析を実装する際、サイトの画面と、ユーザーがアップロードしたHTMLを同じ方法では測れませんでした。今回は現在の実装と、その割り切りを記録します。
計測する場所を二つに分けた
製品のページにはUmamiのブラウザースクリプトを置きます。一方、配信するユーザーのHTMLにはそのスクリプトを挿入していません。こちらはサーバー側からboard-viewというイベントを送ります。
サーバーから同じ閲覧についてpageviewも送る構成にはしていません。また、レポートでpageviewとboard-viewを足して「ユニーク訪問者」とは呼びません。計測対象も欠損の条件も違うためです。
静的ファイルの配信側では最終的な応答ステータスを見ます。2xxと304は対象になり得ますが、HEAD、ディレクトリのリダイレクト、404などは対象外です。さらに、ページ以外のリソースはイベント用コールバックで除外しています。CSSや画像のリクエスト数を閲覧数にしないための境界です。
サーバーで数えるとリンクプレビューも入る
GETが来たことと、人が画面を読んだことは一致しません。現状はUser-Agentの既知の特徴を見て、クローラー、リンクプレビュー、一部の診断ツール、空のUAを除外します。Globalpingのプローブも明示的に扱っています。
ただし、これは人間判定ではありません。ブラウザーを名乗る自動アクセスは残り得ますし、広すぎるパターンは正当なアクセスを落とします。クライアント側でスクリプトが動いた場合も、人間だと断定できません。数字を読む際は、公開やリンクコピーなどの行動と分けて確認します。
アップロード結果を解析サーバーの状態に依存させない
uploadとboard-viewの呼び出し側は、イベント送信の完了をawaitしません。ただし、voidを付けるだけでPromiseの拒否が処理されるわけではありません。送信関数の中で想定する失敗を処理します。
以下は実装のパターンを抜き出した例です。payloadの組み立て、UAフィルター、ログ出力は省略しています。
async function sendEvent(endpoint, payload, fetchImpl = fetch) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetchImpl(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: controller.signal,
});
return response.ok;
} catch {
return false;
} finally {
clearTimeout(timer);
}
}
製品の実装ではHTTPの非OK応答と例外に対して警告を記録します。タイムアウトの標準値は5秒です。finallyでタイマーを解放するので、正常終了した送信にタイマーを残しません。
代わりに、イベント配送の保証はありません。永続キューはなく、プロセス終了などで失われる可能性があります。課金の根拠や監査記録として使うなら、この設計のままでは不十分です。
ネットワークを使わずに確かめたこと
送信関数にはfetchを注入できるようにしています。既存のモジュールテスト9件を再実行し、すべて成功しました。実際の解析サーバーへは送信していません。
| 観点 | 確認する内容 |
|---|---|
| UA判定 | ブラウザー、既知のボット、空の値 |
| 無設定 | 送信を行わない |
| リクエスト | payload、ヘッダー、追加データ、URL末尾の正規化 |
| 失敗 | 非OK応答、ネットワーク例外を呼び出し側へ投げない |
| タイムアウト | 中断を扱いfalseで終了する |
| サイズ | URLやタイトルを上限で切る |
タイムアウトのテストには短い待ち時間を注入しています。このテストで分かるのはコードの振る舞いであり、本番の配送率やUA判定の精度ではありません。
今の判断
Plopinoでは、まず本来のアップロードと閲覧を成立させ、解析は失敗し得る補助情報として扱っています。uploadイベントの件数を新規利用者数に読み替えたり、同じ操作に対応するクライアントとサーバーのイベントを足したりしないことも、実装と同じくらい大事でした。
小さなサービスでも「何を数えるか」「失敗時に何を止めるか」を先に決めておくと、グラフの意味を説明しやすくなります。