はじめに
GDPR や改正個人情報保護法の流れを受けて、Webサービスでは「Cookie同意バナー」の設置が一般的になりました。しかし、同意レベルに応じてSentryやGoogle Analytics 4(GA4)の読み込みを実際に制御する実装まで踏み込んだ記事は意外と少ないように思います。
本記事では、Next.js 15(App Router)で運用中のプロダクト Bitcoin Simulator での実装を基に、Cookie同意の結果に連動して Sentry を無効化し、GA4 スクリプトの注入自体を抑止する方法を解説します。
前提環境
- Next.js 15 (App Router)
-
@sentry/nextjsv10 - GA4(gtag.js)
- React 19
全体アーキテクチャ
┌─────────────────────────────────────────┐
│ layout.tsx │
│ ┌──────────────┐ ┌─────────────────┐ │
│ │ CookieConsent │ │ GoogleAnalytics │ │
│ │ (バナーUI) │ │ (条件付きロード) │ │
│ └──────┬───────┘ └────────┬────────┘ │
│ │ CustomEvent │ │
│ │ 'cookie-consent │ │
│ │ -changed' │ │
│ ▼ ▼ │
│ localStorage useEffect │
│ + Cookie mirror listener │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ instrumentation-client.ts │
│ ┌─────────────────────────────────┐ │
│ │ Sentry.init({ enabled: ... }) │ │
│ │ ← document.cookie 参照 │ │
│ │ + consent-changed listener │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
ポイントは3つです:
-
Cookie同意バナーが
localStorage+Cookieの2箇所に同意レベルを保存 -
Sentry は
instrumentation-client.tsで Cookie を直接参照してenabledを切り替え -
GA4 は React コンポーネント内で
getConsentLevel()を呼び、'all'の場合のみ<script>タグを動的注入
Step 1: Cookie同意バナーの実装
まず、同意レベルを管理するバナーコンポーネントを作ります。
// src/app/_components/cookie-consent.tsx
'use client';
import { useState, useEffect } from 'react';
const COOKIE_CONSENT_KEY = 'cookie_consent';
/**
* 現在の同意レベルを返す
* 'all' — すべて許可(Sentry + GA4 有効)
* 'essential' — 必須のみ(Sentry + GA4 無効)
* null — 未選択
*/
export function getConsentLevel(): 'all' | 'essential' | null {
if (typeof window === 'undefined') return null;
try {
const val = localStorage.getItem(COOKIE_CONSENT_KEY);
if (val === 'all' || val === 'essential') return val;
if (val === '1') {
localStorage.setItem(COOKIE_CONSENT_KEY, 'all');
return 'all';
}
} catch {
// プライベートブラウジング等で localStorage 使用不可
}
return null;
}
export default function CookieConsent() {
const [visible, setVisible] = useState(false);
useEffect(() => {
if (!getConsentLevel()) setVisible(true);
}, []);
const accept = (level: 'all' | 'essential') => {
try {
localStorage.setItem(COOKIE_CONSENT_KEY, level);
} catch { /* storage unavailable */ }
// Cookie にもミラー(instrumentation から参照するため)
document.cookie =
`cookie_consent=${level}; path=/; max-age=31536000; SameSite=Lax; Secure`;
// 同一タブ内の他コンポーネントに通知
window.dispatchEvent(new Event('cookie-consent-changed'));
setVisible(false);
};
if (!visible) return null;
return (
<div role="dialog" aria-label="Cookie consent">
<p>当サイトではCookieを使用しています。</p>
<button onClick={() => accept('all')}>すべて許可</button>
<button onClick={() => accept('essential')}>必須のみ</button>
</div>
);
}
設計上のポイント
| 設計判断 | 理由 |
|---|---|
| localStorage + Cookie の二重保存 | localStorage はクライアント側で高速に参照でき、Cookie は instrumentation-client.ts(React 外)から document.cookie で参照可能 |
| CustomEvent でブロードキャスト | 同一タブ内で GA4 コンポーネントや Sentry リスナーに即座に伝播できる |
| レガシー移行 |
'1' → 'all' の自動変換で、既存ユーザーの再同意を不要に |
| try/catch ガード | Safari プライベートブラウジング等で localStorage が使えないケースに対応 |
Step 2: Sentry の条件分岐
Next.js 15 では、クライアントサイドの Sentry 初期化は instrumentation-client.ts で行います。このファイルは React コンポーネントの外で実行されるため、localStorage ではなく document.cookie を直接パースします。
// src/instrumentation-client.ts
import * as Sentry from '@sentry/nextjs';
// Cookie から同意レベルを取得
const consentCookie = typeof document !== 'undefined'
? document.cookie
.split('; ')
.find(c => c.startsWith('cookie_consent='))
?.split('=')[1]
: undefined;
// production かつ「必須のみ」でない場合のみ有効化
const sentryEnabled =
process.env.NODE_ENV === 'production' &&
consentCookie !== 'essential';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
enabled: sentryEnabled,
tracesSampleRate: 0.1,
replaysOnErrorSampleRate: 0,
replaysSessionSampleRate: 0,
});
// セッション途中で同意が変更された場合にも対応
if (typeof window !== 'undefined') {
window.addEventListener('cookie-consent-changed', () => {
const updated = document.cookie
.split('; ')
.find(c => c.startsWith('cookie_consent='))
?.split('=')[1];
if (updated === 'essential') {
const client = Sentry.getClient();
if (client) {
client.getOptions().enabled = false;
}
}
});
}
なぜ Cookie を使うのか
instrumentation-client.ts は Next.js のブートストラップ時に実行されます。この時点では React の Context もカスタム Hook も使えません。document.cookie は同期的にアクセスできるため、初期化のタイミングで確実に同意状態を参照できます。
セッション中の同意変更
ユーザーがページ遷移せずに同意バナーで「必須のみ」を選んだ場合も、cookie-consent-changed イベントを購読して client.getOptions().enabled = false でランタイムに無効化します。
注意: サーバーサイド(
instrumentation.ts)の Sentry はユーザーの Cookie 同意とは無関係に動作します。サーバーエラーの捕捉はサービス運用上必要であり、ユーザーの個人データとは分離されているためです。
Step 3: GA4 の条件付きロード
GA4 は Sentry と異なり、スクリプトタグの注入自体を抑止するアプローチを取ります。
// src/app/_components/google-analytics.tsx
'use client';
import { useEffect, useRef } from 'react';
import { getConsentLevel } from './cookie-consent';
const GA_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID;
export default function GoogleAnalytics() {
const loaded = useRef(false);
useEffect(() => {
if (!GA_ID) return;
const load = () => {
if (loaded.current) return; // 二重ロード防止
if (getConsentLevel() !== 'all') return; // 同意なし → 何もしない
loaded.current = true;
// gtag.js を動的注入
const script = document.createElement('script');
script.src =
`https://www.googletagmanager.com/gtag/js?id=${GA_ID}`;
script.async = true;
document.head.appendChild(script);
// dataLayer 初期化
window.dataLayer = window.dataLayer || [];
function gtag(...args: unknown[]) {
window.dataLayer.push(args);
}
gtag('js', new Date());
gtag('config', GA_ID, {
anonymize_ip: true,
cookie_flags: 'SameSite=Lax;Secure',
});
};
load();
// 同意変更を購読
window.addEventListener('cookie-consent-changed', load);
return () =>
window.removeEventListener('cookie-consent-changed', load);
}, []);
return null;
}
Sentry との設計差異
| Sentry | GA4 | |
|---|---|---|
| 制御方式 |
enabled フラグ切り替え |
<script> 注入自体を制御 |
| 初期化場所 | instrumentation-client.ts |
React コンポーネント |
| 同意参照 |
document.cookie 直接パース |
getConsentLevel()(localStorage) |
| 一度許可→取消 | ランタイムで無効化 | リロードまで残る(ref ガード) |
GA4 は一度 <script> を注入すると完全に除去するのが難しいため、最初から注入しない設計が重要です。
Step 4: Root Layout での組み込み
// src/app/layout.tsx
import CookieConsent from '@/app/_components/cookie-consent';
import GoogleAnalytics from '@/app/_components/google-analytics';
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<body>
{children}
<CookieConsent />
<GoogleAnalytics />
</body>
</html>
);
}
GoogleAnalytics は return null なので DOM には何もレンダリングされません。Cookie同意が「すべて許可」になるまで、gtag.js のネットワークリクエスト自体が発生しないことを DevTools の Network タブで確認できます。
テスト観点
実装後に確認すべきポイント:
- 初回訪問時: バナー表示中は Sentry のイベント送信なし、GA4 のスクリプト読み込みなし
- 「すべて許可」選択後: Sentry 有効化、GA4 スクリプト注入(Network タブで確認)
- 「必須のみ」選択後: Sentry 無効、GA4 未ロードのまま
- ページリロード: Cookie/localStorage の値が維持され、選択が永続化
-
プライベートブラウジング:
try/catchガードにより localStorage 例外でクラッシュしない
まとめ
Cookie同意と監視ツールの連動は、法令対応だけでなくユーザーの信頼を獲得する上でも重要です。本記事のポイントを整理します:
- 2層保存(localStorage + Cookie)で、React コンテキスト内外の両方から同意状態を参照可能にする
-
Sentry は
instrumentation-client.tsでenabledフラグを制御し、セッション中の変更にもCustomEventで対応 -
GA4 はスクリプトの注入自体を抑止する設計とし、
useRefで二重ロードを防止 -
CustomEvent
'cookie-consent-changed'による疎結合な通知パターンで、バナー・Sentry・GA4 を独立して管理
この設計は Next.js 15 の instrumentation-client.ts の仕組みを活かしつつ、GDPR や日本の個人情報保護法が求める「同意前のトラッキング禁止」を技術的に担保するものです。同様の要件を持つプロジェクトの参考になれば幸いです。
実際に稼働中のプロダクトはこちら: Bitcoin Simulator (bitcoin.ne.jp)