ECサイトのページトラッキング(埋め込みポイント)実装ガイド

背景

インターネットの発展に伴い、データの重要性は今や言うまでもありません。データ収集の質を高めることは、あらゆる企業にとって共通の課題です。特に、天猫(Tmall)、京东(JD.com)、Secoo(寺库)のようなEコマース企業にとって、データ分析はユーザーエクスペリエンスの向上、運営や製品戦略の調整に不可欠です。本稿では、ページトラッキング(埋め込みポイント)の基礎から、実際のツール作成までを解説します。

主な内容

  • 埋め込みポイントとは
  • 埋め込みポイントの動作原理
  • 埋め込みポイントの種類
  • ECサイトフロントエンドの埋め込みポイント標準仕様
  • 非同期リクエストのカプセル化
  • IntersectionObserver - 次世代要素観察API
  • Vueを用いたフロントエンドデータトラッキングツールのゼロからの開発
  • バックエンドログフォーマット(フロントエンドエンジニア向け概要)
  • バックエンドNginx設定(フロントエンドエンジニア向け概要)
  • 改善点
埋め込みポイント(トラッキング)とは

「埋め込みポイント」とは、データ収集、特にユーザー行動データの分野において、特定のユーザー操作やイベントを取得、処理、送信するための技術およびその実装プロセスを指します。例えば、ユーザーが特定のアイコンをクリックした回数や、動画を視聴した時間などを収集します。

動作原理とプロセス概要

Webサイト分析ツールは、ユーザーが対象サイトで行う行動(ページの閲覧、ボタンのクリック、商品のカート追加など)と、それに付随するデータ(注文金額など)を収集する必要があります。初期のトラッキングは主に「ページの表示」という単一の行動に限定されていました。しかし、Ajax技術の普及とECサイトにおける目標分析(コンバージョン分析など)の需要が高まるにつれ、従来の方法では不十分となりました。その後、Google Analyticsはカスタマイズ可能なデータ収集スクリプトを導入し、ユーザーが定義された拡張インターフェースを通じて、わずかなJavaScriptコードでイベントや指標を追跡・分析できるようにしました。現在、百度統計や搜狗分析なども同様の方式を採用しています。基本的な原理とプロセスは共通ですが、後者はJavaScriptを用いてより多くの情報を収集します。

データ収集の基本フロー

まず、ユーザーの行動(ここではページの表示とします)が発生すると、ページ内のトラッキング用JavaScriptコードが実行されます。多くのツールでは、ユーザーがページに小さなJavaScriptコードを埋め込む必要があります。このコードは通常、動的に<script>タグを作成し、そのsrc属性を別のJSファイル(例:dot.js)に設定します。これにより、ブラウザがdot.jsをリクエストして実行します。このJSが実際のデータ収集スクリプトです。データ収集後、JSはバックエンドのスクリプト(例:backend)をリクエストします。このスクリプトはGIF画像に見せかけた動的スクリプトで、PHPやPythonなどで記述されています。JSは収集したデータをHTTPパラメータとしてバックエンドに渡し、バックエンドはパラメータを解析してアクセスログに記録します。以下、Secoo商城を例に各段階を詳しく見ていきます。

1.1 トラッキングスクリプト実行段階
Vue技術スタックを採用。ページリソース読み込み完了後、トラッキングスクリプトが実行されます。

スクリプト実行

1.2 データ収集スクリプト実行段階
データ収集スクリプトdot.jsは、ページ表示時にリクエストされ、実行されます。このスクリプトは主に以下の処理を行います。

  1. ブラウザのJavaScriptオブジェクトを使って基本情報を取得:ページタイトル(document.title)、URL、ユーザーの画面解像度(window.screen)、Cookie情報(document.cookie)など。
  2. ページ上の露出(ビュー可能)領域の情報を収集。
  3. 上記のデータを結合。
  4. HTTPリクエストパラメータにデータを含めてバックエンドスクリプトをリクエスト。

ステップ4では、JavaScriptの一般的なリクエスト方法であるAjaxはクロスドメインリクエストに対応していないという問題があります。そこで、一般的な手法として、スクリプトがImageオブジェクトを作成し、そのsrc属性をバックエンドスクリプトにパラメータ付きで設定します。これにより、クロスドメインリクエストが可能になります。これが、バックエンドスクリプトがGIFファイルに見せかけられる理由です。

HTTPリクエストの様子

1.3 バックエンドスクリプト実行段階
log.gifはGIFに見せかけたスクリプトです。このバックエンドスクリプトは主に以下の処理を行います。

  1. HTTPリクエストパラメータを解析して情報を取得。
  2. サーバー(Webサーバー)からクライアントが直接取得できない情報(訪問者IPなど)を取得。
  3. 情報を所定の形式でログに記録。
  4. 1×1ピクセルの空のGIF画像をレスポンスコンテンツとして生成し、レスポンスヘッダーのContent-typeimage/gifに設定。
  5. レスポンスヘッダーのSet-Cookieで必要なCookie情報を設定。

Cookieを設定する理由は、ユニークユーザーを追跡するためです。リクエスト時にクライアントに指定されたトラッキング用Cookieがなければ、ルールに従ってグローバルにユニークなCookieを生成してユーザーに付与します。既存のCookieがあれば、それを維持するためにSet-Cookieにその値を設定します。

Cookie設定プロセス

トラッキングの種類

業界のトラッキングソリューションは主に以下の3つに分類されます。

コードトラッキング
データ収集が必要な箇所にデータ送信用のコードを直接埋め込みます。

  • 利点:データを送信するタイミングを正確に制御できる。
  • 欠点:メンテナンスコストが高く、更新のたびにトラッキングコードの修正が必要。

ビジュアルトラッキング
データアナリストやプロダクトマネージャーが管理画面上でイベントを視覚的に設定します。腾讯移动分析などが提供する方式です。

ビジュアルトラッキングインターフェース

  • 利点:開発者の支援が不要で、ビジネスチームだけで設定可能。
  • 欠点:クライアント側の行動のみサポート。

ノーコードトラッキング(無埋点)
開発者がSDKを統合するだけで、SDKがアプリ内の全ユーザー行動を自動的に取得・送信します。開発者は追加コードを記述する必要がありません。アナリストは管理画面で関心のある行動を選択し、イベント名を付与します。データ送信後にトラッキング設定が可能です。

  • 利点:開発不要、データ送信後の設定が可能。
  • 欠点:データ量が膨大、クライアント側のみサポート。

ノーコードトラッキングとビジュアルトラッキングは、いずれも開発支援不要です。しかし、データ送信とトラッキング設定の順序が異なります。ノーコードトラッキングはデータ送信後に設定が可能なため、データ量が大幅に増加します。

本稿では主にコードトラッキングを取り上げます。コードトラッキングは更に「命令型(Imperative)」と「宣言型(Declarative)」に分けられます。

命令型トラッキングは、開発者がトラッキングが必要な箇所に手動でコードを追加します。

// ページ読み込み時にトラッキングリクエストを送信
$(document).ready(function(){
   // ... ビジネスロジック
   sendRequest(params);
});
// ボタンクリック時にトラッキングリクエストを送信
$('button').click(function(){
   // ... ビジネスロジック
   sendRequest(params);
});

この方法はトラッキングコードがビジネスロジックに侵入し、コードを煩雑にし、保守性を低下させます。そこで、トラッキングコードとビジネスロジックを分離する「宣言型トラッキング」が求められます。

宣言型トラッキングは、理論的には以下の2点にのみ注目します。

  • トラッキング対象のDOMノード
  • 送信するデータ
<!-- key: 一意識別子, act: トラッキングタイプ -->
<span v-clstag-dot="{'act':'thumbs', 'key': 'details_product_1_dot_thumbs', 'productId': product.productId}">いいね</span>

宣言型トラッキングの実装原理については後述します。

ECサイトフロントエンドのトラッキング標準仕様

適切な仕様を確立することは非常に重要です。ここでは、命名規則、送信規則、データ規則、使用規則について説明します。

1. 命名規則

トラッキング名はログのキー項目となります。英数字とアンダースコアのみで構成し、アプリケーション内で一意である必要があります。

例(行動トラッキングの場合):{ページ名}_{コンポーネント名}_{コンポーネントID}_{機能}_{行動}

コンポーネント名と行動は最も重要で、バックエンドでの処理を決定します。開発前にバックエンドと厳密に合意する必要があります。例:商品リストのクリック。商品をproductと合意していれば、コンポーネント名はproduct。お気に入り追加をthumbsと合意していれば、行動はthumbsとなります。

コンポーネント名例:

  • 広告:ad
  • 商品:product
  • カート:car
  • その他:バックエンドと協議

行動例:

  • クリック:click
  • お気に入り:collection
  • コメント:comment
  • いいね:thumbs
  • カートに追加:add
  • その他:バックエンドと協議
<span v-clstag-dot="{'act':'thumbs', 'key': 'details_product_1_dot_thumbs', 'productId': product.productId}">いいね</span>

ページ起動ログやエラーログの例:{ページ名}_{ページID}_{行動}

'key': 'details_123_show'
'key': 'details_123_error'

2. 送信規則

  • 露出(ビュー)データ:以下の戦略が一般的です。
    - 時間間隔ベース:n秒ごと。
    - データ件数ベース:n件蓄積ごと。
    - リアルタイム送信:低頻度でデータ量が少なく、リアルタイム性が要求される場合。
    - データ消失防止:未送信データをlocalStorageに保存。ブラウザを閉じてもデータは保持され、次回アクセス時に送信されます。または、ネイティブアプリの場合、bridgeを使ってデータを永続化し、次回起動時に送信します。

  • イベント・エラーデータ:イベント発生後、即時に送信します。

3. データ規則
各企業は独自のデータ規則を持ち、送信すべきトラッキングデータを定義します。

データ規則例

4. 使用規則

  • トラッキングスクリプトはページリソース読み込み後に読み込みます。
import { dot } from './assets/js/dot'
// 中央イベントバスのカプセル化
Vue.use(VueBus)
Vue.config.productionTip = false
/* eslint-disable no-new */
// dot.clickExpDot(Vue) は new Vue() の前に記述
dot.clickExpDot(Vue)
window.onload = function () {
  dot.postError()
  dot.dotPageReadyData()
  dot.show()
}
new Vue({
  el: '#app',
  router,
  store,
  components: { App },
  template: '<App/>'
})
  • HTML内での宣言例:
<!-- 露出トラッキング -->
<div class="exposure-statistics" show-dot="{'act':'show','key':'details_ad_1_flowtab_show'}">

<!-- イベントトラッキング -->
<span v-clstag-dot="{'act':'thumbs', 'key': 'details_product_1_dot_thumbs', 'productId': product.productId}">いいね</span>
非同期リクエストのカプセル化

1. Axios(今回はクロスドメイン問題のため未使用)

Vueプロジェクトでは、バックエンドとの通信にAxiosライブラリが一般的に使用されます。AxiosはPromiseベースのHTTPクライアントで、リクエスト/レスポンスのインターセプト、リクエストのキャンセル、JSON変換、CSRF対策などの機能を提供します。

npm install axios
// http.js
import axios from 'axios'
import { Toast } from 'vant'

// 環境に応じたベースURL
if (process.env.NODE_ENV === 'development') {
  axios.defaults.baseURL = 'http://localhost:8080'
} else if (process.env.NODE_ENV === 'production') {
  axios.defaults.baseURL = 'http://localhost:8080'
}

axios.defaults.timeout = 10000
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'

// リクエストインターセプター
axios.interceptors.request.use(config => {
  return config
}, err => {
  return Promise.reject(err)
})

// レスポンスインターセプター
axios.interceptors.response.use(
  response => {
    if (response.status === 200) {
      return Promise.resolve(response.data)
    } else {
      return Promise.reject(response)
    }
  },
  error => {
    if (error.response.status) {
      switch (error.response.status) {
        case 500:
          Toast({ message: 'システムエラー', duration: 1000, forbidClick: true })
          break
        case 201:
          Toast({ message: '処理失敗', duration: 1000, forbidClick: true })
          break
        default:
          Toast({ message: '失敗', duration: 1500, forbidClick: true })
      }
    }
    return Promise.reject(error.response)
  }
)

export function get (url, params) {
  return new Promise((resolve, reject) => {
    axios.get(url, { params: params }).then(res => {
      resolve(res)
    }).catch(err => {
      reject(err.data)
    })
  })
}

export function post (url, params) {
  return new Promise((resolve, reject) => {
    axios.post(url, params).then(res => {
      resolve(res)
    }).catch(err => {
      reject(err.data)
    })
  })
}

2. Imageオブジェクトによるリクエスト(Image Beacon)

サーバーにログデータを送信するだけで、レスポンスが不要な場合に適しています。

export default function analytics (action = 'pageview') {
  (new Image()).src = `https://xxx/test_upload?action=${action}&timestamp=${Date.now()}`
}

この方法はクロスドメイン問題を回避でき、送信結果を気にする必要がなく、シンプルです。多くのECサイトで採用されています。

IntersectionObserver - 次世代要素観察API

ページ領域の露出を統計するには、要素がビューポート内にあるかどうかを判断する必要があり、ここでIntersectionObserverが役立ちます。

IntersectionObserverは、対象要素とその祖先要素またはビューポート(ルート)との交差状態を非同期的に監視するためのAPIです。つまり、要素が画面に表示されているかどうかを観察できます。

IntersectionObserverの概念図

API

var observer = new IntersectionObserver(callback, options)

observer.observe(document.querySelector('img'))   // 観察開始
observer.unobserve(element)    // 観察停止
observer.disconnect()          // 切断

オプション

  • root: 観察に使用するルート要素。デフォルトはブラウザのビューポート。
  • threshold: 交差比率の配列。コールバックをトリガーするタイミングを指定。デフォルトは[0]
const options = {
    root: null,
    threshold: [0, 0.5, 1]
}
var Observer = new IntersectionObserver(callback, options)
Observer.observe(document.querySelector('img'))
  • rootMargin: ビューポートのサイズを拡大・縮小するためのマージン。CSSの指定方法に従います。
const options = {
    root: document.querySelector('.box'),
    threshold: [0, 0.5, 1],
    rootMargin: '30px 100px 20px'
}

rootMarginを指定することで、実質的なビューポートを拡大できます。

コールバック

要素の可視性が変化した時にトリガーされます(要素がビューポートに入った時と出た時の2回)。

IntersectionObserverEntry

コールバックの引数entriesIntersectionObserverEntryオブジェクトの配列で、以下のプロパティを持ちます。

  • boundingClientRect: 対象要素の矩形情報
  • intersectionRatio: 交差領域と対象要素の比率。不可視時は0以下。
  • intersectionRect: 交差領域の矩形情報
  • isIntersecting: 要素が現在可視かどうか(Boolearn値)
  • rootBounds: ルート要素の矩形情報
  • target: 観察対象の要素
  • time: 交差がトリガーされた時刻のタイムスタンプ

画像の遅延読み込み(Lazy Load)の例

const io = new IntersectionObserver(callback);

let imgs = document.querySelectorAll('[data-src]');

function callback(entries){
    entries.forEach((item) => {
        if(item.isIntersecting){
            item.target.src = item.target.dataset.src;
            io.unobserve(item.target);
        }
    });
}

imgs.forEach((item)=>{
    io.observe(item);
});
Vueによるデータトラッキングツールの開発

ここまでの知識を基に、Vue CLIでプロジェクトを作成します。ディレクトリ構成は以下の通りです。

|-- build
|-- config
|-- src
|   |-- api
|   |-- components
|   |-- page
|   |   |-- dot.vue
|   |-- assets
|   |   |-- js
|   |   |   |-- dot.js          // トラッキングツール本体
|   |-- App.vue
|   |-- main.js
|-- static
|-- index.html
|-- package.json

Vueカスタムディレクティブによる宣言型トラッキング

Vueのカスタムディレクティブを利用して、宣言型トラッキングを実装します。

Vue.directive('stat', {
  bind: function () {
    // 準備
  },
  update: function (newValue, oldValue) {
    // 値の更新時に処理。初期値でも呼ばれる。
    // 値のタイプに応じてトラッキングリクエストを送信
  },
  unbind: function () {
    // クリーンアップ
  }
})

Vueアプリケーションでは、リアクティブなデータ変更によりDOM操作が行われるため、トラッキングコードはビジネスロジックから分離しやすくなります。クリック、いいね、コメント、お気に入りなどに宣言型トラッキングを適用します。

<div v-clstag-dot="{'act':'click', 'key': product.productId}"></div>

Vueカスタムディレクティブの実装コード

// dot.js 内
clickExpDot: function (Vue) {
  let that = this
  Vue.directive('clstag-dot', {
    bind: function (el, binding, vnode) {
      el.addEventListener('click', (e) => {
        e.stopPropagation()
        let time = {
          timestamp: new Date().getTime()
        }
        let query = Object.assign({}, time, binding.value, that.params)
        that.analytics(that.splicingStr(query))
      }, false)
    }
  })
}

main.jsで、トラッキング、エラーモニタリング、PV/UV送信、領域露出トラッキングを初期化します。

import { dot } from './assets/js/dot'
dot.clickExpDot(Vue)

window.onload = function () {
  dot.postError()
  dot.dotPageReadyData()
  dot.show()
}
バックエンドログフォーマット(概要)

Nginxのログフォーマット定義例

log_format
"$msec||$remote_addr||$status||$body_bytes_sent||$u_domain||$u_url|
|$u_title||$u_referrer||$u_sh||$u_sw||$u_cd||$u_lang||$http_user_agent
||$u_account";

$u_で始まる変数はカスタム変数です。

Nginx設定(概要)

log.gifはGIF画像に見せかけたバックエンドスクリプトです。OpenResty(NginxにLuaを統合したプラットフォーム)を利用した設定例を示します。

worker_processes  2;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
                      
    log_format user_log_format "$msec||$remote_addr||$status||$body_bytes_sent||$u_domain||$u_url||$u_title||$u_referrer||$u_sh||$u_sw||$u_cd||$u_lang||$http_user_agent||$u_account";

    sendfile        on;
    keepalive_timeout  65;

    server {
        listen       80;
        server_name  localhost;
        location /log.gif {
            default_type image/gif;
            access_log  logs/access.log  main;
        
            access_by_lua "
                local uid = ngx.var.cookie___utrace        
                if not uid then
                    uid = ngx.md5(ngx.now() .. ngx.var.remote_addr .. ngx.var.http_user_agent)
                end 
                ngx.header['Set-Cookie'] = {'__utrace=' .. uid .. '; path=/'}
                if ngx.var.arg_domain then
                    ngx.location.capture('/i-log?' .. ngx.var.args .. '&utrace=' .. uid)
                end 
            ";  
        
            add_header Expires "Fri, 01 Jan 1980 00:00:00 GMT";
            add_header Pragma "no-cache";
            add_header Cache-Control "no-cache, max-age=0, must-revalidate";
        
            empty_gif;
        }   
    
        location /i-log {
            internal;
        
            set_unescape_uri $u_domain $arg_domain;
            set_unescape_uri $u_url $arg_url;
            set_unescape_uri $u_title $arg_title;
            set_unescape_uri $u_referrer $arg_referrer;
            set_unescape_uri $u_sh $arg_sh;
            set_unescape_uri $u_sw $arg_sw;
            set_unescape_uri $u_cd $arg_cd;
            set_unescape_uri $u_lang $arg_lang;
            set_unescape_uri $u_account $arg_account;
        
            log_subrequest on;
            access_log logs/user_defined.log user_log_format;
        
            echo '';
        }   
    }
}

ネットワークパネル

送信パラメータ

生成されたログは、日付と.log.txt形式で保存されます。

改善点

オフライン時の遅延送信

ネットワークが不安定な場合にデータが失われる問題に対処するため、navigator.onLineでネットワーク状態を判定し、オフライン時はlocalStorageにデータを一時保存し、復旧後に送信する仕組みを実装すべきです。

より正確なPV測定: visibilitychangeイベント

現在のPV測定はページ読み込み時に依存していますが、シングルページアプリケーション(SPA)などでは不正確になる可能性があります。以下の点を考慮します。

  1. ページ読み込み時、visibilityStatevisibleならPVを送信。
  2. 読み込み時にhiddenなら、visibilitychangeイベントを監視し、visibleになったら送信。
  3. hiddenからvisibleになり、前回のユーザー操作から「十分な時間」が経過していれば、新しいPVとして送信。
  4. URLのpathnameまたはsearchが変わったら、新しいPVを送信。

これらの改善により、より正確なユーザー行動分析が可能になります。

タグ: ウェブ解析 トラッキング vue.js IntersectionObserver データ収集

5月19日 10:09 投稿