favyメンバーが好きを仕事にする日々を発信するfavicon

favy公式ブログ favicon(ファビコン)

How Introducing A/B Testing Lead to an Open Source Contribution (日本語翻訳付き)

2019/05/16 開発ブログ

Hello everyone!  My name is Robert Hustead, and I am an engineer here at Favy. I’d like to share with everyone an interesting assignment I was given: to implement A/B Testing for topic titles on our homepage, www.favy.jp.  This assignment came with some unforeseen difficulties and a lot of source code reading, but it eventually lead to an open source contribution!

(はじめまして!favy エンジニアのRobert Husteadです。最近www.favy.jpにA/Bテストを導入しました。非常に面白かったので、紹介したいと思います。この機能の実装は想像以上に難しく、多くのソースコードを読む必要がありました。しかし、そのおかげでオープンソースにコントリビュートすることが出来ました。)

ヒューステッド ロバート(Robert Hustead)
アメリカのカンザス州出身。2007年英語の講師として来日。2016年からプログラミングを勉強し初めて、2018年にfavyにジョイン。Webエンジニアとしてフロントとバックを開発しており、主に新規機能の開発に関わっています。GitHubDEV

What is A/B Testing?
(A/B テストとは?)

From Wikipedia, “A/B testing is a randomized experiment with two variants, A and B. It includes application of statistical hypothesis testing or “two-sample hypothesis testing” as used in the field of statistics.”

(広義のA/Bテストは、2つの施策同士を比較検討する行為全般を指します。)

To put that in plain English, we want to show our readers different titles for each article we post, and track which title gets the most clicks and is the most popular.

(ABテストとは、1記事に対し複数のタイトルを用意し、SNS等でランダムに表示させて効果測定することです。)

More than Just A/B
(2つ以上のタイトル同士を比較するA/Bテスト)

While A/B testing is very useful for finding the best choice between two alternatives, we wanted to press the envelope even further!  The assignment given to me was to implement A/B Testing on our front page, but instead of just two titles, we wanted to test 5 titles at once.

(A/Bテストは2つのタイトル同士を比較するのに非常に効果的ですが、favyでは2つ以上のタイトルを比較したく、僕は一度に5つのタイトルを比較する機能を実装する仕事を任されました。)

Moreover, the assignment called for tests to finish after a week of activity or after 100 clicks had been reached.  When editing a topic, titles were also to be displayed in order of their current popularity. When a test finished, the winning alternative was to always be displayed to users, and writers could also restart a test with new titles (keeping the previous winning title as well).

(今回は1週間か、5つの内どれかが100クリックされることでA/Bテストを終了させる実装にしています。また記事編集画面ではタイトルをクリック数の多い順番に表示することにしました。A/Bテストが終了したら、一番良かったタイトルを常に表示するようにし、再度A/Bテストをする時には、記事編集画面でそのタイトルが一番上に表示されるようにしました。)

Luckily, I had some experience implementing A/B Testing on another of our sites, Favy Inbound (https://favy-jp.com)  There, we implemented the “Split” gem, and I dove through the implementation to determine if running tests with more than just 2 alternatives would be possible.  Luckily, it was!

(幸い、僕には favy Inbound でA/Bテストの実装の経験があり、そこでは ‘Split’ というgemを使って実装しました。2つ以上のタイトルを実装できるかどうかを試してみたところ、可能だったので、今回も同じものを使うことにしました。)

What is “Split”?
(“Split” とは?)

From their Github page, “Split is a rack based A/B testing framework designed to work with Rails, Sinatra or any other rack based app.” (https://github.com/splitrb/split)

(Split は簡単なA/B テストのフレームワークです。)

One thing to note about Split is that it requires Redis to run correctly, but if you’ve installed Redis and have it running Split will work right out of the box.  There are many ways to implement Split, but I found that the easiest was to setup all the code in the views.

(Splitではredisが必要ですが、もしインストール済なら正常に動作します。Splitを使った実装には色々な方法がありますが、僕にとって最も簡単だと感じる方法は、viewにコードを書く方法です。)

For example, you can setup the A/B test in a view, which requires a test name and a list of alternatives using the ab_test keyword:

(例えばviewでA/Bテストをセットアップするときに、テスト名と選択肢とするタイトルのリストが必要です。)

- ab_test("name_of_test", "title_alternative_1", "title_alternative_2") do |random_alternative|
  = link_to random_alternative, topic_path(topic)

Then, you can finish the A/B Test in the View the user goes to after clicking the link by specifying the test name:

(そしてリンクをクリックした遷移先のviewでA/Bテストを終了します。)

- ab_finished("name_of_test")

You can then check the results of tests in a dashboard Split sets up for you.  Here we can also select a winner for a test and only display that alternative, or delete a test altogether, along with other useful functionality:

(Splitのダッシュボードで結果を確認することも出来ます。そのダッシュボードでテストの結果が一番良いものを確認することが出来たり、テストの選択肢を削除したり、様々な便利な機能があります。)

Simple, right?  It may appear that way, but there are some issues that need to be addressed:

(簡単ですね?そう見えますが、いくつか難しいところがあります。)

  1. We can’t hard code the alternative title names, because those will be input by our writers. (記事の編集者がタイトルを決めるため、タイトルをハードコードすることが出来ない)
  2. We can’t hard code a test name, because we will be running multiple tests and we need to ensure each name is unique.  Similarly, we won’t be able to finish tests because we don’t know what their name will be. (テスト名は一意である必要があり、複数のテストを同時に走らせるため、テスト名もハードコード出来ません。)
  3. It’s very inefficient to go through each test and select winners from the dashboard when you have 100’s of tests running. (100件以上になる等、あまりに多くのテストを同時に走らせると、ダッシュボードでの確認は非常に難しくなります。)

This doesn’t include things like displaying titles in the proper order or restarting tests, but these were the largest issues I had to confront. 

(タイトルの表示の順番の調整や、再テストの実行機能も難しかったですが、これの問題が一番大きかった課題です。)

Big challenges ahead!  Let’s look at how I approached each of these issues.

(よし!これらの問題をどのように解決していこうか!)

1. We can’t hard code the alternative title names, because those will be input by our writers. (記事の編集者がタイトルを決めるため、タイトルをハードコードすることが出来ない)

To solve this problem, I first created a new model to store our test title alternatives.  This model would only need to keep track of a single string; the alternative title. I then associated this model to Topics with a ‘belongs to/has many’ relationship, as so:

(まずタイトルの選択肢を保存するモデルを作成しました。このモデルはテストのタイトルを文字列として保存しているだけのモデルです。そしてこのモデルをTopicと関連付けました。)

I then setup the topic creation page to allow writers to input up to 5 different titles:

(そして編集画面に5つの異なるタイトルを追加することが出来ました。)

In the Split documentation, I found that the ab_test helper method would accept an array of alternatives.  This worked perfectly with our previous association, so I could now create A/B Tests like this:

(そしてSplitのドキュメントで ab_test ヘルパーメソッドの引数で配列を使うことが出来ることに気づきました。先程の関連付けを使ってA/Bテストをこのように作れるようになりました。)

- ab_test("name_of_test", topic.test_titles.pluck(:title)) do |random_alternative|
  = link_to random_alternative, topic_path(topic)

We’re now able to create A/B Tests using the titles our writers have input!

(これで編集者達が A/B テストを始めることが出来るようになりました。)

2. We can’t hard code a test name, because we will be running multiple tests and we need to ensure each name is unique.  Similarly, we won’t be able to finish tests because we don’t know what their name will be. (テスト名は一意である必要があり、複数のテストを同時に走らせるため、テスト名もハードコード出来ません。)

This was an easy problem to fix.  Instead of hard coding a test name, we could instead use the unique ID of each topic when creating and finishing the test.

(これは簡単でした。テスト名をハードコードするのではなく、topicのidを使うことで一意なテスト名を作成することができました。)

- ab_test("topic_#{topic.id}", topic.test_titles.pluck(:title)) do |random_alternative|
  = link_to random_alternative, topic_path(topic)

- ab_finished("topic_#{topic.id}")

3. It’s very inefficient to go through each test and select winners from the dashboard when you have 100’s of tests running. (100件以上になる等、あまりに多くのテストを同時に走らせると、ダッシュボードでの確認は非常に難しくなります。)

This too was a simple fix.  I set up a recurring job to run every day at noon and midnight.  The goal of this job was to check each topic that was running an A/B Test and see if it either had :

(これも簡単でした。12時と24時に毎日実行されるジョブを作りました。ジョブの目的は全てのA/Bテストに次の2つの条件が満たされているかを確認することです。)

  1. 100 or more clicks or (100回以上クリックされているか)
  2. A week had passed since the A/B test had started (A/Bテストを開始して一週間経ったか)

If either of the above conditions were true, a winner would be selected and the topic would finish it’s A/B test.

(上の2つのどちらかが当てはまればA/Bテストを終了して、一番結果の良かったものをタイトルとして保存します。)

The job was basically set up like this:

(そのジョブはこのようなコードです。)

Trouble Ahead(問題発生)

With this basic setup, we were able to create A/B Tests with 5 alternative topic titles!  Things were running smoothly on our level development environments, but we encountered a strange bug when deploying to our staging servers.  For some reason, Split was unable to locate the Redis server!

(このコードで5つの選択肢のあるA/Bテストが作れるようになりました。しかしローカルでは問題なかったですが、ステージング環境にデプロイするとバグが発生しました。ステージング環境では何故かRedisのサーバーと通信出来ませんでした。)

From the Split documentation,
“Split looks for the Redis host in the environment variable REDIS_URL then defaults to redis://localhost:6379 if not specified by configure block.”

(Splitのドキュメントには「SplitはREDIS_URLという環境変数を探し、configureにも設定されていない場合、デフォルトでは redis://localhost:6379 を参照する」と書いてありました。)

We had setup an environment variable on our server named REDIS_URL that pointed to the Redis server we had created, so why was Split having trouble finding it?  Of course, we could set up an initializer that would point Split directly at the Redis server, but that shouldn’t be necessary, and would be extra code that would be difficult to troubleshoot later.

(ステージング環境でREDIS_URLという環境変数を設定したのに、何故Splitはこれを見つけられなかったのでしょうか?もちろん initializer で redisの情報を設定することも可能ですが、それは無駄なコードな上、後になってバグの原因となりやすいです。)

After extensive reading of the source code for Split, I finally figured the problem.  Our web application makes use of Environment variables that we setup using a gem similar to Figaro (https://github.com/laserlemon/figaro).  However, Split was running its configuration block before our Environment variables were able to be setup!  Therefore, Split would look for a REDIS_URL environment variable, not find it, and fallback to it’s default.  This was fine when working locally, but would obviously not work when deploying to Staging and Production servers.

(Splitのソースコードをしっかり読んだところ、原因が分かりました。favy は環境変数をセットアップするためにFigaro (https://github.com/laserlemon/figaro)に似ている gem を使っています。でも環境変数のセットアップが完了する前に、Split のconfigure を参照してしまっていました。Split が REDIS_URL という環境変数を見つけられなかったので、デフォルトの設定を使ってしまっていました。なので、ローカルでは問題ありませんがステージング環境や本番環境ではバグになってしまいます。)


The problem was Split wasn’t checking to see if it was being run in a Rails application, and if so, waiting until other Rails specific configurations to run before it ran its own.  I took this as an opportunity to make my first contribution to an open source project. I cloned the repository, included code to check if it was being run in a Rails application, and if so to run its configuration after environment variables had been setup.


(Split は Rails のアプリで使われているかどうかをチェックしておらず、適切ではないタイミングで configure を実行していました。初めてオープンソースにコントリビュートするチャンスだと思いました。Splitのリポジトリをクローンして、Railsで使われている場合には適切なタイミングで cofigure を実行するコードを追加しました。)

My code was merged into the Split repository, marking my first contribution to an open code project!

(そして僕のコードがSplitにマージされました。これは僕の初めてのオープンソースプロジェクトへのコントリビュートとなりました。)

Conclusion(最後に)

With our inclusion of A/B Testing, we are hoping to not only increase page views but also increase the productivity of our writing team.  There were a great many challenges when introducing this new functionality, but I work with a great team and they were always available to help me when I got stuck.  Moreover, I was able to make my first contribution to an open source project. I’m looking forward to more challenges in the future!

(A/Bテストの導入で期待していることは、PVの増加と編集チームの生産性の向上です。多くのチャレンジがありましたが、非常に良いチームメンバーに相談することでこの困難を乗り越えることが出来ました。さらにオープンソースプロジェクトにコントリビュートすることも出来ました。次のチャレンジも楽しみにしています。)

そんなfavyのエンジニアチームでは、一緒に働くメンバーを募集しています。


-開発ブログ
-