概要
先日不正なトラフィックを検出したということで、実質広告停止の制限措置を受けました。
Google Analyticsでクリック数を確認していったところ、恐らくAdSense狩りが原因じゃないかと結論付ける。
そのため、不正なクリックを検知して速やかにGoogleに報告する体制を整えることにしました。
Analyticsを使ってチェックできるようにしたが、精度に問題がありそうだという話です。
Prerequisites
- Adsense初心者
- Adsense狩りの検知のシステム化を検討している方
Implementations
設計
- Adsenseのクリック数はAnalyticsを連携設定すれば見れるため、これを利用する。
- ユーザーの識別はAnalyticsが割り当てているクライアントIDを利用する。
- Googleに報告できるよう、クライアントIDとIPアドレスを紐づける。
- Analyticsで簡単に確認できるようにする。
運営しているサイトはReactで作ったSPAで、Firebase Hostingに配置している。
そのため、IPアドレスの取得にはサーバーが必要になる。
最初Analytics以外のサービスにクライアントIDの送信し、そっちでIPは管理しようかと検討した。
しかし、工数や運用実績や利便性を考慮し、Analyticsで完結するようにした。
クライアントIDをAnalyticsに送信する
これは一つの記事としてスピンオフした。
https://qiita.com/qrusadorz/items/b609d1b37b116806fcae
IPアドレスを取得する
IPアドレスの取得なんて、RubyやPHP等のSSRであればググってすぐに見つかる。
しかし、Firebase Hostingでは予約済みサーバーはないため、FaaSのCloud Functionsを利用する。
(Firebase向けFunctionsを利用するのがベストだろうが、開発都合でしばらくGCPのものを利用する)
const util = require('util');
exports.ip = (req, res) => {
// TODO set my domain
res.set('Access-Control-Allow-Origin', '*');
if (req.method === 'OPTIONS') {
// Send response to OPTIONS requests
res.set('Access-Control-Allow-Methods', 'GET');
res.set('Access-Control-Allow-Headers', 'Content-Type');
res.set('Access-Control-Max-Age', '60'); // s
res.status(204).send('');
} else {
const ip = req.headers['x-forwarded-for'] || req.headers['x-appengine-user-ip'] || req.connection.remoteAddress;
const message = JSON.stringify({ ip });
console.log(message);
res.status(200).send(message);
}
};
IPアドレス取得のネタ元はこれ。
x-appengine-user-ipの方がGAEが付与するから、そっちを最優先にしたほうがいいと思う。(Firebaseに移行するからもう修正しないだけ)
https://gist.github.com/katowulf/6fffffb45ee5cbfbca6c3511e5d19528
FirebaseのFunctionsの場合はこちらになるはず。
https://stackoverflow.com/questions/56893378/get-users-ip-address-from-firebase-callable-function
冒頭のAccessから始まる設定やmethodの分岐は、Functionsのドキュメントほぼそのまま。
レスポンスがJSONのため、CORSによるプリフライトで問い合わせに来るための対応がOPTIONSの方の処理。
CORSについて知りたい場合は、MDNとCORSでググれば出てくる。
https://cloud.google.com/functions/docs/writing/http?hl=ja#functions_http_cors_auth-nodejs
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=%REACT_APP_GOOGLE_AN_CLIENT%"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
function gtagPageview(pathname){gtag('config', '%REACT_APP_GOOGLE_AN_CLIENT%', {'page_path': pathname,'custom_map': {'dimension1':'clientId', 'dimension2':'clientIp'}});}
function gtagIpEvent(){gtag('event', 'ip_dimension', {'clientIp': window.ip});}
gtag('js', new Date());
</script>
Functionsから受け取ったIPをwindow.ip
に保存後、gtagIpEvent()
をコールしてAnalyticsに送信している。
カスタムディメンションを利用するため、dimension2にclientIpを入れるようにしておき、gtagIpEvent()
ではclientIp
に値をセットしている。
カスタムディメンションの解説が必要ならこちら。
https://developers.google.com/analytics/devguides/collection/gtagjs/custom-dims-mets
Analyticsで確認する
前述の2ステップが終わると、AnalyticsでクライアントIDとIPが閲覧可能になる。
個人情報関連やAdsense関連が載せれないためモザイクばかりでわかりにくいが、カラム名の通り、クライアントIDとIPが紐づいて広告をクリックした数の多いユーザー順で並んでいる。
最もクリックしたユーザーは24回とこれは悪意があるだろうと見えた…
結論を述べてしまうと、このユーザーはリピーターで、クライアントIDよりAnalyticsのユーザーエクスプローラーで動きをチェックしたが、クリックしまくったとは考えにくい。
また、モザイクで見えないが10回以上クリックしたユーザーが残り4名いる。
これらについても興味をがあって訪れたユーザーで、悪意で来たとは考えられない。
更に、Adsenseの方で確認できるクリック数と、Analyticsで表示されているクリック数に5倍近い差がある。
このことから、Analyticsのクリック数の値が不正確であり、数が多い=不正なユーザーとは言えない状況。
Adsenseのドキュメントに違いが出る場合について解説があるが、どれかに当てはまるとは考えられない。
https://support.google.com/adsense/answer/6090783?hl=ja
SPAだからなのか?おま環なのか?とにかく、私の環境ではこの仕組みは正確に動作せず。
割合としてはユーザーの6%がクリックしており、1回クリックのユーザーが高確率で収益計上されている。
正確に出るケースもあるが、何らかの場合に本来クリックしていないのにクリックとして計算されているケースがある模様。
Conclusion
最終的にはAdSense狩り毎日チェックして、異常があればSlackに通知を送る予定でした。
しかし、事前調査の甘さもあり、まさかのクリック数が信頼できないという結果にシステム化を断念。
クリック数に正確性を求めるならiframe内のクリックを検知するようにする案もある。
しかし、不正な処理として誤解される可能性が最も怖いため、これについては廃案。
不正なトラフィックの防止と調査についてはこちら。
https://support.google.com/adsense/answer/1112983?hl=ja
https://support.google.com/adsense/answer/2583698?hl=ja
ここ半月Adsense狩りへの対策に時間を費やしたが、ここが限界と悟る。
もしこれで狩られてAdsenseのアカウントがBANされても、ここまでやったんだからあきらめがつく。
全力で取り組んだのだから、あとはネットに引っかかたボールがどちらに落ちようと納得がいく。
Have a great day!