Add field test gem to establish a/b testing (#6283) [deploy]
* Initial field test spec work * Clean up specs * Fix codeclimage * Cleanups * Update names * Rename and remove need to pass logged in status * Leadup to prod: two experiments * Move test logic to controller and add tests Co-authored-by: Josh Puetz <josh@grorichpuetz.com>
This commit is contained in:
parent
3b2b9cb77b
commit
1dc1723709
21 changed files with 393 additions and 32 deletions
1
Gemfile
1
Gemfile
|
|
@ -41,6 +41,7 @@ gem "fast_jsonapi", "~> 1.5" # Serializer for Ruby objects
|
|||
gem "fastly", "~> 1.15" # Client library for the Fastly acceleration system
|
||||
gem "fastly-rails", "~> 0.8" # Fastly dynamic caching integration for Rails
|
||||
gem "feedjira", "~> 3.1" # A feed fetching and parsing library
|
||||
gem "field_test", "~> 0.3" # A/B testing
|
||||
gem "figaro", "~> 1.1" # Simple, Heroku-friendly Rails app configuration using ENV and a single YAML file
|
||||
gem "fog-aws", "~> 3.5" # 'fog' gem to support Amazon Web Services
|
||||
gem "front_matter_parser", "~> 0.2" # Parse a front matter from syntactically correct strings or files
|
||||
|
|
|
|||
|
|
@ -133,6 +133,7 @@ GEM
|
|||
bootsnap (1.4.6)
|
||||
msgpack (~> 1.0)
|
||||
brakeman (4.8.0)
|
||||
browser (2.7.1)
|
||||
buffer (0.1.3)
|
||||
addressable
|
||||
environs
|
||||
|
|
@ -223,6 +224,7 @@ GEM
|
|||
responders
|
||||
warden (~> 1.2.3)
|
||||
diff-lcs (1.3)
|
||||
distribution (0.7.3)
|
||||
docile (1.3.2)
|
||||
dogstatsd-ruby (4.7.0)
|
||||
domain_name (0.5.20190701)
|
||||
|
|
@ -314,6 +316,11 @@ GEM
|
|||
ffi-compiler (1.0.1)
|
||||
ffi (>= 1.0.0)
|
||||
rake
|
||||
field_test (0.3.1)
|
||||
activerecord (>= 5)
|
||||
browser (~> 2.0)
|
||||
distribution
|
||||
railties (>= 5)
|
||||
figaro (1.1.1)
|
||||
thor (~> 0.14)
|
||||
fix-db-schema-conflicts (3.0.3)
|
||||
|
|
@ -891,6 +898,7 @@ DEPENDENCIES
|
|||
fastly (~> 1.15)
|
||||
fastly-rails (~> 0.8)
|
||||
feedjira (~> 3.1)
|
||||
field_test (~> 0.3)
|
||||
figaro (~> 1.1)
|
||||
fix-db-schema-conflicts (~> 3.0)
|
||||
fog-aws (~> 3.5)
|
||||
|
|
|
|||
|
|
@ -13,9 +13,27 @@ class Stories::FeedsController < ApplicationController
|
|||
feed.top_articles_by_timeframe(timeframe: params[:timeframe])
|
||||
elsif params[:timeframe] == Timeframer::LATEST_TIMEFRAME
|
||||
feed.latest_feed
|
||||
elsif user_signed_in?
|
||||
ab_test_user_signed_in_feed(feed)
|
||||
else
|
||||
feed.default_home_feed(user_signed_in: user_signed_in?)
|
||||
end
|
||||
ArticleDecorator.decorate_collection(stories)
|
||||
end
|
||||
|
||||
def ab_test_user_signed_in_feed(feed)
|
||||
test_variant = field_test(:user_home_feed, participant: current_user)
|
||||
case test_variant
|
||||
when "base"
|
||||
feed.default_home_feed(user_signed_in: true)
|
||||
when "more_random"
|
||||
feed.default_home_feed_with_more_randomness
|
||||
when "mix_base_and_more_random"
|
||||
feed.mix_default_and_more_random
|
||||
when "more_tag_weight"
|
||||
feed.more_tag_weight
|
||||
else
|
||||
feed.default_home_feed(user_signed_in: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -20,8 +20,9 @@ class Comment < ApplicationRecord
|
|||
validates :commentable_type, inclusion: { in: %w[Article PodcastEpisode] }
|
||||
validates :user_id, presence: true
|
||||
|
||||
after_create :after_create_checks
|
||||
after_commit :calculate_score
|
||||
after_create :after_create_checks
|
||||
after_create_commit :record_field_test_event
|
||||
after_commit :calculate_score
|
||||
after_update_commit :update_notifications, if: proc { |comment| comment.saved_changes.include? "body_markdown" }
|
||||
after_save :bust_cache
|
||||
after_save :synchronous_bust
|
||||
|
|
@ -304,4 +305,8 @@ class Comment < ApplicationRecord
|
|||
def permissions
|
||||
errors.add(:commentable_id, "is not valid.") if commentable_type == "Article" && !commentable.published
|
||||
end
|
||||
|
||||
def record_field_test_event
|
||||
Users::RecordFieldTestEventWorker.perform_async(user_id, :user_home_feed, "user_creates_comment")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ class PageView < ApplicationRecord
|
|||
belongs_to :article
|
||||
|
||||
before_create :extract_domain_and_path
|
||||
after_create_commit :record_field_test_event
|
||||
|
||||
private
|
||||
|
||||
|
|
@ -25,4 +26,10 @@ class PageView < ApplicationRecord
|
|||
def article_tags
|
||||
article.decorate.cached_tag_list_array
|
||||
end
|
||||
|
||||
def record_field_test_event
|
||||
return unless user_id
|
||||
|
||||
Users::RecordFieldTestEventWorker.perform_async(user_id, :user_home_feed, "user_views_article_four_days_in_week")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ class Reaction < ApplicationRecord
|
|||
validate :permissions
|
||||
|
||||
before_save :assign_points
|
||||
after_create_commit :record_field_test_event
|
||||
after_commit :async_bust, :bust_reactable_cache, :update_reactable
|
||||
after_save :index_to_algolia
|
||||
after_save :touch_user
|
||||
|
|
@ -179,4 +180,8 @@ class Reaction < ApplicationRecord
|
|||
def negative?
|
||||
category == "vomit" || category == "thumbsdown"
|
||||
end
|
||||
|
||||
def record_field_test_event
|
||||
Users::RecordFieldTestEventWorker.perform_async(user_id, :user_home_feed, "user_creates_reaction")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ module Articles
|
|||
@number_of_articles = number_of_articles
|
||||
@page = page
|
||||
@tag = tag
|
||||
@randomness = 3 # default number for randomly adjusting feed
|
||||
@tag_weight = 1 # default weight tags play in rankings
|
||||
end
|
||||
|
||||
def published_articles_by_tag
|
||||
|
|
@ -25,28 +27,41 @@ module Articles
|
|||
page(@page).per(@number_of_articles)
|
||||
end
|
||||
|
||||
def default_home_feed_and_featured_story(user_signed_in: false)
|
||||
hot_stories = published_articles_by_tag.
|
||||
where("score > ? OR featured = ?", 9, true).
|
||||
order("hotness_score DESC")
|
||||
featured_story = hot_stories.where.not(main_image: nil).first
|
||||
if user_signed_in
|
||||
offset = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 3, 3, 4, 5, 6, 7, 8, 9, 10, 11].sample # random offset, weighted more towards zero
|
||||
hot_stories = hot_stories.offset(offset)
|
||||
new_stories = Article.published.
|
||||
where("published_at > ? AND score > ?", rand(2..6).hours.ago, -15).
|
||||
limited_column_select.order("published_at DESC").limit(rand(15..80))
|
||||
hot_stories = hot_stories.to_a + new_stories.to_a
|
||||
end
|
||||
hot_stories = rank_and_sort_articles(hot_stories) if @user
|
||||
def default_home_feed_and_featured_story(user_signed_in: false, ranking: true)
|
||||
featured_story, hot_stories = globally_cached_hot_articles(user_signed_in)
|
||||
hot_stories = rank_and_sort_articles(hot_stories) if @user && ranking
|
||||
[featured_story, hot_stories]
|
||||
end
|
||||
|
||||
# Test variation: Base
|
||||
def default_home_feed(user_signed_in: false)
|
||||
_featured_story, stories = default_home_feed_and_featured_story(user_signed_in: user_signed_in)
|
||||
_featured_story, stories = default_home_feed_and_featured_story(user_signed_in: user_signed_in, ranking: true)
|
||||
stories
|
||||
end
|
||||
|
||||
# Test variation: More random
|
||||
def default_home_feed_with_more_randomness
|
||||
@randomness = 7
|
||||
_featured_story, stories = default_home_feed_and_featured_story(user_signed_in: true)
|
||||
stories
|
||||
end
|
||||
|
||||
# Test variation: tags make bigger impact
|
||||
def more_tag_weight
|
||||
@tag_weight = 2
|
||||
_featured_story, stories = default_home_feed_and_featured_story(user_signed_in: true)
|
||||
stories
|
||||
end
|
||||
|
||||
# Test variation: Base half the time, more random other half. Varies on impressions.
|
||||
def mix_default_and_more_random
|
||||
if rand(2) == 1
|
||||
default_home_feed(user_signed_in: true)
|
||||
else
|
||||
default_home_feed_with_more_randomness
|
||||
end
|
||||
end
|
||||
|
||||
def rank_and_sort_articles(articles)
|
||||
ranked_articles = articles.each_with_object({}) do |article, result|
|
||||
article_points = score_single_article(article)
|
||||
|
|
@ -76,7 +91,7 @@ module Articles
|
|||
|
||||
article_tags = article.decorate.cached_tag_list_array
|
||||
@user.decorate.cached_followed_tags.sum do |tag|
|
||||
article_tags.include?(tag.name) ? tag.points : 0
|
||||
article_tags.include?(tag.name) ? tag.points * @tag_weight : 0
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -85,22 +100,35 @@ module Articles
|
|||
end
|
||||
|
||||
def score_randomness
|
||||
random_number = rand
|
||||
if random_number < 0.3
|
||||
3
|
||||
elsif random_number >= 0.3 && random_number < 0.6
|
||||
6
|
||||
else
|
||||
0
|
||||
end
|
||||
rand(3) * @randomness
|
||||
end
|
||||
|
||||
def score_language(article)
|
||||
@user&.preferred_languages_array&.include?(article.language || "en") ? 1 : -10
|
||||
@user&.preferred_languages_array&.include?(article.language || "en") ? 1 : -15
|
||||
end
|
||||
|
||||
def score_experience_level(article)
|
||||
- ((article.experience_level_rating - (@user&.experience_level || 5).abs) / 2)
|
||||
end
|
||||
|
||||
def globally_cached_hot_articles(user_signed_in)
|
||||
# If these query is shared by the all users and fetched often, we can cache it and fetch cold
|
||||
# only every x seconds.
|
||||
Rails.cache.fetch("globally-cached-hot-articles-#{user_signed_in}", expires_in: 20.seconds) do
|
||||
hot_stories = published_articles_by_tag.
|
||||
where("score > ? OR featured = ?", 9, true).
|
||||
order("hotness_score DESC")
|
||||
featured_story = hot_stories.where.not(main_image: nil).first
|
||||
if user_signed_in
|
||||
offset = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 3, 3, 4, 5, 6, 7, 8, 9, 10, 11].sample # random offset, weighted more towards zero
|
||||
hot_stories = hot_stories.offset(offset)
|
||||
new_stories = Article.published.
|
||||
where("published_at > ? AND score > ?", rand(2..6).hours.ago, -15).
|
||||
limited_column_select.order("published_at DESC").limit(rand(15..80))
|
||||
hot_stories = hot_stories.to_a + new_stories.to_a
|
||||
end
|
||||
[featured_story, hot_stories.to_a]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@
|
|||
!url.href.includes('/robots.txt') && // Skip robots for web crawlers
|
||||
!url.href.includes('/shell_') && // Don't fetch for shell.
|
||||
!url.href.includes('/sidekiq') && // Skip for Sidekiq dashboard
|
||||
!url.href.includes('/abtests') && // Skip for field_test dashboard
|
||||
!url.href.includes('/social_previews') && // Skip for social previews
|
||||
!url.href.includes('/users/auth') && // Don't run on authentication.
|
||||
!url.href.includes('/enter') && // Don't run on registration.
|
||||
|
|
|
|||
29
app/workers/users/record_field_test_event_worker.rb
Normal file
29
app/workers/users/record_field_test_event_worker.rb
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
module Users
|
||||
class RecordFieldTestEventWorker
|
||||
include Sidekiq::Worker
|
||||
include FieldTest::Helpers
|
||||
|
||||
sidekiq_options queue: :low_priority, retry: 10
|
||||
|
||||
def perform(user_id, experiment, goal)
|
||||
user = User.find(user_id)
|
||||
|
||||
if goal == "user_views_article_four_days_in_week"
|
||||
determine_pageview_goal(user, experiment)
|
||||
else
|
||||
field_test_converted(experiment, participant: user, goal: goal)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def determine_pageview_goal(user, experiment)
|
||||
past_week_page_view_counts = user.page_views.where("created_at > ?", 7.days.ago).
|
||||
group("DATE(created_at)").count.values
|
||||
past_week_page_view_counts.delete(0)
|
||||
return unless past_week_page_view_counts.size > 3
|
||||
|
||||
field_test_converted(experiment, participant: user, goal: "user_views_article_four_days_in_week")
|
||||
end
|
||||
end
|
||||
end
|
||||
25
config/field_test.yml
Normal file
25
config/field_test.yml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
experiments:
|
||||
user_home_feed: # Home feed collection for logged in user
|
||||
variants:
|
||||
- base
|
||||
- more_random
|
||||
- more_tag_weight
|
||||
- mix_base_more_random
|
||||
weights:
|
||||
- 65
|
||||
- 10
|
||||
- 10
|
||||
- 15
|
||||
goals:
|
||||
- user_creates_comment
|
||||
- user_creates_reaction
|
||||
- user_views_article_four_days_in_week
|
||||
exclude:
|
||||
bots: true
|
||||
|
||||
# Dashboard performance
|
||||
cache: true
|
||||
|
||||
# For logged-in users we'll explicitely pass participant.
|
||||
# If we experiment on non-logged in, we don't want to collect cookies anyway.
|
||||
cookies: false
|
||||
|
|
@ -18,6 +18,7 @@ Rails.application.routes.draw do
|
|||
use Rack::Protection, origin_whitelist: ["https://dev.to"] # resolve Rack Protection HttpOrigin
|
||||
end
|
||||
mount Sidekiq::Web => "/sidekiq"
|
||||
mount FieldTest::Engine, at: "abtests"
|
||||
end
|
||||
|
||||
devise_scope :user do
|
||||
|
|
|
|||
16
db/migrate/20200221184007_create_field_test_memberships.rb
Normal file
16
db/migrate/20200221184007_create_field_test_memberships.rb
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
class CreateFieldTestMemberships < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :field_test_memberships do |t|
|
||||
t.string :participant_type
|
||||
t.string :participant_id
|
||||
t.string :experiment
|
||||
t.string :variant
|
||||
t.timestamp :created_at
|
||||
t.boolean :converted, default: false
|
||||
end
|
||||
|
||||
add_index :field_test_memberships, [:participant_type, :participant_id, :experiment],
|
||||
unique: true, name: "index_field_test_memberships_on_participant"
|
||||
add_index :field_test_memberships, [:experiment, :created_at]
|
||||
end
|
||||
end
|
||||
9
db/migrate/20200222164815_create_field_test_events.rb
Normal file
9
db/migrate/20200222164815_create_field_test_events.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
class CreateFieldTestEvents < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :field_test_events do |t|
|
||||
t.references :field_test_membership
|
||||
t.string :name
|
||||
t.timestamp :created_at
|
||||
end
|
||||
end
|
||||
end
|
||||
18
db/schema.rb
18
db/schema.rb
|
|
@ -433,6 +433,24 @@ ActiveRecord::Schema.define(version: 2020_02_27_214321) do
|
|||
t.index ["reporter_id"], name: "index_feedback_messages_on_reporter_id"
|
||||
end
|
||||
|
||||
create_table "field_test_events", force: :cascade do |t|
|
||||
t.datetime "created_at"
|
||||
t.bigint "field_test_membership_id"
|
||||
t.string "name"
|
||||
t.index ["field_test_membership_id"], name: "index_field_test_events_on_field_test_membership_id"
|
||||
end
|
||||
|
||||
create_table "field_test_memberships", force: :cascade do |t|
|
||||
t.boolean "converted", default: false
|
||||
t.datetime "created_at"
|
||||
t.string "experiment"
|
||||
t.string "participant_id"
|
||||
t.string "participant_type"
|
||||
t.string "variant"
|
||||
t.index ["experiment", "created_at"], name: "index_field_test_memberships_on_experiment_and_created_at"
|
||||
t.index ["participant_type", "participant_id", "experiment"], name: "index_field_test_memberships_on_participant", unique: true
|
||||
end
|
||||
|
||||
create_table "follows", id: :serial, force: :cascade do |t|
|
||||
t.boolean "blocked", default: false, null: false
|
||||
t.datetime "created_at"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,16 @@ RSpec.describe "Comments", type: :request do
|
|||
let(:article) { create(:article, user_id: user.id) }
|
||||
let(:podcast) { create(:podcast) }
|
||||
let(:podcast_episode) { create(:podcast_episode, podcast_id: podcast.id) }
|
||||
let(:base_comment_params) do
|
||||
{
|
||||
comment: {
|
||||
commentable_id: article.id,
|
||||
commentable_type: "Article",
|
||||
user_id: user.id,
|
||||
body_markdown: "New comment #{rand(10)}"
|
||||
}
|
||||
}
|
||||
end
|
||||
let!(:comment) do
|
||||
create(:comment,
|
||||
commentable_id: article.id,
|
||||
|
|
@ -290,6 +300,20 @@ RSpec.describe "Comments", type: :request do
|
|||
end
|
||||
end
|
||||
|
||||
describe "POST /comments" do
|
||||
context "when part of field test" do
|
||||
before do
|
||||
sign_in user
|
||||
allow(Users::RecordFieldTestEventWorker).to receive(:perform_async)
|
||||
end
|
||||
|
||||
it "converts field test" do
|
||||
post "/comments", params: base_comment_params
|
||||
expect(Users::RecordFieldTestEventWorker).to have_received(:perform_async).with(user.id, :user_home_feed, "user_creates_comment")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "PATCH /comments/:comment_id/hide" do
|
||||
include_examples "PATCH /comments/:comment_id/hide or unhide", path: "hide", hidden: "true"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -37,6 +37,21 @@ RSpec.describe "PageViews", type: :request do
|
|||
end
|
||||
end
|
||||
|
||||
context "when part of field test" do
|
||||
before do
|
||||
sign_in user
|
||||
allow(Users::RecordFieldTestEventWorker).to receive(:perform_async)
|
||||
end
|
||||
|
||||
it "converts field test" do
|
||||
post "/page_views", params: {
|
||||
article_id: article.id,
|
||||
referrer: "test"
|
||||
}
|
||||
expect(Users::RecordFieldTestEventWorker).to have_received(:perform_async).with(user.id, :user_home_feed, "user_views_article_four_days_in_week")
|
||||
end
|
||||
end
|
||||
|
||||
context "when user not signed in" do
|
||||
it "creates a new page view" do
|
||||
post "/page_views", params: {
|
||||
|
|
|
|||
|
|
@ -181,5 +181,17 @@ RSpec.describe "Reactions", type: :request do
|
|||
expect(Reaction.all.size).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
context "when part of field test" do
|
||||
before do
|
||||
sign_in user
|
||||
allow(Users::RecordFieldTestEventWorker).to receive(:perform_async)
|
||||
end
|
||||
|
||||
it "converts field test" do
|
||||
post "/reactions", params: article_params
|
||||
expect(Users::RecordFieldTestEventWorker).to have_received(:perform_async).with(user.id, :user_home_feed, "user_creates_reaction")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -91,5 +91,25 @@ RSpec.describe "Stories::FeedsIndex", type: :request do
|
|||
expect(feed_service).to have_received(:latest_feed)
|
||||
end
|
||||
end
|
||||
|
||||
context "when there are no params passed (base feed) and user is signed in" do
|
||||
before do
|
||||
sign_in user
|
||||
end
|
||||
|
||||
it "sets a field test" do
|
||||
get "/stories/feed"
|
||||
expect(FieldTest::Membership.all.size).to be(1)
|
||||
expect(FieldTest::Membership.last.participant_id).to eq(user.id.to_s)
|
||||
expect(FieldTest::Membership.last.experiment).to eq("user_home_feed")
|
||||
end
|
||||
end
|
||||
|
||||
context "when there are no params passed (base feed) and user is signed not in" do
|
||||
it "sets a field test" do
|
||||
get "/stories/feed"
|
||||
expect(FieldTest::Membership.all.size).to be(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -85,6 +85,30 @@ RSpec.describe Articles::Feed, type: :service do
|
|||
expect(featured_story).to eq(hot_story)
|
||||
end
|
||||
end
|
||||
|
||||
context "when ranking is true" do
|
||||
it "performs article ranking" do
|
||||
allow(feed).to receive(:rank_and_sort_articles).and_call_original
|
||||
feed.default_home_feed_and_featured_story(ranking: true)
|
||||
expect(feed).to have_received(:rank_and_sort_articles)
|
||||
end
|
||||
end
|
||||
|
||||
context "when ranking is false" do
|
||||
it "does not perform article ranking" do
|
||||
allow(feed).to receive(:rank_and_sort_articles).and_call_original
|
||||
feed.default_home_feed_and_featured_story(ranking: false)
|
||||
expect(feed).not_to have_received(:rank_and_sort_articles)
|
||||
end
|
||||
end
|
||||
|
||||
context "when ranking not passed" do
|
||||
it "performs article ranking" do
|
||||
allow(feed).to receive(:rank_and_sort_articles).and_call_original
|
||||
feed.default_home_feed_and_featured_story
|
||||
expect(feed).to have_received(:rank_and_sort_articles)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#default_home_feed" do
|
||||
|
|
@ -115,6 +139,36 @@ RSpec.describe Articles::Feed, type: :service do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#default_home_feed_with_more_randomness" do
|
||||
let!(:new_story) { create(:article, published_at: 10.minutes.ago, score: 10) }
|
||||
let(:stories) { feed.default_home_feed_with_more_randomness }
|
||||
|
||||
it "includes stories from between 2 and 6 hours ago" do
|
||||
expect(stories).not_to include(old_story)
|
||||
expect(stories).to include(new_story)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#mix_default_and_more_random" do
|
||||
let!(:new_story) { create(:article, published_at: 10.minutes.ago, score: 10) }
|
||||
let(:stories) { feed.mix_default_and_more_random }
|
||||
|
||||
it "includes stories from between 2 and 6 hours ago" do
|
||||
expect(stories).not_to include(old_story)
|
||||
expect(stories).to include(new_story)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#more_tag_weight" do
|
||||
let!(:new_story) { create(:article, published_at: 10.minutes.ago, score: 10) }
|
||||
let(:stories) { feed.more_tag_weight }
|
||||
|
||||
it "includes stories from between 2 and 6 hours ago" do
|
||||
expect(stories).not_to include(old_story)
|
||||
expect(stories).to include(new_story)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#score_followed_user" do
|
||||
context "when article is written by a followed user" do
|
||||
before { user.follow(article.user) }
|
||||
|
|
@ -161,21 +215,21 @@ RSpec.describe Articles::Feed, type: :service do
|
|||
describe "#score_randomness" do
|
||||
context "when random number is less than 0.6 but greater than 0.3" do
|
||||
it "returns 6" do
|
||||
allow(feed).to receive(:rand).and_return(0.5)
|
||||
allow(feed).to receive(:rand).and_return(2)
|
||||
expect(feed.score_randomness).to eq 6
|
||||
end
|
||||
end
|
||||
|
||||
context "when random number is less than 0.3" do
|
||||
it "returns 3" do
|
||||
allow(feed).to receive(:rand).and_return(0.1)
|
||||
allow(feed).to receive(:rand).and_return(1)
|
||||
expect(feed.score_randomness).to eq 3
|
||||
end
|
||||
end
|
||||
|
||||
context "when random number is greater than 0.6" do
|
||||
it "returns 0" do
|
||||
allow(feed).to receive(:rand).and_return(0.9)
|
||||
allow(feed).to receive(:rand).and_return(0)
|
||||
expect(feed.score_randomness).to eq 0
|
||||
end
|
||||
end
|
||||
|
|
@ -192,7 +246,7 @@ RSpec.describe Articles::Feed, type: :service do
|
|||
before { article.language = "de" }
|
||||
|
||||
it "returns a score of -10" do
|
||||
expect(feed.score_language(article)).to eq(-10)
|
||||
expect(feed.score_language(article)).to eq(-15)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ RSpec.shared_examples "#enqueues_on_correct_queue" do |queue_name, args|
|
|||
describe "#perform_async" do
|
||||
it "enqueues the job" do
|
||||
Sidekiq::Testing.fake!
|
||||
|
||||
expect do
|
||||
described_class.perform_async(args)
|
||||
end.to change { Sidekiq::Queues[queue_name].size }.by(1)
|
||||
|
|
|
|||
66
spec/workers/users/record_field_test_event_worker_spec.rb
Normal file
66
spec/workers/users/record_field_test_event_worker_spec.rb
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Users::RecordFieldTestEventWorker, type: :worker do
|
||||
include_examples "#enqueues_on_correct_queue", "low_priority", 1
|
||||
include FieldTest::Helpers
|
||||
|
||||
describe "#perform" do
|
||||
let(:worker) { subject }
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
context "with user who is part of field test" do
|
||||
before do
|
||||
field_test(:user_home_feed, participant: user)
|
||||
end
|
||||
|
||||
it "records user_creates_reaction field test conversion" do
|
||||
worker.perform(user.id, "user_home_feed", "user_creates_reaction")
|
||||
expect(FieldTest::Event.last.field_test_membership.participant_id).to eq(user.id.to_s)
|
||||
expect(FieldTest::Event.last.name).to eq("user_creates_reaction")
|
||||
end
|
||||
|
||||
it "records user_creates_comment field test conversion" do
|
||||
worker.perform(user.id, "user_home_feed", "user_creates_comment")
|
||||
expect(FieldTest::Event.last.field_test_membership.participant_id).to eq(user.id.to_s)
|
||||
expect(FieldTest::Event.last.name).to eq("user_creates_comment")
|
||||
end
|
||||
|
||||
it "records user_views_article_four_days_in_week field test conversion if qualifies" do
|
||||
7.times do |n|
|
||||
create(:page_view, user_id: user.id, created_at: n.day.ago)
|
||||
end
|
||||
worker.perform(user.id, "user_home_feed", "user_views_article_four_days_in_week")
|
||||
expect(FieldTest::Event.last.field_test_membership.participant_id).to eq(user.id.to_s)
|
||||
expect(FieldTest::Event.last.name).to eq("user_views_article_four_days_in_week")
|
||||
end
|
||||
|
||||
it "does not record user_views_article_four_days_in_week field test conversion if not qualifying" do
|
||||
2.times do |n|
|
||||
create(:page_view, user_id: user.id, created_at: n.day.ago)
|
||||
end
|
||||
worker.perform(user.id, "user_home_feed", "user_views_article_four_days_in_week")
|
||||
expect(FieldTest::Event.all.size).to be(0)
|
||||
end
|
||||
end
|
||||
|
||||
context "with user who is not part of field test" do
|
||||
it "records user_creates_reaction field test conversion" do
|
||||
worker.perform(user.id, "user_home_feed", "user_creates_reaction")
|
||||
expect(FieldTest::Event.all.size).to be(0)
|
||||
end
|
||||
|
||||
it "records user_creates_comment field test conversion" do
|
||||
worker.perform(user.id, "user_home_feed", "user_creates_comment")
|
||||
expect(FieldTest::Event.all.size).to be(0)
|
||||
end
|
||||
|
||||
it "records user_views_article_four_days_in_week field test conversion if qualifies" do
|
||||
7.times do |n|
|
||||
create(:page_view, user_id: user.id, created_at: n.day.ago)
|
||||
end
|
||||
expect(FieldTest::Event.all.size).to be(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue