ソース解説(3) 旗を折るのではなく、読み込まない
クッキー同意のバナーは、置くこと自体は難しくありません。難しいのは、「同意していない人には何も送っていない」と自分で言い切れる形にすることでした。
今回はその部分のコードを読んでいきます。保存する形、版番号を持たせた理由、そして「読み込まない」という選び方です。
第3回。前回:目次は本文から作る 次回:速くしたら、デプロイが止まった(近日公開)
同意の記録をどう保存しているか、壊れた記録をどう扱うか、離れた部品へどうやって変化を伝えているか、そしてJavaScriptが動かない環境で何をしないことにしたかです。
止めているのは1つだけ
最初に決めたのは、何に対して同意を求めるかです。
このサイトが読み込むものを数えると、外部へ通信するのはタグマネージャー1つでした。書体は自前で配信しているので提供元への通信は起きません。紹介リンクは、押して移動したときに初めて相手へ届きます。問い合わせの送信先は、送信ボタンを押したときだけ呼ばれます。
読了状態と明暗の設定も保存していますが、こちらはサイトの中で完結していて外へ出ません。止める理由が無いので、同意の対象に入れていません。
並べるのは、実際に止まるものだけにしています。止まらないものを並べると、選ぶ手間だけが増えます。
保存する形
記録はこの形です。
export type ConsentRecord = {
analytics: boolean;
decidedAt: string;
version: number;
};
保存先はCookieではありません。サーバー側がこの値を使わないからです。Cookieはリクエストのたびに自動で送られますが、送り先に使い道がありません。ブラウザの中だけに置いています。
decidedAt を持っているのは、あとから「いつ同意したか」を示せるようにするためです。
版番号を持たせた理由
version が、この設計でいちばん考えた部分です。
const VERSION = 1;
何に同意を求めるかが変わったら、この数字を上げます。版が違う記録は「まだ答えていない」として扱い、もう一度たずねます。
同意は「そのとき説明されたものに対して」もらっています。
あとから送信先を1つ足したときに、以前の同意をそのまま使い回すと、その人が知らない相手へ黙って送り始めることになります。聞き直す手間より、そちらのほうが問題として大きい。
版番号は、その判断を忘れても実行される形にしたものです。
壊れている記録は、無かったことにする
読み出す側は、自分で書いた値でも疑っています。
const parsed: unknown = JSON.parse(raw);
if (typeof parsed !== "object" || parsed === null) return null;
const record = parsed as Partial<ConsentRecord>;
if (typeof record.analytics !== "boolean") return null;
if (record.version !== VERSION) return null;
型を unknown で受けているのが要点です。保存されている文字列が、こちらの期待する形である保証はありません。古い形式が残っていることも、別のタブで書き換わっていることもあります。
全体が try で囲んであり、保存が使えない環境では例外が出ます。そのときも null、つまり「同意していない」扱いになります。
プライベートモードなどで保存が読めないと、答えが分かりません。
分からないときに送る側へ倒すか、送らない側へ倒すか。ここでは送らない側に倒しています。判断がつかない状態を「同意あり」として扱う理由がありません。
書き込む側も同じ考え方です。保存に失敗しても、そのページのあいだは選んだとおりに動かします。記録が残らないので次に開いたときはまたたずねますが、「保存できないから同意も無かったことにする」よりは筋が通っています。
離れた部品へ、どう伝えるか
記録するのはバナー、それを見て読み込むかどうか決めるのはタグマネージャー側の部品です。2つは画面上の別の場所にあり、親子の関係もありません。
保存の仕組みには「変わったよ」と教えてくれる機能がないので、自分で合図を投げています。
window.dispatchEvent(
new CustomEvent<ConsentRecord>(CONSENT_CHANGED, { detail: record }),
);
受け取る側は、こう待っています。
useEffect(() => {
const sync = () => setGranted(readConsent()?.analytics === true);
sync();
window.addEventListener(CONSENT_CHANGED, sync);
return () => window.removeEventListener(CONSENT_CHANGED, sync);
}, []);
最初に1回読んで、そのあとは合図が来るたびに読み直します。最後の return は後片付けで、これを書かないと画面を移動するたびに待ち受けが増え続けます。
旗を折るのではなく、読み込まない
同意の実装には2通りあります。先に読み込んでおいて「送ってよいか」の旗だけ切り替えるやり方と、同意までそもそも読み込まないやり方です。
ここでは後者にしました。
if (!GTM_ID || !granted) return null;
この1行で、同意が無いあいだは何も出力されません。
先に読み込む方式では、同意していない人のブラウザにもタグの本体が届いています。何を送る・送らないの判断が相手側の設定に移るので、こちらのコードを全部読んでも「同意前は何も送っていない」とは言い切れません。
読み込まなければ、通信そのものが起きません。確かめたいことと、確かめられることが一致します。
代償として、同意しなかった人の分は数字に出ません。それは正しく「同意しなかった人がいる」という事実なので、受け入れています。
JavaScriptが動かない環境で、何をやめたか
タグマネージャーには、JavaScriptが無効な環境向けに noscript の中へ枠を1つ置く決まりがあります。それを置くのをやめました。
同意は、ブラウザに保存した記録を読んで判断しています。JavaScriptが動かない環境では、その記録を読むことも、同意をたずねることもできません。それでも枠だけは動きます。
置いたままにすると、「同意を確かめられない人にだけ、確かめずに送る」ことになります。分からないときに送らない側へ倒す、という前提をここでも通しました。
今回の学び
まず、同意は対象とセットでしか意味を持たないということ。何に対する同意かが変われば、前の同意は使えません。版番号は、その判断を人の記憶に頼らず実行させるための仕掛けでした。
次に、分からないときにどちらへ倒れるかを、先に決めておくこと。読めない・保存できない・確かめられない、という状態は必ず起きます。そのとき何が起きるかは、書いた時点で決まっています。
そして、自分のコードで説明できる形を選ぶこと。旗を切り替える方式でも、おそらく実際には送られません。ただ、それは相手の設定を信じているだけで、こちらからは確かめられない。言い切れるかどうかで選びました。
関連記事

「送っていない」と言い切るために、同意まで読み込まないことにした
止める対象の数え方、旗を立てる方式と読み込まない方式の違い、スクリプトが無い環境の扱い、そして同意の記録に版を持たせた理由です。

GTMの設定は誰でも読める
コンテナIDをURLに打ち込むと、どんなタグがどんな条件で発火するかが誰でも読める。これは不具合ではなく、そうでなければ動かない仕組み。ブラウザで動くものに秘密は持てないという前提と、そこから導かれる「書いてはいけないもの」の話。

「読み終えた」をどう数えるか
GA4にはスクロール距離の計測が最初からある。それでも使わなかった。スクロール率が測っているのは「画面が下まで移動したか」であって、「読み終えたか」ではないから。サイトにすでにある読了状態を使って、未読から読了に変わった瞬間だけを送るまでの記録。