A/BテストしてるWebサービスにfastlyを導入した

# 概要
A/Bテストしてるサイトにfastly導入したのでやったことをまとめておく。
基本的にはfastly公式に書かれている内容を踏襲して設定すれば問題ない。

<b>TL;DR</b>
SP/PC向けのA/Bテストするサービスにfastlyを使うときは、VCLを書く。
* リクエストのユーザエージェントを見てヘッダに載せる
* A/Bパターンを計算してヘッダに載せてset-cookieする
* UA+ABパターンの組み合わせをVaryヘッダに載せる

# 背景
* バックエンドにAPIサーバを置きBFFでSSRしてhtmlを配信する、SPAのWebサービスを開発中。
* 検索されたクエリでバックエンドからデータを取得してリストで表示する。
* このWebサービスのエッジにhtmlをキャッシュするためにfastlyを導入する。

# 課題１
ユーザエージェントを見てPCとSPで表示するビューコンポーネントを変えている。しかし、ページ対して最初にアクセスしてきたユーザのユーザエージェント(SP/PC)のhtmlがキャッシュされてしまい、次回以降にアクセスするユーザのユーザエージェントがSP/PCに関わらず、最初にアクセスしてきたユーザ向けに生成されたhtmlが返されてしまう。

例えば、
1. ユーザ1(SPユーザ)が`/list`ページにアクセスする
2. BFFはユーザエージェントを確認しSP向けのhtmlをSSRして返す
3. fastlyが`/list`のSP向けhtmlをキャッシュする
4. ユーザ2（<b>PCユーザ</b>）が`/list`ページにアクセスする
5. fastlyは<b>SP向けの</b>htmlキャッシュを返す

# やったこと1
<a href="https://docs.fastly.com/guides/vcl-tutorials/delivering-different-content-to-different-devices">fastlyにも書かれている</a>が、ユーザエージェントからsp/pcを判定するvclを書いてcustom vclに登録し、Mainから呼び出せばよい。<a href="https://github.com/varnishcache/varnish-devicedetect/blob/master/devicedetect.vcl">このVCL</a>をほとんどコピペで問題ない。sp/pcだけ分かれば良いなら`req.http.X-UA-Device`にはsp/pcを書くようにする。

``` detect_device.vcl
sub detect_device {
  set req.http.X-UA-Device = "pc"
  if (req.http.User-Agent ~ "(?i)ip(hone|od)" || ...) {
    set req.http.X-UA-Device = "sp"
  }
}
```

これをメインのVCL(main.vcl)でincludeし、fastlyへリクエストが届いたとき（vcl_recv）にメソッドを呼び出してVaryヘッダに載せる。

```main.vcl
include "detect_device"

sub vcl_recv {
#FASTLY recv
  ...

  call detect_device;
  return(lookup);
}

sub vcl_fetch {
#FASTLY fetch
  ...

  if (beresp.http.Vary) {
  	set beresp.http.Vary = beresp.http.Vary ", X-UA-Device"; 
  } else {
  	set beresp.http.Vary = "X-UA-Device";
  }
  return(deliver);
}
```

fastlyではVaryヘッダの値でキャッシュが紐づいて保管されるので、X-UA-Deviceが別なら異なるキャッシュとして管理されるようになる。これでSP向け、PC向けに分けてキャッシュを管理できる。
BFFではX-UA-Deviceの値を取得し、それに応じてSSRしたhtmlを返す。


参考：
https://www.fastly.com/blog/best-practices-using-vary-header
https://docs.fastly.com/guides/vcl-tutorials/delivering-different-content-to-different-devices


# 課題2
検索パターンをA/Bテストするためにcookieで振り分けている。しかし、ユーザエージェントの場合と同様に一度パターンがキャッシュされてしまうと、それ以降全てのユーザに対して同一パターンのキャッシュが返されてしまう。

例えば、
1. ユーザ1が`/list`ページにアクセスする
2. BFFはユーザ1にランダムにAパターンを割り振りcookieをセットする
3. BFFはAパターンのhtmlをSSRして返す
4. fastlyが`/list`に対してAパターンのhtmlをキャッシュする
5. ユーザ2が`/list`ページにアクセスする（A/Bパターンが振り分けされない）
6. fastlyは<b>Aパターンの</b>htmlキャッシュを返す

## やったこと2
このケースでもVCLを用意することで解決する。具体的には、BFFでA/Bパターンを計算してset-cookieするのではなく、fastlyのVCLでA/Bパターンを割り振りset-cookieする。

```abtest.vcl
sub set_abparam_into_header {
  #in recv
  if (!req.http.Cookie:TestParam) {
    if (randombool(50,100)) {
      set req.http.x-test-param = "a-pattern";
    } else {
      set req.http.x-test-param = "b-pattern";
    }
  } else {
    set req.http.x-test-param = req.http.Cookie:TestParam; 
  }
}

sub set_vary_header {
#in fetch
  if (beresp.http.Vary) {
  	set beresp.http.Vary = beresp.http.Vary ", x-test-param, X-UA-Device"; 
  } else {
  	set beresp.http.Vary = "x-test-param, X-UA-Device";
  }
}

sub set_abparam_into_cookie {
#in deliver
  if (!req.http.Cookie:TestParam){
    add resp.http.Set-Cookie = "TestParam=" req.http.x-test-param + "; Expires=" + time.add(now, 24w) + "; Path=/;";
  }
}

```

それぞれのメソッドをmain.vclの、vcl_recv, vcl_fetch, vcl_deliverから呼び出す。
これにより、A/Bテストのパラメータがリクエストのヘッダに載り、クライアントへset-cookieして返される。
BFFでは、リクエストのヘッダからx-test-paramを取得して、適切なパターンのhtmlをSSRする。

参考：
https://www.fastly.com/blog/ab-testing-edge

# おまけ： はまったこと
恥ずかしながらちょっとはまったところをメモしておく。fastly関係なくね？と言われたら何も言えない...

1. Vary headerは、対象のheaderのNameを書くので、""で囲まないといけない
2. ヘッダのNameはCase Insensitiveということを忘れててBFFでreq.headers["X-UA-Device"]とかで読み取ろうとしてundefinedになった
3. set-cookieでPath=/;を忘れてパスごとにcookieが設定されてしまった。

などなど...

# まとめ
SP/PC向けのA/Bテストするサービスにfastlyを使うときは、VCLを書く。
* リクエストのユーザエージェントを見てヘッダに載せる
* A/Bパターンを計算してヘッダに載せてset-cookieする
* UA+ABパターンの組み合わせをVaryヘッダに載せる

VCL怖くない！
実際に動くのかの確認はactiveにしてサーバあげないとできないけど、
saveしたらエラー教えてくれるし、バージョン管理もできるし、結構よくできてる。

余裕があったら、Drone CIでfastly向けブランチをpushするたびにfastlyのエンドポイントを構築・更新する仕組みも作っているので、記事にするかも。