読者です 読者をやめる 読者になる 読者になる

俺の報告

RoomClipを運営するエンジニアの日報(多分)です。

GoogleAnalyticsをアドブロックされた時の対策例 - 日報 #155

iOS9.0からSafariでアドブロック機能が追加されたとかで、
各所盛り上がりを見せております。

特に問題なのはGoogleAnalyticsによる計測までブロックされている!という衝撃的事件でございまして、
広告以外の分野まで飛び火して大盛り上がりの様子です。
AdBlockの検知jsなどが公開されて、今はいったん沈静化したようですが、
「確認しといて」と言われっぱなされた一部のエンジニアは猛烈にアンニュイな気持ちになっていることでしょう。

ということで、 少し冷静にその辺を整理してみたいと思います。

今回の騒ぎはSafariの「Content Blocking Safari Extensions」というエクステンション機能が公開されたことに端を発しています。
正式なドキュメントはこちら。

https://developer.apple.com/library/prerelease/ios/releasenotes/General/WhatsNewInSafari/Articles/Safari_9.html

これ自体はなんのことはない、chromeでもありそうな(多分あると思います)機能でして、 要は「ある条件をトリガーに、ブロッキングするような動作をする」という条件をjsonにして登録できる機能なんですね。

詳しくはこの辺をご参照。 http://www.toyship.org/archives/2182

んで、このある条件をトリガーというところは、
例えば「特定の名前のクラス名のDOM要素」とか「特定のドメインのjsロード」とか、 まぁそういうことを指定できるわけです。
ほいで、「ブロッキングするような動作」というのは、
「DOM要素を削除する」とか「ロードを禁止する」とかそういう動作のようです。

この機能を使えば、「google-analytics.comというドメインのjsロード」を条件に、
「ロードを禁止する」というブロッキング動作を規定できるようになります。
これは何もiPhoneだけの話ではなく、自分でも簡単にSafariから設定できます。

で、なんと新しいiOS9.0からサードパーティのアプリでadBlockList.jsonを設定して、 iPhoneSafari設定からそのリストをロードできるようになってるっぽいんですね。
まつまり、Crystalのようなad blockアプリをインストールすると、
(恐らく)最新版のリストjsonをアプリがセットして、
それをもとにSafariブロッキング動作をする、
という流れのようです。
OSSでのblockerList.jsonは例えばこんな感じ。
https://github.com/krishkumar/BlockParty/blob/master/RediffBlock/blockerList.json
ちゃんとgoogle-analytics.comがurl-filterにしていされており、action.type = 'block'となっています。

この状態でGA計測のサイトにアクセスするとSafariのコンソールに、

Content blocker prevented frame displaying http://hogehoge.com/ from loading a resource from http://www.google-analytics.com/ga.js

こんなようなログが吐き出されているのがわかります。
恐らく

<script src="http://google-analytics.com/ga.js">

が引っかかったんでしょう。
なるほど、よくわかりましたと。

さて、ここまでわかれば色々と対策を打てそうなものですね。

対策1 GoogleAnalyticsをブロックさせない追記:下記の手法では厳しいです…ごめんなさい。

追記! 下記の方法ではgoogle-analytics.comへの全ての通信を代替できないので、ブロックさせないのは無理でした。申し訳ありません…
下記のようなアイディアとしてgoogle-analytics.comの手前に自前のproxyを置くなどが考えられます。

Googleのポリシー上OKならこのやり方がベストなんじゃないかなぁと思いつつ、
これはやっていいのかどうなのか不明なので、
とりあえずやり方だけ書いて置きます。

単純にgoogle-analytics.comがフィルタ対象なのであれば、
別のドメインからga.jsを読めばいいだけの話です。

<script type='text/javascript'>
  var _gaq = _gaq || [];
  _gaq.push(['_setAccount', 'UA-XXXXXX-X']);
  _gaq.push(['_setDomainName', 'hoge.hoge']);
  (function() {
    var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
    var protocol = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www');
    var src = protocol + '.google-analytics.com/ga.js';
    ga.src = '/ga_js?p=' + encodeURI(protocol);
    var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
  })();
</script>

ここで /ga_js というページを自身のドメインに用意して、

<?php
  $p = isset($_GET['p']) ? $_GET['p'] : 'http://www';
  $base_analytics_uri = '.google-analytics.com/ga.js';
  $url = $p . $base_analytics_uri;
  echo file_get_contents($url);
?>

こんな感じで google-analytics.com/ga.js を読み込んで返してあげれば、
ブロックされないでしょう。(1回だけテストしたらブロックされませんでした)
(ですが、ずーっとこのやり方でいけるのかは分かりません。)

これがOKなやり方なのか非常に疑問なので、まだ実装はしていませんが、
なんか、、、大丈夫だと自信を持って言える方をお待ちしております。

対策2 対策しないけど、blockを計測する

被害の規模で行動を決めるというは当然のことですね。
やっぱりまだまだAdBlockアプリは有料も多いし皆そんなにいれてないだろう、
とかいう予想が本当なのかチェックしましょう。

とうことで、
http://www.google-analytics.com/ga.js
を読み込みます。
http://jsbeautifier.org/
上記サイトで難読化されたga.jsを綺麗にして眺めますと、

// 2153行目あたり
var Z = new Y; 

// 850行目あたり
Y = function() {
  T(Y.prototype,"push", Y.prototype.push, 5);
  T(Y.prototype, "_getPlugin", Pc, 121);
  T(Y.prototype, "_createAsyncTracker", Y.prototype.Sa, 33);
  T(Y.prototype, "_getAsyncTracker", Y.prototype.Ta, 34);
  this.I = new nf;
  this.eb = []
 };

// 83行目あたり
var nf = function() {
  this.prefix = "ga.";
  this.values = {}
};

// 729行目あたり
var W = window,

// 2154行目あたり
    (function(a) {
        if (!Fe(a)) {
            H(123);
            var b = !1,
                c = function() {
                    if (!b && Fe(a)) {
                        b = !0;
                        var d = J, 
                            e = c; 
                        d.removeEventListener ? d.removeEventListener("visibilitychange", e, !1) : d.detachEvent && d.detachEvent("onvisibilitychange", e)
                    }    
                };   
            Ga(J, "visibilitychange", c)
        }    
    })(function() {
        var a = W._gaq,
            b = !1;
        if (a && Ba(a.push) && (b = "[object Array]" == Object.prototype.toString.call(Object(a)), !b)) {
            Z = a; 
            return
        }    
        W._gaq = Z; 
        // ここ!!! console.log(W._gaq.I.prefix);
        b && Z.push.apply(Z, a)
    });

上記をよく読めばわかりますが、
まず 2153行目あたりで、変数ZにYをnewしています。
Yのオブジェクトは、850行目あたりで、this.Iにnfというオブジェクトをnewしています。
nfオブジェクトは83行目あたりで、

nf.prefix = 'ga.';

しています。
ほいで、2154行目あたりにある即時関数で、
window._gaqにZを代入しています。

つまり、window._gaqの要素名Iにおいて、prefix='ga.'が代入されているはずです。
これが入っていればきっと正しくga.jsが起動していると思われます。
(まぁ仕様変更されたら辛いですが)

なので、
各ページのjsにおいて、

$(document).ready(function() {

  // 例えばこんな感じ。
  // 例えば1秒後にwindow._gaq.I.prefixが格納されていればgoogle-analytics.com/ga.jsはブロックされなかったみたいな
  setTimeout(function () {
    if (window._gaq != undefined) {
      if (window._gaq.I != undefined) {
        if (window._gaq.I.prefix != 'ga.') {
          // ここでロードされているか判定
        }
      }
    }
  }, 1000);

}

こんなようなことをしておけば、「こいつアナリティクスブロックしてきた」とわかります。
これをカウンティングしておけば、大体のPVに対してどの程度ロストしたのかを測ることができると思います。

以上ですが、
google-analytics.comをフィルタしてくるってどういう意図なんでしょうかね。
ちょっとそこは詳しく聞いてみたいところです。

さて、日々生じる外敵脅威と闘いながらも、
常に対策し切る頑強なエンジニアを弊社は募集しております。
なにとぞ、よろしくお願いいたします。

www.wantedly.com
www.wantedly.com