🐋

WEBビーコン方式のアクセス解析ツールの仕組み解説

に公開

アクセス解析ツールとは

Webアナリティクスツールともいいますが、Webページなどのアクセス解析を行うツールのことです。
GA4やOSSだとMATOMOなど色々なツールがあります。

この記事を書く背景

後輩や同僚がよく同じサイトに入っているGA4と別のアクセス解析ツールで数値がズレてるとよく騒いでたんで
あらためてwebビーコン方式の仕組みをレクチャーしようと思ったからです。
今回紹介するのは基本的かつ原始的な方法で、基本的なJSと基本的なPythonで作成し(昔書いたコードなのでちょっと古いかも)
かつセキュリティや利便性を度外視したレクチャー目的のものになります。

タグ受けの仕組み

  1. ブラウザにアクセスすると設置したタグが起動(発火とかいう)しJavaScriptのコードをリクエスト
  2. リクエストしたJSを返却し、そのJSのコードが動くとcookieやパラメータ部分を作成する
  3. 2で作成した内容をパラメータに付与しタグ受けサーバと呼ばれるものにリクエスト(GET)されそのログが集計される。
  4. 通信としての整合性を保つために1x1の透明gifを返却する

タグとは

タグとはなにかというと、アクセス解析を行うWebサイトがあるとして
そこに埋め込む用のJavaScriptのことです。

index.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset = "UTF-8">
        <title>タグ</title>
    </head>
    <body>
        <h1>タグテスト用html</h1>
        <!--タグここから-->
        <script>
            var sNew = document.createElement("script");
            sNew.async = true;
            _id = "10001";
            sNew.src  = "http://localhost:5000/entry.js?id="+_id;
            var s0 = document.getElementsByTagName('script')[0];
            s0.parentNode.insertBefore(sNew, s0);
        </script>
     <!--タグここまで-->

    </body>
</html>

ここでやっているのは、ざっくりいうとscriptの要素を生成して、指定のURLに非同期でリクエストを送っています。
その際識別のためにIDなどをパラメータとして付与しています。図で言う1の部分。

タグ受けサーバ

タグ受けサーバーというのは、リクエストを受け付けるためのサーバー。基本的にWebサーバーとやっていることは変わらない。
今回は簡単にpythonとFlaskを使用して作ってみた。

tag_server.py
# ! /usr/bin/env python
# -*- coding :utf-8 -*-
from flask import *
import logging
import logging.handlers
import base64
import io
import datetime

# flask app
app = Flask(__name__)

# 夜中にlogが切り替わる
handler = logging.handlers.TimedRotatingFileHandler(
    filename='log/tags.log',
    when='midnight'
)
# log出力部分の設定
handler.setLevel(logging.INFO)
handler.setFormatter(logging.Formatter('[%(asctime)s] %(message)s'))
app.logger.addHandler(handler)


# entry.jsを返却するための関数
@app.route('/entry.js',methods=['GET', 'POST'])
def ajax_ddl():
    URL = 'http://localhost:5000/tag?'
    js = ""
    with open("tags.js","r",encoding="utf-8")as f:
        js_line = f.readlines()
        for i in js_line:
            js+=i.replace("¥n","")
    tmp = request.url.split("id=")
    js += 'var _imagePath = "'+URL+tmp[1]+'*" + now.getTime()+"*"+url+"*"+ref+"*"+cookie;'
    js += 'var _img = new Image(1,1);'
    js += '_img.src = _imagePath;'
    js += '_img.onload = function(){_void();}'
    resp = app.make_response(js)
    resp.mimetype = "text/JavaScript"
    return resp

# 取得したlog情報を出力するし1×1のgifを返却する関数
@app.route("/tag",methods=['GET', 'POST'])
def return_gif():
    URL = request.url.replace("%2F","/");
    app.logger.info(URL)
    gif = 'R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='
    gif_str = base64.b64decode(gif)
    return send_file(io.BytesIO(gif_str), mimetype='image/gif')


if __name__ == '__main__':
    app.run(host='localhost', port=5000, threaded=True,debug=True)

まず、ajax_ddlという関数でブラウザからのリクエストを受け取ります。
図では、2の部分にあたります。
返却するJSはあらかじめ用意されたJSにpythonの中で生成したJSを組み合わせたものを返却してます。

tag.js
var _void = function () { return; };
var now = new Date();
var url = window.location;
var ref = document.referrer;
url = encodeURI(url);
ref = encodeURI(ref);
if (ref == "") {
  ref = 'N/A'
}

if (url == "") {
  url = 'N/A'
}

function ckSave(){
    var date = new Date();
    term = parseInt(90);
    val = now.getTime()+Math.floor(Math.random()*10000);
    date.setTime(date.getTime()+term*24*60*60*1000);
    document.cookie = "_ck="+escape(val)+";expires="+date.toGMTString();

}
function ckLoad(){
    var r = document.cookie.split(';');
    //console.log(r);
    for(var i = 0; i < r.length; i++){
        content=r[i].split("=");
        if(content[0]==="_ck"){
            console.log(content);
            return content[1];
        }else{
            //return 0;
        }
    }
}
var result = document.cookie.indexOf('_ck');
if(result === -1){
    ckSave();
}
var cookie = ckLoad();
console.log(location.hostname);


このtag.jsがブラウザにcookieを付与し必要な情報をパラメータに入れ
あらためて、リクエストをタグ受けサーバに飛ばします。
図では3の部分にあたります。
最後に、return_gifという関数でlogのファイルへの書き込みと、1×1の透明gifの返却をしています。
図では4にあたります。
最後に、今回はなんらかのファイルに出力する想定にしてますが、このログを集計すると
PVやパラメータを付与してCVなどが取れるようになります。
改めてですが製品になってるものはもっと複雑になってます。

現状について

実は...

アクセス解析ツール(WEBアナリティクスツール)の有名どころでGA4などがありますが、
実は今紹介した方法が取られていたのはGA4以前のUAと呼ばれていた頃でGA4の現状では
紹介した方法は利用されていません。
まだこの方法を利用してるツールももちろんありますが、少なくともGA4では利用されていません。

UAではなぜこの方法でやっていたのか?

UA(ユニバーサルアナリティクス)で1x1のGIFを返却していた理由は、主に「データ送信のための仕組み」と「幅広い環境での計測」を実現するためです。
主に下記のような理由で行われてきました。

  • データ送信のための仕組み
    • UAでは、ユーザーの行動データをGoogleアナリティクスのサーバーに送信する際、https://www.google-analytics.com/collect などのエンドポイントにリクエストを送ります。このリクエストは、ページビューやイベントなどのパラメータを含んだHTTPリクエスト(GET)として送信されます。
  • 1x1 GIFの役割
    • サーバー側はこのリクエストに対し、実体のない透明な1x1ピクセルのGIF画像を返却します。これは、画像リクエスト(imgタグ)としてもデータ送信できるようにするための工夫です。
    • JavaScriptが動作しないメールクライアントや、外部スクリプトの実行が制限された環境でも、imgタグ経由でトラッキングリクエストを送信でき、そのレスポンスとして「画像」を返すことで、通信の整合性を保つことができます。
  • ピクセルトラッキング
    • この仕組みは「トラッキングピクセル」や「ピクセルトラッキング」と呼ばれ、メールの開封率計測などにも広く利用されてきました。

GA4ではどうなっているか?

GA4では、JavaScript(gtag.jsやGoogleタグマネージャーなど)がイベントデータを直接HTTPリクエスト(主にPOST)でGA4のエンドポイントに送信します。
イベントベースというところがUAとの違いになります。

  • 1x1の透明GIFが不要に
    • 主にデータ送信方式の進化とWeb技術の発展により、画像リクエストによるトラッキングの必要性がなくなりました。
  • クロスドメイン制約の回避が不要に
    • UA時代は、JavaScriptから別ドメインのGoogleサーバーへ直接データ送信(AjaxやXMLHttpRequest)ができなかったため、imgタグを使って1x1GIF画像をリクエストし、クエリパラメータで計測情報を送信していました。この方法は、画像リクエストはブラウザのセキュリティ制約を受けずに外部サーバーへ送信できるという特徴を活用したものです。
  • APIベースのデータ送信へ移行
    • GA4では、JavaScript(gtag.jsなど)が直接HTTPリクエスト(主にPOST)でGoogleのAPIエンドポイントにデータを送信できるようになりました。現代のブラウザはクロスドメイン通信(CORS)に対応しているため、画像リクエストを装う必要がなくなったのです。
  • より柔軟で効率的なデータ構造
    • GA4はイベントベースで計測データをJSON形式などで送信します。これにより、従来のクエリパラメータによる情報伝達よりも、複雑で柔軟なデータ構造を扱えるようになりました。

GA4リクエストイメージ図

まとめ

アクセス解析のビーコン方式について、現状と昔の比較と簡易的なコードを交えて解説させていただきました。
あくまで通信の話になるので、サイト環境や通信状況によっては、ズレるのも当たり前だなと思っていただければ幸いです。
もちろん数十万PVのサイトで数万PVズレているなどは問題だと思うので、そうなるとなんらかのリファクタリングは必要ですが、母数に対して少ない割合であれば無視しても分析にはそこまで影響しないと思っております。
あくまでこれらのツールは傾向を分析するものであり、細かい数値を比較するものではないとをお伝えしたかった次第です。

Discussion