Google Analyticsの先へ、アクセス解析ツールを独自実装してユーザーの理解を深める


はじめまして、この記事がQiita初投稿になります @kojiiです。以後お見知りおきを。
株式会社ベーシックでWebマーケティング関連プロダクトの開発を統括していまして[Homeup!](https://homeup.me)や[ferret](https://ferret-plus.com)といったサービスを立ち上げから見ています。
ふだんはマーケティング・テクノロジーにフォーカスした技術トレンドを追っているのですが、本記事ではアクセス解析やユーザートラッキングといったWebマーケティングの入口となる部分の技術的な側面をお伝えできればと思います。


## 目次

1. アクセス解析ツールを独自実装して得られること
2. クライアントサイドの実装をするうえで心がけること
3. ファーストパーティーCookieとサードパーティCookie
4. UIDとSIDの付与の仕方（セッションの概念）
5. ドメインをまたいだ間接計測の実装方法
6. 低コストな集計を実現可能にするミドルウェア達
7. まとめ

## アクセス解析ツールを独自実装して得られること
アクセス解析では無料で使えて誰もが知ってる Google Analytics（GA）や、Mixpanelといった高機能なツールが多数存在していて、これらのツールを使用して高度な解析結果が得られます。
それでも、独自実装が必要な場合は以下のようなことが考えられるのではないでしょうか。

* GAが高機能すぎて使いこなせない／本当に必要な計測結果だけシンプルに知りたい
* サイトに来訪したユーザーの行動履歴を追いたい
* GAでは得られないサイト内行動を集計したい
* 高度なABテストなどマーケティング施策と連動させたい
* マーケティングツールを提供しようとしていて、それにバンドルする解析ツールが必要

これらの事はもちろん独自で実装すれば叶うわけですが、それ以上に関連技術やノウハウがナレッジとして蓄積しマーケティング施策の幅が格段に広がると言うことと、何よりもアクセスログをしっかりと追うことでエンドユーザーをより深く知ることができるようになると言うことが重要です。

**マーケティングの基本は相手を知ること**です。

自分達で集計を行う場合、一行のログも無駄にはできません。これはGAを使っていては感じとることが難しいと思うのですが、実装している段階で一つ一つのログと真摯に向き合う必要がでてくるので、そのログの裏側にあるユーザー心理なども理解しやすくなり、マーケティングのセンスが磨かれるといった二次効果が大きいと個人的には思っています。

なお、独自実装したとしても業界標準はGAです。なので、GAによる計測も行い解析ツールの精度をGAに合せられるようにブラッシュアップしていくことをお勧めします。


## クライアントサイドの実装をするうえで心がけること
自社のサイトやサービスでのみ使用する解析ツールやマーケティングツールを実装しようとしているのであれば何も問題ありません。この章は読み飛ばしてください。

もしあなたが、解析ツールや、マーケティングツール、ソーシャルプラグインなど実装したコードを他のサイトに埋め込んでもらう必要がある場合、ページの描画に影響がでないよう、注意が必要です。

### お行儀よくしましょう
そこは他人の庭です。
後述するようにグローバルスコープを汚したり、動作が遅くなるような原因となるコードを書いてはいけません。まあ、当たり前の嗜みですね。

### グローバルスコープを汚さない
グローバルスコープに定義する変数は一つだけにして、その変数にプロパティやファンクションを定義して使用するようにしましょう。

~~~~javascript
(function(w) {
  w.sample = {
    hoge: "hoge";
    fuga: "fuga;
  };
  var hoge = "hoge";
  var fuga = fuction() {
    return "fuga";
  }
}).call(this);
console.log(hoge); // => undefined
console.log(sample.hoge); // => hoge
console.log(sample.fuga()); // => fuga
~~~~

### jQuery などのライブラリは使用しない
すでに、違うバージョンのライブラリをそのサイトで読み込んでいるかもしれません。jQueryなどを使いたくなるところですが、ここはプレーンなJavascriptで実装するようにしましょう。
また、スクリプトを読み込むときは `aync` 属性を付けて描画をブロックしないようにしましょう。

## ファースパーティーCookieとサードパーティーCookie
アクセス解析を行うときにはユーザー識別IDをCookieに登録するのが一般的です。
Cookie には、ファーストパーティーとサードパーティの2種類があり、最近ではiOSのSafariがサードパーティCookieをデフォルトでブロックしていると言うことから、ファーストパーティーCookieを使用することが一般的になって着ています。GAでもファーストパーティーCookieを使用しています。

両者の違いは、アクセスしているページのドメインから発行されているCookieかそうでないかの違いになります。
アクセスしているページのドメインから発行されているものがファーストパーテイーCookieで、そうでないものがサードパーティーCookieになります。
以下のスクリーンショットは、contents.ferret-plus.comにアクセスした際のCookie情報ですが、GAが発行している`__utma`という名前のCookieがユーザー識別のためのIDで、Domainの値が`.ferret-plus.com`になっていることからファーストパーティであるとがわかります。

![Developer_Tools_-_https___contents_ferret-plus_com_.png](https://qiita-image-store.s3.amazonaws.com/0/789/22245b06-73d6-38ba-33a7-758f5c0ccf54.png "Developer_Tools_-_https___contents_ferret-plus_com_.png")

上記のスクリーンショットで四角で囲われいる部分はDomainの値が`.google.com`になっていてサードパーティCookieなので、iOSのSafariなどではこれらの情報は google.comへは送信されません。

では、どのようにして、Cookieに保存された `__utma`の値を、Googleのサーバーへ送信しているのかというと、`https://www.google-analytics.com/r/collect`で取得できるビーコン用のGif画像を取得する時にパラメータとして渡しています。

<img width="1318" alt="Developer_Tools_-_https___contents_ferret-plus_com__と_HTTPSを使っているのにCookieに「secure」属性を設定していない危険なサイトが話題に___スラド_セキュリティ.png" src="https://qiita-image-store.s3.amazonaws.com/0/789/32ab515e-ca44-aa7d-d682-a011ca3290a0.png">

これらの処理はJavascriptで行います。

~~~javascript
var cookies = {};
document.cookie.split(";").forEach(function(c) {
  kv = c.trim().split("=");
  coookies[kv[0]] = kv[1];
});

var encodeParam = function(str) {
  try {
    return encodeURIComponent(str);
  } catch {
    return "";
  }
}

var hash = {
  uid: cookies.uid,
  title: document.title,
  referrer: document.referrer,
  ...
}

var params = [];
for (key in hash) {
  params.push("key" + encodeParam(hash[key]));
}

img = document.createElement("img");
img.src = "https://yourdomain.com/beacon.gif?" + params.join("&");
img.type = "image/gif";
img.async = true
img.style.display = "none";
document.body.appendChild(img);
~~~


## UIDとSID(セッションの概要)
Googleが `__utma`で設定しているように、独自で解析ツールを作成する場合もユーザー識別IDをCookieに設定する必要があります。それ以外にも、セッション単位出の行動単位で見るために、セッションIDも設定しておくことが効果的です。
以降、ユーザー識別IDをUID、セッションIDをSIDとします。

### UID
UIDはCookieのexpier属性(有効期限)を一年から二年とした長期間有効なCookieに設定することでユーザーを一意に識別します。
一意とはいえCookieに保存する情報なので、ブラウザがPCのchromeからAndroidのスマホに変わったら別のユーザーとして認識されてしまいます。

### SID
GAで定義するところのセッションとは、ざっくり表わすと以下とおり定義されています。
https://support.google.com/analytics/answer/2731565?hl=ja
特定UIDで日付の変わり目や、30分以上間隔の空いたアクセスがあった場合に新たにSIDを発行するようにしましょう。

* 時間単位の終了（1 日の終わりなど）:
  * 操作が行われない状態で 30 分経過後
  * 午前 0 時
* キャンペーンの切り替わり:
  * キャンペーン経由でサイトにアクセスして離脱した後、別のキャンペーン経由でサイトに戻ってきた場合。

## ドメインをまたいだ間接計測の実装方法
サイトを運営しているとドメインをまたいで画面遷移して、時には他ドメインがコンバージョンページだったりするときがあります。
例えばメディアを運営していて、記事広告を販売してその記事を閲覧した人が広告主のページでどれだけコンバージョンしたのかということをレポートするために必要になったりします。
ファーストパーティCookieでユーザートラッキングをしている場合、iframeを利用することで間接計測を実施することが可能です。

間接計測を実装する場合、シンプルに次のことだけ実現することができれば良いです。
**本サイト（のドメイン）で設定されたUIDと同じUIDが他サイト（のドメイン）でも利用されている状態。**

以下に例を示します。
other-site.com が外部サイト
main-site.com がメインのサイトで
main-site.com で取得できている UID, SID を other-site.com から送信できれば OK です。

~~~html:other-site.html
<html>
  <head>
    <script src="http://main-site.com/crossdomain.js">
  </head>
  <body>
    〜
    <iframe src="http://main-site.com/embbed.html">
  </body>
</html>
~~~

~~~html:embbed.html
<html>
  <head>
    <script>
      now = new Date().getTime();
      sid = getSid(now);
      uid = getUid(now);
      data = JSON.stringify({uid: uid, sid: sid})
      window.parent.postMessage(data, "*")
    </script>
  </head>
</html>
~~~


~~~javascript:crossdomain.js
window.addEventListener("message", function(event){
  if(event.origin.indexOf("main-site.com") > -1){
    if(typeof(event.data) == "string"){
      var data = JSON.parse(event.data)
      var sid = data.sid;
      var uid = data.uid;
      sendTracking(uid, sid);
    }
  }
}, false);

function sendTracking(uid, sid) {
  var hash = {
    url: document.URL,
    ref: document.referrer,
    pt: document.title,
    res: screen.width + "x" + screen.height,
    uid: uid,
    sid: sid
  }

  params = []

  for (key in hash) {
    val = encodeURIComponent(hash[key])
    params.push(key + '=' + val)
  }

  var src = "//main-site.com/beacon.gif?" + (params.join("&"));
  var mk = document.createElement("img");
  mk.src = src;
  document.getElementsByTagName('head')[0].appendChild(mk);
}
~~~

## 低コストな集計を実現可能にするミドルウェア達
最後に集計ですね。

クライアントサイドではいろいろ制約がありましたが、サーバーサイドはどのように実装しても問題ありません。
いいですね自由って。

そんななかで、私がROIが良いと思っているアーキテクチャを簡単に説明します。

簡単な流れは以下の通りです。

~~~
nginxでログを受けとる
↓
ltsv* でアクセスログを出力
↓
アクセスログをfluentd* に流し込む
↓
fluentd のout_execプラグインで SID単位で集計・中間データ作成、DynaoDB* のアクセスログを保存
↓
解析サーバーで中間データとアクセスログから解析を行いDBに保存
~~~

なかでもポイントになるのは `*` 印のついた箇所になります。

### ltsv でアクセスログを出力
nginx の設定で簡単に、ltsv形式でログを出力することができます。
nginx で、`beacon.gif` を取得しにくるときのアクセスをすべて受けとめますが、ファイルサイズのすごく小さいしログを出力するだけなので、めちゃくちゃアクセスを捌けます。
ltsvのログフォーマットはこんな感じで設定しています。

~~~conf:nginx.conf
log_format  ltsv  "date:$time_local\t"
                  "client_id:$arg_cid\t"
                  "uid:$arg_uid\t"
                  "sid:$arg_sid\t"
                  "url:$arg_url\t"
                  "title:$arg_pt\t"
                  "referrer:$arg_ref\t"
                  "resolution:$arg_res\t"
                  "ip:$http_x_forwarded_for\t"
                  "ua:$http_user_agent";
~~~

### fluend に流し込む
fluentd が ltsv 形式のログの入力をサポートしているので ltsv を指定するだけでアクセスログを以降JSONとして扱ってくれます。

設定はこんな感じで行っています。
fluentd のマスターサーバーにデータを送信していてマスターサーバーは、マルチプロセスでポート番号を分けて動かしています。負荷が高くなったらサーバーを増やしてスケールアウトも簡単にできますね。

~~~conf:td-agent.conf
<source>
  type tail
  format ltsv
  path /var/log/nginx/tag.log
  pos_file /var/log/td-agent/tag.log.pos
  tag fluent.master
</source>

<match fluent.master>
  type forward
  send_timeout 10s
  recover_wait 10s
  heartbeat_interval 1s
  phi_threshold 10
  hard_timeout 60s
  buffer_type  file
  buffer_path /var/log/td-agent/buffer/tag-production
  buffer_chunk_limit 8m
  buffer_queue_limit 128
  flush_interval 1s
  flush_at_shutdown true
  <server>
    name fluent-1
    host 172.31.xxxx.xxxx
    port 24224
  </server>
  <server>
    name fluent-2
    host 172.31.xxxx.xxxx
    port 24225
  </server>
  <server>
    name fluent-3
    host 172.31.xxxx.xxxx
    port 24226
  </server>
  <secondary>
    type file
    path /var/log/fluentd/forward-failed
  </secondary>
</match>
~~~

### DynamoDBにアクセスログを保存する
[Homeup!](https://homeup.me)では、ユーザーの行動履歴を追えるようにするために、すべてのアクセスログをDBに保存しています。
もともと、フリーミアムで提供しようとしていたプロダクトだったので、インフラコストを極限まで削る必要がありいろいろ調べたのですが、なかでもDynamoDBのコスパがすごく良かったので採用にいたっています。

β版でのリリースから1年くらいたつのですが、いまでの特定ユーザーの1年前の行動履歴を管理画面で見ることができます。
クライアントが増えてくるとアクセスログの量が物凄いことになるのですが、DynamoDBだと低コストで運用できています。

仮に1ヶ月のトラフィックが 1億PVの場合、月のランニングコストは 450ドルくらいになるかと思います。
※ 間違ってたらご指摘ください ><

~~~ruby
#1行のアクセスログが2KBあったと想定して
(2.kilobyte * 100000000) / 1.gigabyte

# => 190GB
# 1GBあたりの単価が $0.285、25GBは無料なので
165 * 0.285
# => 47.025ドル
# 1億PVでストレージの利用料は 47ドルです。

# DynamoDBは スループット課金なので
100000000 / 1.month
# => 秒間 38アクセス
# 書き込みスループット:$0.00742:10 ユニットの書き込み容量あたり/1 時間
#（1 時間あたり最大 36,000 回の書き込みを実行するために十分な容量）
0.00742 * 38 * 24 * 30
# => 203.0112ドル

# 読み込みスループット:$0.00742:50 ユニットの読み込み容量あたり/1 時間
#（1 時間あたり最大 180,000 回の強力な整合性のある読み込みを実行するために十分な容量）
# 読み込みが書き込みの5倍のアクセスがあったとして同額の
# => 203.0112ドル

# 合計 47 + 203 + 203 = 453ドル
~~~

## まとめ
サーバーまわりは、これだけで十分なトピックスがあるので、集計ロジックの部分など、また新ためて記事にしようかと思います。
GAは無料でも十分な集計や解析を行うことができるサービスですが、サービスの性質や状態に合せて独自での実装を検討してみてはいかがでしょうか。