1
1

Next.js に GA4 + Cookie 同意バナーを埋め込む

Posted at

はじめに

GA4 を利用するため、Cookie Consent Banner を自前で埋め込んだ。本来?、Cookie を利用する場合、自前での対応はほぼ不可能であるため、Consent Management Platform(CMP)を利用すべきであるが、それだと JavaScript 埋め込むだけなので、今回は自前での実装を行う。

Screenshot 2024-07-03 at 13.49.13.png

CMP を利用するか否かは、

”訴えられない”ように Google Analytics を使う方法を考える

を参考に、慎重に検討する必要がある。

手順

Cookie Notice 等は既に用意されているものとする。

  1. GA4 のスクリプトを default では送信しないように埋め込む
  2. Cookie Conset Banner を埋め込み、同意状態を LocalStorage に保存する
  3. 同意が取れたら送信する

今回は、安全第一で、同意が取れなければ送らないという方針にしているが、対応する国やエリアによっては、拒否されるまでは取って良かったりなどあるので、どこ向けかで方針は変わる。いずれにしても、Cookie 同意の対応は、今回紹介する Banner 埋め込みが2割、ポリシーが8割(こっちが無理)という感じだ。

Cookie 利用に同意するユーザは 3 割程度らしいので、なんとかデータをたくさん取りたいマーケター的には、拒否されるまで取って良いところでは、取って欲しいと言うに違いない。

GA4 のスクリプトを埋め込む

Next.js には、3rd Party 用のライブラリがあり、ただ埋め込むだけなら簡単だ。

import { GoogleAnalytics } from '@next/third-parties/google'

export default function Page() {
  return <GoogleAnalytics gaId="G-XYZ" />
}

@next/third-paties では、Google Tag Manager, Google Analytics, Google Maps, YouTube
などが上記のようなお手軽さで利用できる。ただし、このライブラリは現在 Experimental であり、今回やりたいことはできないので、残念ながら使えない。今後に期待。詳細は、こちら を参照。

埋め込むべきタグは、基本のタグ、

<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-S84EEZJCTF"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'G-XXXX');
</script>

これに、同意ステータスのデフォルトを追加したものになる。

gtag('consent', 'default', {
  'ad_storage': 'denied',
  'ad_user_data': 'denied',
  'ad_personalization': 'denied',
  'analytics_storage': 'denied'
});

region オプションなどもあるので、エリアごとのデフォルト値が設定可能だ。詳細は、同意設定を管理する(ウェブ) を参照。できることは日々変わっているので、定期的に確認を。

Next.js では、サードパーティのスクリプトをロードしたり、インラインスクリプトを読み込むには、next/script を使う。strategy オプションで、ロードのタイミングを調整できる。詳細は以下を参照。

以上を踏まえ、GA4 コンポーネントは以下のようになる

src/app/components/GA4.tsx
'use client';

import Script from 'next/script';

type Props = {
  gaId: string;
};

export default function GA4({ gaId }: Props) {
  return (
    <>
      <Script
        strategy="afterInteractive"
        src={`https://www.googletagmanager.com/gtag/js?id=${gaId}`}
      />
      <Script
        id="google-analytics"
        strategy="afterInteractive"
        dangerouslySetInnerHTML={{
          __html: `
                window.dataLayer = window.dataLayer || [];
                function gtag(){dataLayer.push(arguments);}
                gtag('js', new Date());

                gtag('consent', 'default', {
                    'ad_storage': 'denied',
                    'ad_user_data': 'denied',
                    'ad_personalization': 'denied',
                    'analytics_storage': 'denied'
                });
                gtag('config', '${gaId}', {
                    page_path: window.location.pathname,
                })
                `,
        }}
      />
    </>
  );
}

これを、今回は全てのページに埋め込みたいので、RootLayout に埋め込む。読み込みは、strategy オプションで調整されるので、どこに配置するかは気にしなくて良い。afterInteractive (default) は、Next.js のコードの邪魔をしない。

Cookie Consent Banner を埋め込む

まず、同意状態を LocalStorage に保存するので、get, set, remove の helper を作っておく。

src/app/lib/storageHelper.ts
import 'client-only';

export function getLocalStorage(key: string, defaultValud: any) {
  const stickyValue = localStorage.getItem(key);

  return stickyValue !== null && stickyValue !== 'undefined'
    ? JSON.parse(stickyValue)
    : defaultValud;
}

export function setLocalStorage(key: string, value: any) {
  localStorage.setItem(key, JSON.stringify(value));
}

export function removeLocalStorage(key: string) {
  localStorage.removeItem(key);
}

client-only パッケージはインストールが必要。

CookieConsentBanner コンポーネントのスケルトンは、以下のようになる

src/app/components/CookieConsentBanner.tsx
'use client'

export default function CookieConsentBanner() {
  // ユーザの同意状態
  const [consentMode, setConsentMode] = useState<ConsentModeType | null>(null);

  // LocalStorage から同意状態をロード
  useEffect(() => {...}, [setConsentMode]);
  // 同意状態が変わったら、update イベントを送信
  useEffect(() => {...}, [ConsentMode]);
  // ボタンが押されたら、LocalStorage, State を更新
  const setCookieConsent = (value: boolean) => {...}

  return <div>...</div>

ユーザの同意状態

  // ユーザの同意状態
  const [consentMode, setConsentMode] = useState<ConsentModeType | null>(null);
type ConsentModeType = {
  ad_storage: 'granted' | 'denied' | undefined;
  ad_user_data: 'granted' | 'denied' | undefined;
  ad_personalization: 'granted' | 'denied' | undefined;
  analytics_storage: 'granted' | 'denied' | undefined;
};

同意も拒否もしていない状態を null とする。

表示部分

  return (
    <div
      className={clsx(styles.cookieConsentBanner, {
        [styles.cookieConsentBannerHidden]: consentMode !== null,
      })}
    >
      <p>
        当社サイトでは、Cookieを使用しています。各規約をご確認の上ご利用ください: <br />
        <Link href="/cookies">Cookie Policy</Link>,
        <Link href="/privacy">Privacy Policy</Link> および
        <Link href="/terms">Terms of Use</Link>
      </p>
      <div className={styles.buttons}>
        <button
          className={styles.cookieDecline}
          onClick={() => setCookieConsent(false)}
        >
          拒否する
        </button>
        <button
          className={styles.cookieAccepted}
          onClick={() => setCookieConsent(true)}
        >
          Cookie を受け入れる
        </button>
      </div>
    </div>
  )
}

選択同意は今回はなしにした。必要な場合は、選択させるバルーンなどを設置したりなどが必要。同意または拒否済みの場合は、表示しないように style を調整した。

初期状態のロード

 // LocalStorage から同意状態をロード
 useEffect(() => {
   const storedConsentMode = getLocalStorage('consentMode', null);
   setConsentMode(storedConsentMode);
 }, [setConsentMode]);

ここでは LocalStorage から読み込んだものを State に入れているだけだが、ポリシーを更新した場合や、同意から一定期間経過した場合や、LocalStorage に保存されている内容が不適切な場合などに、同意を取り直すようにしたい場合は、その内容をここに書く。同意したポリシーのバージョンや、同意日時などは、LocalStorage に一緒に入れておけば良い。

LocalStorage/State の更新

  // ボタンが押されたら、LocalStorage, State を更新
  const setCookieConsent = (value: boolean) => {
    const newValue = value === true ? 'granted' : 'denied';
    const newConsentMode: ConsentModeType = {
      ad_storage: newValue,
      ad_user_data: newValue,
      ad_personalization: newValue,
      analytics_storage: newValue,
    };
    setLocalStorage('consentMode', newConsentMode);
    setConsentMode(newConsentMode);
  }

前述の通り、選択同意はしないので、全同意か全拒否のどちらかになっている。だったら、LocalStorage にも true/false を入れておけば良いのでは?と思うが、その通りである。その通りであるが、将来的に選択同意のロジックを入れるかも知れないし、ポリシー同意のバージョン、日時も入れておきたいので、このようにしてある。

update イベントを送信

  // 同意状態が変わったら、update イベントを送信
  useEffect(() => {
    if (consentMode !== null) {
      window.gtag('consent', 'update', {
        ad_storage: consentMode.ad_storage,
        ad_user_data: consentMode.ad_user_data,
        ad_personalization: consentMode.ad_personalization,
        analytics_storage: consentMode.analytics_storage,
      });
    }
  }, [ConsentMode]);

これで、同意していればデータ取得が開始し、拒否していればデータ取得は開始しない。同意も拒否もしていなければ、呼び出されない(ので取得されない)。

RootLayout に埋め込む。

まとめ

今回追加・修正したファイルは以下の通り。

src/app/
 ├── lib
 │   └── storageHelper.ts
 ├── components
 │   ├── CookieConsentBanner.tsx
 │   └── GA4.tsx
 └── layout.tsx

実際にこの方法で埋め込んだ Lang x Lang では、

  • 同意から一定期間が経過
  • ポリシーの変更があった
  • LocalStorage に保存された内容が不正

な場合に、同意を取り直すようにしている(ここで、removeLocalStorage を使う)。ただし、近い将来 CMP を利用する予定なので、これは一時的な対応だ。実際の動作は Lang x Lang で確認できる。Local Storage の中身を確認してみると良い。

繰り返しになるが、Cookie Consent Management は自前での対応はほぼ不可能なので、今回紹介した方法は、真っ黒をグレーに変える程度の対応である。

なお、実際にこれで運用してみて、データはきちんと取れてはいるが、同意率3割程度というのは、まぁだいたいそんなものかなという感じだ。うざすぎるバナーで離脱説も納得だが、ユーザにとって Essential Cookie 以外、受け入れるメリットがない。ユーザはちゃんと考えてるということですね。

(おしまい)

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1