広告の効果測定サービスで利用している、計測用のJavaScriptタグでSafariのITP対応した話

# はじめに

この記事は下記の勉強会での発表資料になります

https://connpass.com/event/68449/

---

# 自己紹介

株式会社オプト　シニアエンジニア @sisisin

- [GitHub](https://github.com/sisisin)
- [Twitter](https://twitter.com/_sisisin)
- フロントエンドマン（Angular,AngularJS中心）
- Scalaも少々
- 最近はスクラムマスターやってます


---

先日、Safari11にてIntelligent Tracking Prevention(以下ITP)という機能がリリースされました
ITP機能はユーザーのプライバシー保護のためにCookie利用に制限をかける機能で、これにより弊社の広告の効果測定サービスのユーザー行動の計測部分にも影響が出ました

`最前線で使われるリアルなJSフレームワーク事情！` という題目とはちょっと逸れてしまいますが、その問題について元々どのように計測を行っていたか、そこでどういうケースで問題になったか、最終的にどのようにその問題を解決したかを説明しようと思います

時間の関係で結構端折ってるところがあるのでご留意いただければと思います 🙏

---

# ITP機能とそれによって引き起こされる問題の概要

ITP機能とは、クロスサイトトラッキングを行っていると判定されたドメインとの通信においてCookieの利用に制限をかける機能です

元々3rd party cookieの利用には制限がかかってましたが、ITP機能では1st party cookieであってもユーザー行動の収集を行っているドメインだと判定された場合には利用に制限がかかるというのが重要なポイントです

---

# ユーザー行動の計測をどのように行うか

## ユーザー行動の計測とは

- `ユーザーが広告をクリックした`などの行動をしたら、その情報を計測サーバーへ投げ、情報を集積すること
- ユーザー識別のために計測サーバーで発行したuidをCookieに付与してユーザー識別を行う
- ユーザー行動としては、広告クリック、PV、CVを計測する

---

## 計測方法について

- 広告のリンクに計測サーバーのURLを設定してもらい、ユーザーに直接計測用サーバーへアクセスさせることで計測する方法
    - このとき、ランディングページへリダイレクトさせるので、リダイレクト計測と称される
- 広告主のサイトにユーザー行動を収集するJavaScriptタグを埋めてもらい、そのスクリプトが計測用サーバーと通信することで計測を行う方法
    - リダイレクト計測に対してダイレクト計測と称される

---

こういった情報を収集・集計することによって、`とある広告をクリックしてからCVした`というような行動を数字として出せるようになる

---

# ユーザー行動の計測をどのように行うか

## 技術的な話

### 求められる要件

- ブラウザ環境の考慮幅が広い
    - 広くユーザー行動を収集できることが正義
        - とはいえシェアが少なすぎるものについては省いているので、IE8とかは動作保証はしてません。~~ご安心ください（？）~~
        - だが、当然モバイルSafariは対応したい
    - DOMのAPIに潜む罠をよーく考慮して書かないとすぐ爆死する

---

### 求められる要件

- 配信するスクリプトのサイズは小さく保ちたい
    - ロードが遅いのは悪（計測が上がらない、広告主のサイトが重くなる、などなど）
    - 従って、配信されるコードは自前実装が基本
        - lodashやES2015のpolyfillを使えると思うなよ！というノリ
        - 今のところ外部ライブラリへの依存はなし
        - 当初はIE8対応も視野に入れて書いていたので、`Array.prototype.map`系統ですら自前で実装

---

### 求められる要件

- ビジネス的にミッションクリティカルな部分
    - 広告の効果測定というサービスの性質上、計測が上がってこなくなるような不具合は許されない
    - 実際に計測がロストすると補填に繋がる場合も・・・ 😨

---

総括すると、制約が多い割にバグったら即死で手応え満載な感じです！(前向きな表現)

---

### 利用してるライブラリなど

- TypeScript
    - polyfillなしで`target="ES5"`指定ならサイズは問題にならない
        - 行数・サイズ共にトランスパイル後の方がむしろ少ない
        - 生でES3なJavaScriptを書くよりは多少増えるところもあるはある（例えばclassの継承を書くと`__extends`という関数が増える、など）
    - ユニットテストで保証できる範囲にも限界があるので、やはり静的型付けは欲しい
- ビルド：browserify
- テスト：`mocha`,`jsdom`を利用したnode環境でのユニットテスト及び`selenium-webdriver`を利用したブラウザテスト
    - テスト時にはローカルにproxyサーバを立てて、ドメインにちゃんとCookieが付与されているか、といったテストもしている

---

### 計測リクエストの例

ITP対応前にどのように計測をしていたかを具体例を交えて説明します

---

#### 広告をクリックして、リダイレクト計測をした場合

![IMG_20171102_153743.jpg](https://qiita-image-store.s3.amazonaws.com/0/54872/9f47dffb-6264-191d-25ee-18b4834aa0b9.jpeg)

---

#### 広告をクリックして、リダイレクト計測をした場合

1. ユーザーは広告クリックによって計測サーバーへアクセス
2. 計測サーバーはそのリクエストから各情報を取得
    - リファラやIP、広告IDなど、リクエストから取れる情報を一通りとる
    - Cookieを確認し、uidがなければ発行し、Cookieを付与
3. リダイレクトレスポンスを返し、LPへ遷移させる

この例で、とある広告がとあるユーザーによって1回クリックされたという計測を行える
ポイントとして、リダイレクト計測時には**JavaScriptは実行されない**ことがあげられる

---

#### 広告主のサイトにてCVページへ遷移してダイレクト計測をした場合

![IMG_20171102_182413.jpg](https://qiita-image-store.s3.amazonaws.com/0/54872/f5719841-bcfc-4e3c-264a-f85fa5a4cf04.jpeg)

---

#### 広告主のサイトにてCVページへ遷移してダイレクト計測をした場合

1. CVページ表示時に埋められている設定を元に専用のjsファイルを取得
2. 計測サーバーへのリクエストを発火
  - リクエスト時にはリダイレクトのときと同様の情報を送信する
3. リクエストが返ってきたら、uidをLPドメインのCookieへ書き込み

JavaScriptによる計測を行う場合、uidの同期(=LPドメインへの書き込み)と、そのuid同期を踏まえたリクエストの順序制御がキモ


---

# ここまでを踏まえて、何が問題になったか

1. リダイレクト計測時の問題
    - そもそもuidをLPドメインへ書き込めない
    - 既にuidを持っているユーザであっても新規作成・上書きされてしまうパターンがあった
2. クロスドメインの問題
  - とあるLPドメインでuidをCookieに保存していても、ドメインを跨ぐとその値は読めないので同一ユーザーとして識別出来ない

---

## リダイレクト計測時の問題

- そもそもuidをLPドメインへ書き込めない
- 既にuidを持っているユーザであっても新規作成・上書きされてしまうパターンがあった

### 何をしたか

- リダイレクト時に計測およびuid生成を実施しないようにした
- LPに計測タグを置き、リダイレクト時にもダイレクト計測をするようにした（！）

---

Before(再掲)
![IMG_20171102_153743.jpg](https://qiita-image-store.s3.amazonaws.com/0/54872/9f47dffb-6264-191d-25ee-18b4834aa0b9.jpeg)

---

Before(再掲)

1. ユーザーは広告クリックによって計測サーバーへアクセス
2. 計測サーバーはそのリクエストから各情報を取得
    - リファラやIP、広告IDなど、リクエストから取れる情報を一通りとる
    - Cookieを確認し、uidがなければ発行し、Cookieを付与
3. リダイレクトレスポンスを返し、LPへ遷移させる

---

After
![IMG_20171102_154648.jpg](https://qiita-image-store.s3.amazonaws.com/0/54872/53f74d32-7378-9c44-847f-d08591d9bc15.jpeg)

---

After
1. ユーザーは広告クリックによって計測サーバーへアクセス
2. 計測サーバー何もせずにLPへリダイレクト
3. LPでタグが発火し、計測サーバへリクエストを実施
4. ダイレクト計測のリクエストを元に計測を上げ、uidを発行・付与
5. タグがuidをLPドメインへ書き込み

---

### どうしてこうなった

- ITP環境下では、リダイレクト時のリクエストであってもリクエストヘッダにCookieが付けられない
    - つまり、とあるユーザーの2回めのリダイレクト計測時に同一ユーザーだと判定できない
- LPドメインCookieへの書き込み自体は既存の仕組みがあったので、それに乗っかればよかった


---

## クロスドメインの問題

- とあるLPドメインでuidをCookieに保存していても、ドメインを跨ぐとその値は読めないので同一ユーザーとして識別出来ない

### 何をしたか

- ページ遷移のイベントをフックしてクエリパラメータにuidくっつけて飛ばすようにした
- サブドメイン跨ぎの場合は、cookieのdomain属性にトップレベルドメインを指定するようにした

---

### どうしてこうなった

#### ページ遷移時にクエリパラメータにuid付与について

- 計測サーバードメインの持つCookieが取れないため、LPドメインの持つCookieを正とする必要があった
- だが、`document.cookie`で取得できる値は自ドメインのものだけ
- 従って、ドメインをまたいでuidを同期するための仕組みが必要だった

---

#### cookieのdomain属性にトップレベルドメインを指定について

- Cookieの仕様[RFC 6265のDomain属性](https://triple-underscore.github.io/RFC6265-ja.html#section-4.1.2.3)に従って書くとサブドメイン跨ぎであれば同じCookie値を参照できるため


---

# 感想

- フロント側・サーバ側みたいな考え方ではなく、`計測`という概念を一括りで考えると答えに近づきやすいという学びを得た
- ITPの仕様について諸説あった中、予想しながら方針決めて進めて駄目で手戻りがあってつらみ・・・😇
    - WebKitのBlogとか読んでみても、結局その文章をどう解釈すればいいのかというのがはっきりしなかった
        - `ユーザー行動`って具体的に何を指すのか？など
    - WebKitのコード実際に読んで調べたことを書いた記事があり、自分たちもそこまでやってよかったかもとか
- `見た目`を制御するフロントエンドとはかなり毛色の異なる設計だったので、チーム間で設計思想の共有が難しかった
    - 試しに始めてモブプロやってみたが有効性が実感できた
    - 認識合わせ大事
- あってよかった充実したブラウザテスト


---

# 質疑