LoginSignup
32
21

More than 1 year has passed since last update.

モダンフロント技術におけるGTMの問題

ここでいうモダンフロント技術は、VueやReactなど仮想DOMを用いられた技術を指します。

仮想DOMなので「DOMが存在しているとは限らない」

要素の表示や要素のクリックなどのトリガーに関して、class名やらid名などDOMの情報に対して条件を絞りイベントを制御すると思いますが、必ずしもGTMがその仮想DOMが正しく検知できるとは限らないです。
仮想は消失したり入れ替わったりするものなので。

そのため、GTMの既存の要素の表示や要素のクリックに依存しない設計を理想とします。

GTMのページビュートリガーが使えない

モダンフロント系の技術では、React Routerやらvue-routerなどの仕組みによって、
サーバーサイドを介さずにクライアントサイドのみでページの変更を行なっているケースが多いです。

つまり、History APIで更新しているだけなので、ページビュートリガーが機能しないです。

仮に既存のGTMのトリガーを使うとしても、初期ロード時は「ページビュー」、遷移時は「履歴の変更」が発火するため、
2つのトリガーを1つのタグに対して付与する必要があります。
※ 正しく発火する保証はしきれないかなとおもいます...重複するケースがありそうな気もしてます。

履歴の変更は、URLの一部が変更されたとき、またはpushState APIときを検知するトリガーです。

モダンフロント技術におけるGTMとの付き合い方

既存のトリガーがすこぶる使えなさそうで「じゃあどうすればいい?GTM使わなければいい?」って思った人もいると思いますが、
ここでカスタムイベント トリガーの登場です!

カスタムイベント トリガーとは?

カスタムイベントトリガーは、キー名event / 値イベント名dataLayer.pushされたものを検知するトリガーです。

下記のように設定します。

dataLayer.push({
  event: "page_view" // eventパラメータにイベント名をいれる
})

スクリーンショット 2021-12-24 4.00.32.png

▼ ドキュメント

つまり、ページビューやら要素のクリックなどをコード側で検知して、
このデータレイヤーをpushしてあげれば良いわけです!!

設計

:warning: 注意

※ Next.jsを使ってるため、React routerを使ってる人はよしなにメソッドを置き換えてください...
※ 処理をシンプルにしてます。サービスで運用しているものはもう少し複雑だったりします。特にデータレイヤーで送るものに関しては、拡張してください。

事前準備: dataLayer.pushの処理を別関数でわける

型定義したかったり、初期化処理を閉じ込めたかったりしたので、
dataLayer.pushの処理を分けて使ってます。

type PageViewEvent = {
  event: 'page_view';
  pagePath: string;
};

type InViewEvent = {
  event: 'inView';
  label: string;
};

type ClickEvent = {
  event: 'click';
  label: string;
};

export type DataLayerType =
  | PageViewEvent
  | InViewEvent
  | ClickEvent

export const pushDataLayer = (data: DataLayerType): void => {
  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push(data);
};

この処理を、'@/libs/analytics'からimportできるようにしています。

ページビュー

下記Componentをpages/_appに読み込みます。
(Componentじゃなくても、hooksでも良いと思います。)

ちょっと複雑に見えますが、やってることは至ってシンプルで、

だけです。

import { VFC, useRef, useEffect, useCallback } from 'react';
import { useRouter } from 'next/router';
import { pushDataLayer } from '@/libs/analytics';

export const TrackPageView: VFC = () => {
  const once = useRef(false);
  const { asPath, basePath, events } = useRouter();

  // page_viewイベントを送信
  // pagePathだけじゃなく、pageのタイトルとかも送っていいかもね
  // pageLocationとかの方がGAのパラメータと合ってよかったかも^^
  const pushPageView = useCallback((url) => {
    pushDataLayer({
      event: 'page_view',
      pagePath: url,
    });
  }, []);

  // 初期ロード時の処理
  useEffect(() => {
    // routeChangeCompleteが返すurlと同じ形になるよう整形
    const url = `${basePath}${asPath}`.replace(/\/$|\/\?/, (matched) =>
      matched === '/?' ? '?' : ''
    );
    !once.current && pushPageView(url);
    once.current = true;
  }, [pushPageView, prepared, basePath, asPath]);

  // URLに変更があった時の処理
  useEffect(() => {
    if (!prepared) return;
    events.on('routeChangeComplete', pushPageView);

    return () => {
      events.off('routeChangeComplete', pushPageView);
    };
  }, [events, pushPageView, prepared]);

  return null;
};

仮想ページビュー

通常の履歴の変更以外にもページビューを送りたい(仮想ページビュー)ケースもあると思います。
基本仮想ページビューを送りたいのは、要素の表示 / 非表示のケースだとは思うので、このように実装しています。

(正直、ページビューじゃなくて、別のイベントでもいいとは思いますがね...!)

import { VFC, useRef, useEffect } from 'react';
import { useRouter } from 'next/router';
import { pushDataLayer } from '@/libs/analytics';

type Props = {
  onMount?: boolean;
  onUnMount?: boolean;
  suffix: string;
};

export const TrackVirtualPageView: VFC<Props> = ({
  onMount,
  onUnMount,
  suffix,
}) => {
  const { basePath } = useRouter();

  const mount = useRef(onMount);
  const unMount = useRef(onUnMount);

  const path = useRef(`${basePath}/${suffix}`);

  useEffect(() => {
    if (!prepared) return;

    mount.current &&
      pushDataLayer({
        event: 'page_view',
        pagePath: path.current,
      });

    const unmounted = unMount.current;
    const _path = path.current;
    return () => {
      unmounted &&
        pushDataLayer({
          event: 'page_view',
          pagePath: _path,
        });
    };
  }, [prepared]);

  return null;
};

要素が表示されたタイミングで仮想ページビューを送る

<TrackVirtualPageView suffix="hoge" onMount />

要素が非表示されたタイミングで仮想ページビューを送る

<TrackVirtualPageView suffix="hoge" onUnMount />

これを、検知したいComponetのどっかに追加すればokです。

クリックの検知

めっちゃシンプルですが、クリックを検知してデータレイヤーを送るだけの処理をもったComponentを作りました。

import React, { VFC, ReactNode, useCallback } from 'react';
import { pushDataLayer } from '@/libs/analytics';

type Props = {
  children: ReactNode;
  label: string;
};

export const TrackOnClick: VFC<Props> = ({ children, label }) => {
  const handleClick = useCallback(() => {
    pushDataLayer({
      event: 'click',
      label,
    });
  }, [label]);

  return <div onClick={handleClick}>{children}</div>;
};

これを囲ってあげるだけです。

<TrackOnClick label="hoge">
  <Button />
</TrackOnClick>

要素の表示

要素の表示は、react-intersection-observerを使って検知しています。
このライブラリはとても便利で、何秒表示されたから発火する、何pxスクロールされたら発火するなどの制御ができ、
GTMの要素の表示トリガーができる設定を網羅できます。

▼ 参考

import React, { VFC, ReactNode, useEffect } from 'react';
import { pushDataLayer } from '@/libs/analytics';
import { useInView, IntersectionOptions } from 'react-intersection-observer';

/**
 * continue - イベントの発火を継続させるか(デフォルトは一回のみ)
 * options
 *   - rootMargin: 上からの距離 (中央は'50%')
 *   - delay: 何秒遅らせるか
 */

type Props = {
  label: string;
  children: ReactNode;
  continuous?: boolean;
  options?: Omit<IntersectionOptions, 'triggerOnce'>;
};

export const TrackInView: VFC<Props> = ({
  children,
  label,
  continuous = false,
  options,
}) => {
  const { ref, inView } = useInView({
    ...options,
    triggerOnce: !continuous,
  });

  useEffect(() => {
    if (!inView) return;

    pushDataLayer({
      event: 'inView',
      label,
    });
  }, [inView, label]);

  return <div ref={ref}>{children}</div>;
};

こんな感じに使います。

<TrackInView label="voice_5s" options={{ delay: 5000 }} continuous>
  <SomethingComponent />
</TrackInView>

もし、もっと良い方法があったら教えていただきたいです!!

32
21
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
32
21