こんにちは。地域情報部エンジニアの岡田です。
地域情報部では、
2019年10月に「地域情報サービス静岡版」としてトクバイアプリ内でサービスをリリースしました。
https://locoguide.co.jp/news/2019-10-15/
どの段階のサービスでも、改善を目指し、
ABテストや、
新機能を一部のユーザーに公開して、
価値があるのかを検証すると思います。
今回は、
そういったABテストや限定公開ができる機能を自作し、
gem化したので紹介したいと思います。
gem: https://github.com/Jun0kada/limited_release
なぜ自作したか
既存のgemでもABテストや限定公開機能はありますが、
自作した理由は、
シンプルな機能で良いので
学習コストが低く、
すぐ限定公開機能をリリースできるようにしたいと思ったからです。
また、今回自作をするにあたり、こちらのgemを参考にしました。 https://github.com/amatsuda/motorhead
機能の要件
- シンプルでさっと実装できる
- PDCAをたくさん回すために少ない実装で済む
- 既存のアプリコードを触らない
- 既存アプリにバグを作る可能性の排除
- エラーを起こさない
- 例:ABテストしているUIでエラーが起きたら元のUIを表示する
要件に入れなかったことと理由
- stylesheetやjavascript
- assetsの管理はプロダクトによってかなり違いがあるので、それぞれに任せる
- %公開とか
- 実装大変なので...
- コンバージョンの計測
- 実装大変なので...
- 独自でログ取ってることも多いと思うので必要ないかなと思い
使い方
使い方をサンプルコードで紹介します
例:/articles(記事一覧ページ)の並び順とUIを変えてみる
既存のアプリコードはこんな感じです。
- 公開日時降順の記事一覧
- ulで表示
# routes.rb Rails.application.routes.draw do resources :articles # ... end
# app/controllers/articles_controller.rb class ArticlesController < ApplicationController def index @articles = Article.order(published_at: :desc).page(params[:page]) end # ... end
<!-- app/views/articles/index.html.erb --> <ul> <% @articles.each do |article| %> <li> <%= link_to article.title, article_path(article) %> </li> <% end %> </ul>
そして、
下記の仕様でABテストとして限定公開したいと思います。
- 公開日時の降順ではなく、更新日時の降順
- ulではなくolで表示
- 計測のために記事詳細ページへのリンクにはパラメーターを付ける
- だいたい50%くらいのユーザーに提出
# config/limited_releases/articles_page_ab_test.rb class ArticlesPageAbTest include LimitedRelease::Feature active_if do # ユーザーIDが奇数か偶数かでだいたい50% current_user.id.odd? end routes do get '/articles', to: 'articles#index' end helpers do def link_to_article_with_tracking_params(article) link_to article.title, article_path(article, articles_page_ab_test: true) end end end
# app/controllers/limited_release/articles_controller.rb class LimitedRelease::ArticlesController < ::ArticlesController limited_release 'ArticlesPageAbTest' def index @articles = Article.order(updated_at: :desc).page(params[:page]) # OR # super # @articles = @articles.reorder(updated_at: :desc).page(params[:page]) end end
<!-- app/views/limited_release/articles/index.html.erb --> <ol> <% @articles.each do |article| %> <li> <%= link_to_article_with_tracking_params(article) %> </li> <% end %> </ol>
これで、IDが奇数のユーザーには、
記事の並び順が更新日時の降順、かつ、olで表示されます。
解説
サンプルコードを見ながら、このgemがどういう実装になっているか説明したいと思います。
1. LimitedRelease::Feature
まず、config/limited_releases/articles_page_ab_test.rb
で、 LimitedRelease::Feature
moduleをincludeすることにより、下記が行われます。
ArticlesPageAbTest::Helper
moduleを生成- これらのメソッドを生成
#active_if
- block引数を
@active_if
に代入する
- block引数を
#active?
@active_if
を評価する
#routes
- block引数を
@routes
に代入する
- block引数を
#helpers
- block引数を
ArticlesPageAbTest::Helper
で評価する
- block引数を
ソースコード: https://github.com/Jun0kada/limited_release/blob/master/lib/limited_release/feature.rb
2. routesにappendして先にroutingマッチさせる
1で@routes
に代入したroutesの定義を、
Rails起動時にRails.application.routes.prepend
します。
この際、limited_release
のnamespace配下にすることで、
既存コードにあるcontrollerと衝突しないようになっています。(namespaceはconfigにて変更可能)
rake routes
で定義を確認すると、
このようにlimited_release
配下の定義が一番上にappendされています。
Helper | HTTP Verb | Path | Controller#Action |
---|---|---|---|
limited_release_articles_path | GET | /articles(.:format) | limited_release/articles#index |
articles_path | GET | /articles(.:format) | articles#index |
これにより、/articles
というpathでリクエストが来ると、
limited_release/articles
controllerのindexアクションが呼ばれます
ソースコード: https://github.com/Jun0kada/limited_release/blob/master/lib/limited_release/railtie.rb#L6-L12
3. /articles
にリクエストが来る
リクエストが来たcontrollerがlimited_release
namespace配下の場合、下記が実行されます。
3-1. around_actionでエラーを潰す
around_actionの実装は、このようになっていて、
エラーが発生すると、rescueします。
def wrap_rescue begin yield rescue LimitedRelease::Controller::InvalidCondition headers['X-Cascade'] = 'pass' rescue => e headers['X-Cascade'] = 'pass' LimitedRelease.config.on_error.call(e) end end
headers['X-Cascade']
に'pass'
をセットすることで、
他のroutesを探索し、
マッチするcontrollerがあれば実行、
なければ、ActionController::RoutingError
がraiseされます。
この振る舞いはRailsの機能です。
この機能によって、
ページを条件によってオーバーライドしたり、新しいページを提供することができます。
また、
開発者がエラーを検知できるように
後述するLimitedRelease::Controller::InvalidCondition
以外のエラーの場合、
LimitedRelease.config.on_error.call(error)
を実行します(こちらの挙動はconfigで変更可能)
これにより、
限定公開機能のバグ等でエラーが発生してもユーザーに500エラーを返さず、
フォールバックした実行結果、もしくは404を返します。
ソースコード: https://github.com/Jun0kada/limited_release/blob/master/lib/limited_release/controller.rb#L27
3-2. 限定公開classを特定
controllerの名前から、限定公開classを特定します。
例:
'LimitedRelease::MyFeatureController'.split('::')[1].sub(/Controller\z/, '').classify.constantize => MyFeature
また、サンプルコードのようにclassを指定することもできます。
class LimitedRelease::ArticlesController < ::ArticlesController limited_release 'ArticlesPageAbTest' # ...
ソースコード: https://github.com/Jun0kada/limited_release/blob/master/lib/limited_release/controller.rb#L39
3-3. active条件を判定する
@active_if
に代入した条件をリクエストのコンテキストで評価し、有効でなかった場合、
先述したLimitedRelease::Controller::InvalidCondition
をraiseします。
ソースコード: https://github.com/Jun0kada/limited_release/blob/master/lib/limited_release/controller.rb#L43
3-4. 限定公開機能のヘルパーを読み込む
ArticlesPageAbTest::Helper
を読み込み、ヘルパーメソッドとして使えるようにします。
ソースコード: https://github.com/Jun0kada/limited_release/blob/master/lib/limited_release/controller.rb#L47
4. あとは通常と同じ
あとは通常のRailsと同じ動作を行います。
まとめ・使った感想
良かったこと
基本的にcontrollerやviewを作成するだけなので、
学習コストや実装工数が少なく済み、
1日に何個も限定公開機能をリリースできました。
また、
既存コードをまったくいじらないので、
バグを作ってしまう心配がないことも、心理的に◎でした!
悪かったこと、今後追加したい機能
ページの一部しか変更しない場合でも、
ページ全体をオーバーライドしないといけないので、
ちょっと無駄な感じがしました。
partial viewだけをオーバーライドできる機能などを追加したくなりました!
最後に
ぜひみなさん使ってみてください!
コントリビュート、issueもお待ちしております!
https://github.com/Jun0kada/limited_release