概要
A/Bテストしてるサイトにfastly導入したのでやったことをまとめておく。
基本的にはfastly公式に書かれている内容を踏襲して設定すれば問題ない。
TL;DR
SP/PC向けのA/Bテストするサービスにfastlyを使うときは、VCLを書く。
- リクエストのユーザエージェントを見てヘッダに載せる
- A/Bパターンを計算してヘッダに載せてset-cookieする
- UA+ABパターンの組み合わせをVaryヘッダに載せる
背景
- バックエンドにAPIサーバを置きBFFでSSRしてhtmlを配信する、SPAのWebサービスを開発中。
- 検索されたクエリでバックエンドからデータを取得してリストで表示する。
- このWebサービスのエッジにhtmlをキャッシュするためにfastlyを導入する。
課題1
ユーザエージェントを見てPCとSPで表示するビューコンポーネントを変えている。しかし、ページ対して最初にアクセスしてきたユーザのユーザエージェント(SP/PC)のhtmlがキャッシュされてしまい、次回以降にアクセスするユーザのユーザエージェントがSP/PCに関わらず、最初にアクセスしてきたユーザ向けに生成されたhtmlが返されてしまう。
例えば、
- ユーザ1(SPユーザ)が
/list
ページにアクセスする - BFFはユーザエージェントを確認しSP向けのhtmlをSSRして返す
- fastlyが
/list
のSP向けhtmlをキャッシュする - ユーザ2(PCユーザ)が
/list
ページにアクセスする - fastlyはSP向けのhtmlキャッシュを返す
やったこと1
fastlyにも書かれているが、ユーザエージェントからsp/pcを判定するvclを書いてcustom vclに登録し、Mainから呼び出せばよい。このVCLをほとんどコピペで問題ない。sp/pcだけ分かれば良いならreq.http.X-UA-Device
にはsp/pcを書くようにする。
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ヘッダに載せる。
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が
/list
ページにアクセスする - BFFはユーザ1にランダムにAパターンを割り振りcookieをセットする
- BFFはAパターンのhtmlをSSRして返す
- fastlyが
/list
に対してAパターンのhtmlをキャッシュする - ユーザ2が
/list
ページにアクセスする(A/Bパターンが振り分けされない) - fastlyはAパターンのhtmlキャッシュを返す
やったこと2
このケースでもVCLを用意することで解決する。具体的には、BFFでA/Bパターンを計算してset-cookieするのではなく、fastlyのVCLでA/Bパターンを割り振りset-cookieする。
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関係なくね?と言われたら何も言えない...
- Vary headerは、対象のheaderのNameを書くので、""で囲まないといけない
- ヘッダのNameはCase Insensitiveということを忘れててBFFでreq.headers["X-UA-Device"]とかで読み取ろうとしてundefinedになった
- 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のエンドポイントを構築・更新する仕組みも作っているので、記事にするかも。