Commit b1f14036 authored by Jay's avatar Jay Committed by Doug Stull

Ensure variant when creating ExperimentSubjects

parent 9d8d82ec
...@@ -32,8 +32,8 @@ class ApplicationExperiment < Gitlab::Experiment # rubocop:disable Gitlab/Namesp ...@@ -32,8 +32,8 @@ class ApplicationExperiment < Gitlab::Experiment # rubocop:disable Gitlab/Namesp
subject = value[:namespace] || value[:group] || value[:project] || value[:user] || value[:actor] subject = value[:namespace] || value[:group] || value[:project] || value[:user] || value[:actor]
return unless ExperimentSubject.valid_subject?(subject) return unless ExperimentSubject.valid_subject?(subject)
variant = :experimental if @variant_name != :control variant_name = :experimental if variant&.name != 'control'
Experiment.add_subject(name, variant: variant || :control, subject: subject) Experiment.add_subject(name, variant: variant_name || :control, subject: subject)
end end
def record! def record!
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe ApplicationExperiment, :experiment do RSpec.describe ApplicationExperiment, :experiment do
subject { described_class.new('namespaced/stub', **context) } subject(:application_experiment) { described_class.new('namespaced/stub', **context) }
let(:context) { {} } let(:context) { {} }
let(:feature_definition) { { name: 'namespaced_stub', type: 'experiment', default_enabled: false } } let(:feature_definition) { { name: 'namespaced_stub', type: 'experiment', default_enabled: false } }
...@@ -15,7 +15,7 @@ RSpec.describe ApplicationExperiment, :experiment do ...@@ -15,7 +15,7 @@ RSpec.describe ApplicationExperiment, :experiment do
end end
before do before do
allow(subject).to receive(:enabled?).and_return(true) allow(application_experiment).to receive(:enabled?).and_return(true)
end end
it "doesn't raise an exception without a defined control" do it "doesn't raise an exception without a defined control" do
...@@ -26,7 +26,7 @@ RSpec.describe ApplicationExperiment, :experiment do ...@@ -26,7 +26,7 @@ RSpec.describe ApplicationExperiment, :experiment do
describe "#enabled?" do describe "#enabled?" do
before do before do
allow(subject).to receive(:enabled?).and_call_original allow(application_experiment).to receive(:enabled?).and_call_original
allow(Feature::Definition).to receive(:get).and_return('_instance_') allow(Feature::Definition).to receive(:get).and_return('_instance_')
allow(Gitlab).to receive(:dev_env_or_com?).and_return(true) allow(Gitlab).to receive(:dev_env_or_com?).and_return(true)
...@@ -34,25 +34,25 @@ RSpec.describe ApplicationExperiment, :experiment do ...@@ -34,25 +34,25 @@ RSpec.describe ApplicationExperiment, :experiment do
end end
it "is enabled when all criteria are met" do it "is enabled when all criteria are met" do
expect(subject).to be_enabled expect(application_experiment).to be_enabled
end end
it "isn't enabled if the feature definition doesn't exist" do it "isn't enabled if the feature definition doesn't exist" do
expect(Feature::Definition).to receive(:get).with('namespaced_stub').and_return(nil) expect(Feature::Definition).to receive(:get).with('namespaced_stub').and_return(nil)
expect(subject).not_to be_enabled expect(application_experiment).not_to be_enabled
end end
it "isn't enabled if we're not in dev or dotcom environments" do it "isn't enabled if we're not in dev or dotcom environments" do
expect(Gitlab).to receive(:dev_env_or_com?).and_return(false) expect(Gitlab).to receive(:dev_env_or_com?).and_return(false)
expect(subject).not_to be_enabled expect(application_experiment).not_to be_enabled
end end
it "isn't enabled if the feature flag state is :off" do it "isn't enabled if the feature flag state is :off" do
expect(Feature).to receive(:get).with('namespaced_stub').and_return(double(state: :off)) expect(Feature).to receive(:get).with('namespaced_stub').and_return(double(state: :off))
expect(subject).not_to be_enabled expect(application_experiment).not_to be_enabled
end end
end end
...@@ -60,11 +60,11 @@ RSpec.describe ApplicationExperiment, :experiment do ...@@ -60,11 +60,11 @@ RSpec.describe ApplicationExperiment, :experiment do
let(:should_track) { true } let(:should_track) { true }
before do before do
allow(subject).to receive(:should_track?).and_return(should_track) allow(application_experiment).to receive(:should_track?).and_return(should_track)
end end
it "tracks the assignment", :snowplow do it "tracks the assignment", :snowplow do
subject.publish application_experiment.publish
expect_snowplow_event( expect_snowplow_event(
category: 'namespaced/stub', category: 'namespaced/stub',
...@@ -74,24 +74,24 @@ RSpec.describe ApplicationExperiment, :experiment do ...@@ -74,24 +74,24 @@ RSpec.describe ApplicationExperiment, :experiment do
end end
it "publishes to the client" do it "publishes to the client" do
expect(subject).to receive(:publish_to_client) expect(application_experiment).to receive(:publish_to_client)
subject.publish application_experiment.publish
end end
it "publishes to the database if we've opted for that" do it "publishes to the database if we've opted for that" do
subject.record! application_experiment.record!
expect(subject).to receive(:publish_to_database) expect(application_experiment).to receive(:publish_to_database)
subject.publish application_experiment.publish
end end
context 'when we should not track' do context 'when we should not track' do
let(:should_track) { false } let(:should_track) { false }
it 'does not track an event to Snowplow', :snowplow do it 'does not track an event to Snowplow', :snowplow do
subject.publish application_experiment.publish
expect_no_snowplow_event expect_no_snowplow_event
end end
...@@ -102,13 +102,13 @@ RSpec.describe ApplicationExperiment, :experiment do ...@@ -102,13 +102,13 @@ RSpec.describe ApplicationExperiment, :experiment do
signature = { key: '86208ac54ca798e11f127e8b23ec396a', variant: 'control' } signature = { key: '86208ac54ca798e11f127e8b23ec396a', variant: 'control' }
expect(Gon).to receive(:push).with({ experiment: { 'namespaced/stub' => hash_including(signature) } }, true) expect(Gon).to receive(:push).with({ experiment: { 'namespaced/stub' => hash_including(signature) } }, true)
subject.publish_to_client application_experiment.publish_to_client
end end
it "handles when Gon raises exceptions (like when it can't be pushed into)" do it "handles when Gon raises exceptions (like when it can't be pushed into)" do
expect(Gon).to receive(:push).and_raise(NoMethodError) expect(Gon).to receive(:push).and_raise(NoMethodError)
expect { subject.publish_to_client }.not_to raise_error expect { application_experiment.publish_to_client }.not_to raise_error
end end
context 'when we should not track' do context 'when we should not track' do
...@@ -117,7 +117,7 @@ RSpec.describe ApplicationExperiment, :experiment do ...@@ -117,7 +117,7 @@ RSpec.describe ApplicationExperiment, :experiment do
it 'returns early' do it 'returns early' do
expect(Gon).not_to receive(:push) expect(Gon).not_to receive(:push)
subject.publish_to_client application_experiment.publish_to_client
end end
end end
end end
...@@ -125,13 +125,15 @@ RSpec.describe ApplicationExperiment, :experiment do ...@@ -125,13 +125,15 @@ RSpec.describe ApplicationExperiment, :experiment do
describe '#publish_to_database' do describe '#publish_to_database' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
let(:publish_to_database) { application_experiment.publish_to_database }
shared_examples 'does not record to the database' do shared_examples 'does not record to the database' do
it 'does not create an experiment record' do it 'does not create an experiment record' do
expect { subject.publish_to_database }.not_to change(Experiment, :count) expect { publish_to_database }.not_to change(Experiment, :count)
end end
it 'does not create an experiment subject record' do it 'does not create an experiment subject record' do
expect { subject.publish_to_database }.not_to change(ExperimentSubject, :count) expect { publish_to_database }.not_to change(ExperimentSubject, :count)
end end
end end
...@@ -139,16 +141,16 @@ RSpec.describe ApplicationExperiment, :experiment do ...@@ -139,16 +141,16 @@ RSpec.describe ApplicationExperiment, :experiment do
let(:context) { { context_key => context_value } } let(:context) { { context_key => context_value } }
where(:context_key, :context_value, :object_type) do where(:context_key, :context_value, :object_type) do
:namespace | build(:namespace) | :namespace :namespace | build(:namespace, id: non_existing_record_id) | :namespace
:group | build(:namespace) | :namespace :group | build(:namespace, id: non_existing_record_id) | :namespace
:project | build(:project) | :project :project | build(:project, id: non_existing_record_id) | :project
:user | build(:user) | :user :user | build(:user, id: non_existing_record_id) | :user
:actor | build(:user) | :user :actor | build(:user, id: non_existing_record_id) | :user
end end
with_them do with_them do
it 'creates an experiment and experiment subject record' do it 'creates an experiment and experiment subject record' do
expect { subject.publish_to_database }.to change(Experiment, :count).by(1) expect { publish_to_database }.to change(Experiment, :count).by(1)
expect(Experiment.last.name).to eq('namespaced/stub') expect(Experiment.last.name).to eq('namespaced/stub')
expect(ExperimentSubject.last.send(object_type)).to eq(context[context_key]) expect(ExperimentSubject.last.send(object_type)).to eq(context[context_key])
...@@ -156,6 +158,16 @@ RSpec.describe ApplicationExperiment, :experiment do ...@@ -156,6 +158,16 @@ RSpec.describe ApplicationExperiment, :experiment do
end end
end end
context "when experiment hasn't ran" do
let(:context) { { user: create(:user) } }
it 'sets a variant on the experiment subject' do
publish_to_database
expect(ExperimentSubject.last.variant).to eq('control')
end
end
context 'when there is not a usable subject' do context 'when there is not a usable subject' do
let(:context) { { context_key => context_value } } let(:context) { { context_key => context_value } }
...@@ -183,15 +195,15 @@ RSpec.describe ApplicationExperiment, :experiment do ...@@ -183,15 +195,15 @@ RSpec.describe ApplicationExperiment, :experiment do
end end
it "doesn't track if we shouldn't track" do it "doesn't track if we shouldn't track" do
allow(subject).to receive(:should_track?).and_return(false) allow(application_experiment).to receive(:should_track?).and_return(false)
subject.track(:action) application_experiment.track(:action)
expect_no_snowplow_event expect_no_snowplow_event
end end
it "tracks the event with the expected arguments and merged contexts" do it "tracks the event with the expected arguments and merged contexts" do
subject.track(:action, property: '_property_', context: [fake_context]) application_experiment.track(:action, property: '_property_', context: [fake_context])
expect_snowplow_event( expect_snowplow_event(
category: 'namespaced/stub', category: 'namespaced/stub',
...@@ -233,7 +245,7 @@ RSpec.describe ApplicationExperiment, :experiment do ...@@ -233,7 +245,7 @@ RSpec.describe ApplicationExperiment, :experiment do
describe "#key_for" do describe "#key_for" do
it "generates MD5 hashes" do it "generates MD5 hashes" do
expect(subject.key_for(foo: :bar)).to eq('6f9ac12afdb9b58c2f19a136d09f9153') expect(application_experiment.key_for(foo: :bar)).to eq('6f9ac12afdb9b58c2f19a136d09f9153')
end end
end end
...@@ -256,26 +268,26 @@ RSpec.describe ApplicationExperiment, :experiment do ...@@ -256,26 +268,26 @@ RSpec.describe ApplicationExperiment, :experiment do
with_them do with_them do
it "returns the url or nil if invalid" do it "returns the url or nil if invalid" do
allow(Gitlab).to receive(:dev_env_or_com?).and_return(true) allow(Gitlab).to receive(:dev_env_or_com?).and_return(true)
expect(subject.process_redirect_url(url)).to eq(processed_url) expect(application_experiment.process_redirect_url(url)).to eq(processed_url)
end end
it "considers all urls invalid when not on dev or com" do it "considers all urls invalid when not on dev or com" do
allow(Gitlab).to receive(:dev_env_or_com?).and_return(false) allow(Gitlab).to receive(:dev_env_or_com?).and_return(false)
expect(subject.process_redirect_url(url)).to be_nil expect(application_experiment.process_redirect_url(url)).to be_nil
end end
end end
it "generates the correct urls based on where the engine was mounted" do it "generates the correct urls based on where the engine was mounted" do
url = Rails.application.routes.url_helpers.experiment_redirect_url(subject, url: 'https://docs.gitlab.com') url = Rails.application.routes.url_helpers.experiment_redirect_url(application_experiment, url: 'https://docs.gitlab.com')
expect(url).to include("/-/experiment/namespaced%2Fstub:#{subject.context.key}?https://docs.gitlab.com") expect(url).to include("/-/experiment/namespaced%2Fstub:#{application_experiment.context.key}?https://docs.gitlab.com")
end end
end end
context "when resolving variants" do context "when resolving variants" do
it "uses the default value as specified in the yaml" do it "uses the default value as specified in the yaml" do
expect(Feature).to receive(:enabled?).with('namespaced_stub', subject, type: :experiment, default_enabled: :yaml) expect(Feature).to receive(:enabled?).with('namespaced_stub', application_experiment, type: :experiment, default_enabled: :yaml)
expect(subject.variant.name).to eq('control') expect(application_experiment.variant.name).to eq('control')
end end
context "when rolled out to 100%" do context "when rolled out to 100%" do
...@@ -284,10 +296,10 @@ RSpec.describe ApplicationExperiment, :experiment do ...@@ -284,10 +296,10 @@ RSpec.describe ApplicationExperiment, :experiment do
end end
it "returns the first variant name" do it "returns the first variant name" do
subject.try(:variant1) {} application_experiment.try(:variant1) {}
subject.try(:variant2) {} application_experiment.try(:variant2) {}
expect(subject.variant.name).to eq('variant1') expect(application_experiment.variant.name).to eq('variant1')
end end
end end
end end
...@@ -298,18 +310,18 @@ RSpec.describe ApplicationExperiment, :experiment do ...@@ -298,18 +310,18 @@ RSpec.describe ApplicationExperiment, :experiment do
before do before do
allow(Gitlab::Experiment::Configuration).to receive(:cache).and_call_original allow(Gitlab::Experiment::Configuration).to receive(:cache).and_call_original
cache.clear(key: subject.name) cache.clear(key: application_experiment.name)
subject.use { } # setup the control application_experiment.use { } # setup the control
subject.try { } # setup the candidate application_experiment.try { } # setup the candidate
end end
it "caches the variant determined by the variant resolver" do it "caches the variant determined by the variant resolver" do
expect(subject.variant.name).to eq('candidate') # we should be in the experiment expect(application_experiment.variant.name).to eq('candidate') # we should be in the experiment
subject.run application_experiment.run
expect(subject.cache.read).to eq('candidate') expect(application_experiment.cache.read).to eq('candidate')
end end
it "doesn't cache a variant if we don't explicitly provide one" do it "doesn't cache a variant if we don't explicitly provide one" do
...@@ -320,11 +332,11 @@ RSpec.describe ApplicationExperiment, :experiment do ...@@ -320,11 +332,11 @@ RSpec.describe ApplicationExperiment, :experiment do
# the control. # the control.
stub_feature_flags(namespaced_stub: false) # simulate being not rolled out stub_feature_flags(namespaced_stub: false) # simulate being not rolled out
expect(subject.variant.name).to eq('control') # if we ask, it should be control expect(application_experiment.variant.name).to eq('control') # if we ask, it should be control
subject.run application_experiment.run
expect(subject.cache.read).to be_nil expect(application_experiment.cache.read).to be_nil
end end
it "caches a control variant if we assign it specifically" do it "caches a control variant if we assign it specifically" do
...@@ -332,27 +344,27 @@ RSpec.describe ApplicationExperiment, :experiment do ...@@ -332,27 +344,27 @@ RSpec.describe ApplicationExperiment, :experiment do
# that this context will always get the control variant unless we delete # that this context will always get the control variant unless we delete
# the field from the cache (or clear the entire experiment cache) -- or # the field from the cache (or clear the entire experiment cache) -- or
# write code that would specify a different variant. # write code that would specify a different variant.
subject.run(:control) application_experiment.run(:control)
expect(subject.cache.read).to eq('control') expect(application_experiment.cache.read).to eq('control')
end end
context "arbitrary attributes" do context "arbitrary attributes" do
before do before do
subject.cache.store.clear(key: subject.name + '_attrs') application_experiment.cache.store.clear(key: application_experiment.name + '_attrs')
end end
it "sets and gets attributes about an experiment" do it "sets and gets attributes about an experiment" do
subject.cache.attr_set(:foo, :bar) application_experiment.cache.attr_set(:foo, :bar)
expect(subject.cache.attr_get(:foo)).to eq('bar') expect(application_experiment.cache.attr_get(:foo)).to eq('bar')
end end
it "increments a value for an experiment" do it "increments a value for an experiment" do
expect(subject.cache.attr_get(:foo)).to be_nil expect(application_experiment.cache.attr_get(:foo)).to be_nil
expect(subject.cache.attr_inc(:foo)).to eq(1) expect(application_experiment.cache.attr_inc(:foo)).to eq(1)
expect(subject.cache.attr_inc(:foo)).to eq(2) expect(application_experiment.cache.attr_inc(:foo)).to eq(2)
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment