😊

Cloudflare版GTMのZarazの挙動確認&自前プラグインを実装

2024/10/16に公開

https://www.cloudflare.com/ja-jp/application-services/products/zaraz/

Zaraz はCloudflare 版の GTM

要約すると Google Tag Manager の Cloudflare 版。クライアントだけではなく、Cloudflare CDN を通る時の CDN Edge でも動く。

メジャーなサードパーティスクリプトはCloudflare謹製のものがあるが、基本的には GTM にあるものをそのまま移せるようなものではなく、専用に実装されないといけない。

詳しい使い方はこちら

https://zenn.dev/kameoncloud/articles/a2a0ee914c3eea

Zaraz の目的

なんでわざわざ普及したGTMではなくこんなものを?という疑問があると思う。とくに初見だとこれがなんなのか本当にわからないはず。自分もサードパーティスクリプトの開発者を経験したからこそ分かる部分と、まだわかってない部分がある。

とりあえず次のブログでZarazの買収意図が書いてある。

https://blog.cloudflare.com/ja-jp/why-cloudflare-bought-zaraz/

あなたがサードパーティのスクリプト開発者で、スクリプトを適切に保護していない場合、Zarazがより多くのWebに展開されるにつれて、あなたのスクリプトは動かなくなることに注意してください。現在、Cloudflareは全Webサイトの20%近くを保護していますが、近い将来、Zarazの技術によってすべてのWebサイトが保護されるようになるとかんがえています。当社は、お客様のサイトで動作するすべてのスクリプトが、最新のセキュリティ、信頼性、そしてパフォーマンスの基準を満たしていることを確認したいと考えています。そこに至るまでに支援が必要な場合は、ぜひzaraz@cloudflare.comまでお問い合わせください。皆様を支援する準備をしてお待ちしています。

おそらく Cloudflare 視点だと、主にGTM起点でネットワーク上を流れるサードパーティスクリプトが、ネットワークのトラフィックの大部分を占めているのがわかっているのだと思う。そして、それらは本質的でないユーザーの帯域を奪い合い、ホストアプリケーションごとサービスを低速化させている。

つまり Cloudflare 自体がどれだけ速くなろうとも、人々がGTMを使い続ける限りネットワークのラウンドトリップでパフォーマンスの限界があるし、またサードパーティスクリプト上のセキュリティ問題は発生し続ける。エンジニアがどれだけセキュリティを意識しようが、非エンジニアのいれるサードパーティスクリプトの導入で簡単にインシデントが起きうる。

Cloudflareはサードパーティスクリプトがなくせないことがわかってるが、CDNでこの問題に対処できるプレーヤーでもある。つまり、自分たちでサードパーティスクリプトのレイヤーを提供しつつ、CDN上でスクリプトを払い出すときに、その実行権限を制御している。

例えば GDPR や各種の法規制で何ができるか/できないかの制御を Cloudflare CDN が握っているので、注入されるスクリプト側では、それに違反する振る舞いが基本的にはできなくなる。
そして、CDN Edge でコロケーションするので、ネットワークラウンドトリップを排除できる。これにより、パフォーマンスとセキュリティを同時に提供する。

運用側からすると、パフォーマンスは餌でセキュリティが鞭という感じはありそう…

Zaraz で GA4 を動かす時に何が起こるか

とにかく中で何が起こるか調べて見ないと何も言えない気がしたので、手元で実験した。

元はこのGAのタグ。

<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-xxxxxxxxxxxx"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'G-xxxxxxxxxxxxxxxxxxx');
</script>

元のタグを除去し、Measurement ID を Cloudflare Zaraz 側に登録しながら GA4 連携を有効化する。
すると、GAの代わりに次のようなタグが挿入される。

<script defer="" referrerpolicy="origin" src="/cdn-cgi/zaraz/s.js?z=JTdCJTIyZXhlY3V0ZWQlMjIlM0ElNUIlNUQlMkMlMjJ0JTIyJTNBJTIyRG9jdW1lbnQlMjIlMkMlM...=="></script>
<script nonce="2a367df0-c31c-45b7-8db2-64dbc7400bc6">try{(function(w,d){!function(j,k,l,m){if(j.zaraz)console.error("zaraz is loaded twice");else{j[l]=j[l]||{};j[l].executed=[];j.zaraz={deferred:[],listeners:[]};j.zaraz._v="5811";j.zaraz._n="2a367df0-c31c-45b7-8db2-64dbc7400bc6";j.zaraz.q=[];j.zaraz._f=function(n){return async function(){var o=Array.prototype.slice.call(arguments);j.zaraz.q.push({m:n,a:o})}};for(const p of["track","set","debug"])j.zaraz[p]=j.zaraz._f(p);j.zaraz.init=()=>{var q=k.getElementsByTagName(m)[0],r=k.createElement(m),s=k.getElementsByTagName("title")[0];s&&(j[l].t=k.getElementsByTagName("title")[0].text);j[l].x=Math.random();j[l].w=j.screen.width;j[l].h=j.screen.height;j[l].j=j.innerHeight;j[l].e=j.innerWidth;j[l].l=j.location.href;j[l].r=k.referrer;j[l].k=j.screen.colorDepth;j[l].n=k.characterSet;j[l].o=(new Date).getTimezoneOffset();if(j.dataLayer)for(const t of Object.entries(Object.entries(dataLayer).reduce(((u,v)=>({...u[1],...v[1]})),{})))zaraz.set(t[0],t[1],{scope:"page"});j[l].q=[];for(;j.zaraz.q.length;){const w=j.zaraz.q.shift();j[l].q.push(w)}r.defer=!0;for(const x of[localStorage,sessionStorage])Object.keys(x||{}).filter((z=>z.startsWith("_zaraz_"))).forEach((y=>{try{j[l]["z_"+y.slice(7)]=JSON.parse(x.getItem(y))}catch{j[l]["z_"+y.slice(7)]=x.getItem(y)}}));r.referrerPolicy="origin";r.src="/cdn-cgi/zaraz/s.js?z="+btoa(encodeURIComponent(JSON.stringify(j[l])));q.parentNode.insertBefore(r,q)};["complete","interactive"].includes(k.readyState)?zaraz.init():j.addEventListener("DOMContentLoaded",zaraz.init)}}(w,d,"zarazData","script");window.zaraz._p=async bs=>new Promise((bt=>{if(bs){bs.e&&bs.e.forEach((bu=>{try{const bv=d.querySelector("script[nonce]"),bw=bv?.nonce||bv?.getAttribute("nonce"),bx=d.createElement("script");bw&&(bx.nonce=bw);bx.innerHTML=bu;bx.onload=()=>{d.head.removeChild(bx)};d.head.appendChild(bx)}catch(by){console.error(`Error executing script: ${bu}\n`,by)}}));Promise.allSettled((bs.f||[]).map((bz=>fetch(bz[0],bz[1]))))}bt()}));zaraz._p({"e":["(function(w,d){})(window,document)"]});})(window,document)}catch(e){throw fetch("/cdn-cgi/zaraz/t"),e;};</script>
<script>(function(w,d){})(window,document)</script><script>(function(w,d){;w.zarazData.executed.push("Pageview");})(window,document)</script>

zaraz のサードパーティスクリプトを挿入し、ここが PageView Event が送信している。おそらくこれがサーバーサイドのJSとしてトラッキングイベントとして送られる。何のデータを渡していいかをzarazが制御しているっぽい。

ペイロードの中身は escape(btoa(JSON.stringify({...})))

unescape(atob('JTdCJTIyZXhlY3V0ZWQlMjIlM0ElNUIlNUQlMkMlMjJ0JTIyJTNBJTIyRG9jdW1lbnQlMjIlMkMlM...=='))
//=>{"executed":[],"t":"Document","x":0.9681367419126385,"w":3072,"h":1728,"j":984,"e":507,"l":"<testurl>","r":"","k":24,"n":"UTF-8","o":-540,"q":[]}

この結果、GAの管理画面でトラックされたのを確認した。認証データを隠したり、色々できる。

Zaraz と Managed Components

この手のツールで大事なのは、困った時に自分で実装できることである。なので、自分で zaraz のタグを実装する方法を調べた。

まず、Cloudflare と独立して Managed Components という規格がある。これは Cloudflare と無関係なふりをしているが、実態としてはほぼ zaraz と 1:1 に紐づいていそう。ややマッチポンプ感がある。

https://managedcomponents.dev/

以下のドキュメントで、この仕様にしたがって動く Cloudflare Workers をデプロイする。

https://developers.cloudflare.com/zaraz/advanced/load-custom-managed-component/

https://managedcomponents.dev/getting-started/quickstart/ に従って、npm init managed-component でコードをスキャフォルドする。自分はクライアントコードを実行できるやつを作った。

import { ComponentSettings, Manager } from '@managed-components/types'

export default async function (manager: Manager, _settings: ComponentSettings) {
  manager.addEventListener('pageview', event => {
    console.log('Hello server!')
    event.client.execute("console.log('Hello browser')")
  })
}

これをデプロイ。

npm run build
npx managed-component-to-cloudflare-worker ./dist/index.js my-custom-mc

(なんか色々と権限を聞かれたが、メモするのを忘れた)

これをやっておくと Cloudflare Zaraz の管理画面から、GA4 を選んだときのパスで Custome Managed Component のプルダウンからこの my-custom-mc が選べるようになってるので、これを選んで有効化する。

結果としてこのコードがHTMLに挿入されるようになる。

<script>(function(w,d){{console.log('Hello browser')}})(window,document)</script>

つまり、Zaraz提供側は CDN Edge を通る時の処理、クライアントを通る時の2層の処理が書けるが、何のデータが渡ってくるかはCDNの管理側が握っている。

感想

開発者視点だと、GTMよりマシだがCloudflareの強烈なポリシーに従うことを要求されるので、Googleや各種デベロッパーは嫌がりそう。とはいえ、セキュリティとパフォーマンスのお題目を持っているので、スモールスタートならGTMではなくZarazを仕込んでおくといいのかもしれない。GDPR対応の名目でやることも考えられる。

Cloudflare の意図としては、非開発者のウェブ管理者を GTM から引き剥がしてZarazに誘導したいのだろうが、現状は機能不足で、Zaraz のプラグイン一覧でどれだけ今の要求を達成できるか?というのが争点になるだろう。

実際パフォーマンス上のメリットは大きく、一旦は開発主導でメリットを訴えて採用しておくといいかもしれない。開発力がある組織なら自力で Managed Components を実装するなどのアプローチが考えられる。

とにかく大手のウェブサイトほど、AnalyticsやAdの入れ過ぎでユーザー体験が悪化しているし、データを取るのが良いことと考えられているフシがある。この状況は正しくない。これは、思いつく限り全部を入れてそうなニコニコ動画のランキングページの図

流行るかなぁ… 開発者としては間違いなくGTMよりは嬉しいが…

Discussion