From 1dc17237098a62682c59774780c7e37a10d6b43d Mon Sep 17 00:00:00 2001 From: Ben Halpern Date: Tue, 3 Mar 2020 10:21:29 -0500 Subject: [PATCH] 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 --- Gemfile | 1 + Gemfile.lock | 8 ++ app/controllers/stories/feeds_controller.rb | 18 +++++ app/models/comment.rb | 9 ++- app/models/page_view.rb | 7 ++ app/models/reaction.rb | 5 ++ app/services/articles/feed.rb | 78 +++++++++++++------ app/views/service_worker/index.js.erb | 1 + .../users/record_field_test_event_worker.rb | 29 +++++++ config/field_test.yml | 25 ++++++ config/routes.rb | 1 + ...221184007_create_field_test_memberships.rb | 16 ++++ ...20200222164815_create_field_test_events.rb | 9 +++ db/schema.rb | 18 +++++ spec/requests/comments_spec.rb | 24 ++++++ spec/requests/page_views_spec.rb | 15 ++++ spec/requests/reactions_spec.rb | 12 +++ spec/requests/stories/feeds_spec.rb | 20 +++++ spec/services/articles/feed_spec.rb | 62 ++++++++++++++- .../enqueues_on_correct_queue.rb | 1 - .../record_field_test_event_worker_spec.rb | 66 ++++++++++++++++ 21 files changed, 393 insertions(+), 32 deletions(-) create mode 100644 app/workers/users/record_field_test_event_worker.rb create mode 100644 config/field_test.yml create mode 100644 db/migrate/20200221184007_create_field_test_memberships.rb create mode 100644 db/migrate/20200222164815_create_field_test_events.rb create mode 100644 spec/workers/users/record_field_test_event_worker_spec.rb diff --git a/Gemfile b/Gemfile index e221531a3..5a8387767 100644 --- a/Gemfile +++ b/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 diff --git a/Gemfile.lock b/Gemfile.lock index 51b353031..b19456311 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/app/controllers/stories/feeds_controller.rb b/app/controllers/stories/feeds_controller.rb index df315eea6..eabbe9d92 100644 --- a/app/controllers/stories/feeds_controller.rb +++ b/app/controllers/stories/feeds_controller.rb @@ -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 diff --git a/app/models/comment.rb b/app/models/comment.rb index 557cccc77..310572dfc 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -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 diff --git a/app/models/page_view.rb b/app/models/page_view.rb index 78611d5f7..79f1f4a0c 100644 --- a/app/models/page_view.rb +++ b/app/models/page_view.rb @@ -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 diff --git a/app/models/reaction.rb b/app/models/reaction.rb index 7f350a21a..bb414a830 100644 --- a/app/models/reaction.rb +++ b/app/models/reaction.rb @@ -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 diff --git a/app/services/articles/feed.rb b/app/services/articles/feed.rb index 79db59fcc..3d5c8fe94 100644 --- a/app/services/articles/feed.rb +++ b/app/services/articles/feed.rb @@ -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 diff --git a/app/views/service_worker/index.js.erb b/app/views/service_worker/index.js.erb index 3f823b095..8b9cf6526 100644 --- a/app/views/service_worker/index.js.erb +++ b/app/views/service_worker/index.js.erb @@ -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. diff --git a/app/workers/users/record_field_test_event_worker.rb b/app/workers/users/record_field_test_event_worker.rb new file mode 100644 index 000000000..9e471a5ea --- /dev/null +++ b/app/workers/users/record_field_test_event_worker.rb @@ -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 diff --git a/config/field_test.yml b/config/field_test.yml new file mode 100644 index 000000000..eea114f48 --- /dev/null +++ b/config/field_test.yml @@ -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 diff --git a/config/routes.rb b/config/routes.rb index 2d9d8473c..740953ebc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/db/migrate/20200221184007_create_field_test_memberships.rb b/db/migrate/20200221184007_create_field_test_memberships.rb new file mode 100644 index 000000000..f1de8cc5c --- /dev/null +++ b/db/migrate/20200221184007_create_field_test_memberships.rb @@ -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 diff --git a/db/migrate/20200222164815_create_field_test_events.rb b/db/migrate/20200222164815_create_field_test_events.rb new file mode 100644 index 000000000..4eb260038 --- /dev/null +++ b/db/migrate/20200222164815_create_field_test_events.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 75fa7870f..cdfe4408d 100644 --- a/db/schema.rb +++ b/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" diff --git a/spec/requests/comments_spec.rb b/spec/requests/comments_spec.rb index 91ed6f255..90634ab4b 100644 --- a/spec/requests/comments_spec.rb +++ b/spec/requests/comments_spec.rb @@ -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 diff --git a/spec/requests/page_views_spec.rb b/spec/requests/page_views_spec.rb index 15441db20..3c096c8a8 100644 --- a/spec/requests/page_views_spec.rb +++ b/spec/requests/page_views_spec.rb @@ -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: { diff --git a/spec/requests/reactions_spec.rb b/spec/requests/reactions_spec.rb index 5ea51cb78..af02f6312 100644 --- a/spec/requests/reactions_spec.rb +++ b/spec/requests/reactions_spec.rb @@ -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 diff --git a/spec/requests/stories/feeds_spec.rb b/spec/requests/stories/feeds_spec.rb index 118392eae..b3b4bc46c 100644 --- a/spec/requests/stories/feeds_spec.rb +++ b/spec/requests/stories/feeds_spec.rb @@ -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 diff --git a/spec/services/articles/feed_spec.rb b/spec/services/articles/feed_spec.rb index 095770b69..61f73e7f9 100644 --- a/spec/services/articles/feed_spec.rb +++ b/spec/services/articles/feed_spec.rb @@ -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 diff --git a/spec/workers/shared_examples/enqueues_on_correct_queue.rb b/spec/workers/shared_examples/enqueues_on_correct_queue.rb index badb8b37f..4c54de967 100644 --- a/spec/workers/shared_examples/enqueues_on_correct_queue.rb +++ b/spec/workers/shared_examples/enqueues_on_correct_queue.rb @@ -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) diff --git a/spec/workers/users/record_field_test_event_worker_spec.rb b/spec/workers/users/record_field_test_event_worker_spec.rb new file mode 100644 index 000000000..c012c1bb1 --- /dev/null +++ b/spec/workers/users/record_field_test_event_worker_spec.rb @@ -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