Commit 9893c544 authored by Mayra Cabrera's avatar Mayra Cabrera

Merge branch 'jejacks0n/update-gitlab-experiment-0.6.1' into 'master'

Update gitlab-experiment to 0.6.1

See merge request gitlab-org/gitlab!64551
parents ea26fad6 0b4f8905
...@@ -496,7 +496,7 @@ gem 'flipper', '~> 0.21.0' ...@@ -496,7 +496,7 @@ gem 'flipper', '~> 0.21.0'
gem 'flipper-active_record', '~> 0.21.0' gem 'flipper-active_record', '~> 0.21.0'
gem 'flipper-active_support_cache_store', '~> 0.21.0' gem 'flipper-active_support_cache_store', '~> 0.21.0'
gem 'unleash', '~> 0.1.5' gem 'unleash', '~> 0.1.5'
gem 'gitlab-experiment', '~> 0.5.4' gem 'gitlab-experiment', '~> 0.6.1'
# Structured logging # Structured logging
gem 'lograge', '~> 0.5' gem 'lograge', '~> 0.5'
......
...@@ -470,7 +470,7 @@ GEM ...@@ -470,7 +470,7 @@ GEM
numerizer (~> 0.2) numerizer (~> 0.2)
gitlab-dangerfiles (2.1.2) gitlab-dangerfiles (2.1.2)
danger-gitlab danger-gitlab
gitlab-experiment (0.5.4) gitlab-experiment (0.6.1)
activesupport (>= 3.0) activesupport (>= 3.0)
request_store (>= 1.0) request_store (>= 1.0)
scientist (~> 1.6, >= 1.6.0) scientist (~> 1.6, >= 1.6.0)
...@@ -1487,7 +1487,7 @@ DEPENDENCIES ...@@ -1487,7 +1487,7 @@ DEPENDENCIES
github-markup (~> 1.7.0) github-markup (~> 1.7.0)
gitlab-chronic (~> 0.10.5) gitlab-chronic (~> 0.10.5)
gitlab-dangerfiles (~> 2.1.2) gitlab-dangerfiles (~> 2.1.2)
gitlab-experiment (~> 0.5.4) gitlab-experiment (~> 0.6.1)
gitlab-fog-azure-rm (~> 1.1.1) gitlab-fog-azure-rm (~> 1.1.1)
gitlab-labkit (~> 0.18.0) gitlab-labkit (~> 0.18.0)
gitlab-license (~> 1.5) gitlab-license (~> 1.5)
......
...@@ -10,22 +10,28 @@ class ApplicationExperiment < Gitlab::Experiment # rubocop:disable Gitlab/Namesp ...@@ -10,22 +10,28 @@ class ApplicationExperiment < Gitlab::Experiment # rubocop:disable Gitlab/Namesp
end end
def publish(_result = nil) def publish(_result = nil)
return unless should_track? # don't track events for excluded contexts super
record_experiment if @record # record the subject in the database if the context contains a namespace, group, project, actor or user
track(:assignment) # track that we've assigned a variant for this context
push_to_client publish_to_client if should_track? # publish the experiment data to the client
publish_to_database if @record # publish the experiment context to the database
end end
# push the experiment data to the client def publish_to_client
def push_to_client
Gon.push({ experiment: { name => signature } }, true) Gon.push({ experiment: { name => signature } }, true)
rescue NoMethodError rescue NoMethodError
# means we're not in the request cycle, and can't add to Gon. Log a warning maybe? # means we're not in the request cycle, and can't add to Gon. Log a warning maybe?
end end
def publish_to_database
# if the context contains a namespace, group, project, user, or actor
value = context.value
subject = value[:namespace] || value[:group] || value[:project] || value[:user] || value[:actor]
return unless ExperimentSubject.valid_subject?(subject)
variant = :experimental if @variant_name != :control
Experiment.add_subject(name, variant: variant || :control, subject: subject)
end
def track(action, **event_args) def track(action, **event_args)
return unless should_track? # don't track events for excluded contexts return unless should_track? # don't track events for excluded contexts
...@@ -41,14 +47,23 @@ class ApplicationExperiment < Gitlab::Experiment # rubocop:disable Gitlab/Namesp ...@@ -41,14 +47,23 @@ class ApplicationExperiment < Gitlab::Experiment # rubocop:disable Gitlab/Namesp
@record = true @record = true
end end
def exclude!
@excluded = true
end
def control_behavior def control_behavior
# define a default nil control behavior so we can omit it when not needed # define a default nil control behavior so we can omit it when not needed
end end
# TODO: remove
# This is deprecated logic as of v0.6.0 and should eventually be removed, but
# needs to stay intact for actively running experiments. The new strategy
# utilizes Digest::SHA2, a secret seed, and generates a 64-byte string.
def key_for(source, seed = name)
source = source.keys + source.values if source.is_a?(Hash)
ingredients = Array(source).map { |v| identify(v) }
ingredients.unshift(seed)
Digest::MD5.hexdigest(ingredients.join('|'))
end
private private
def feature_flag_name def feature_flag_name
...@@ -58,13 +73,4 @@ class ApplicationExperiment < Gitlab::Experiment # rubocop:disable Gitlab/Namesp ...@@ -58,13 +73,4 @@ class ApplicationExperiment < Gitlab::Experiment # rubocop:disable Gitlab/Namesp
def experiment_group? def experiment_group?
Feature.enabled?(feature_flag_name, self, type: :experiment, default_enabled: :yaml) Feature.enabled?(feature_flag_name, self, type: :experiment, default_enabled: :yaml)
end end
def record_experiment
subject = context.value[:namespace] || context.value[:group] || context.value[:project] || context.value[:user] || context.value[:actor]
return unless ExperimentSubject.valid_subject?(subject)
variant = :experimental if @variant_name != :control
Experiment.add_subject(name, variant: variant || :control, subject: subject)
end
end end
---
name: gitlab_experiment_middleware
introduced_by_url:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323643
milestone: '14.1'
type: development
group: group::adoption
default_enabled: false
...@@ -2,16 +2,22 @@ ...@@ -2,16 +2,22 @@
Gitlab::Experiment.configure do |config| Gitlab::Experiment.configure do |config|
config.base_class = 'ApplicationExperiment' config.base_class = 'ApplicationExperiment'
config.mount_at = '/-/experiment'
config.cache = Gitlab::Experiment::Cache::RedisHashStore.new( config.cache = Gitlab::Experiment::Cache::RedisHashStore.new(
pool: ->(&block) { Gitlab::Redis::SharedState.with { |redis| block.call(redis) } } pool: ->(&block) { Gitlab::Redis::SharedState.with { |redis| block.call(redis) } }
) )
end
# TODO: This shim should be removed after the feature flag is rolled out, as
# it only exists to facilitate the feature flag control of the behavior.
module Gitlab::Experiment::MiddlewareWithFeatureFlags
attr_reader :app
# TODO: This will be deprecated as of v0.6.0, but needs to stay intact for def call(env)
# actively running experiments until a versioning concept is put in place to return app.call(env) unless Feature.enabled?(:gitlab_experiment_middleware)
# enable migrating into the new SHA2 strategy.
config.context_hash_strategy = lambda do |source, seed| super
source = source.keys + source.values if source.is_a?(Hash)
data = Array(source).map { |v| (v.respond_to?(:to_global_id) ? v.to_global_id : v).to_s }
Digest::MD5.hexdigest(data.unshift(seed).join('|'))
end end
end end
Gitlab::Experiment::Middleware.prepend(Gitlab::Experiment::MiddlewareWithFeatureFlags)
...@@ -3,11 +3,10 @@ ...@@ -3,11 +3,10 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe ApplicationExperiment, :experiment do RSpec.describe ApplicationExperiment, :experiment do
subject { described_class.new('namespaced/stub') } subject { described_class.new('namespaced/stub', **context) }
let(:feature_definition) do let(:context) { {} }
{ name: 'namespaced_stub', type: 'experiment', group: 'group::adoption', default_enabled: false } let(:feature_definition) { { name: 'namespaced_stub', type: 'experiment', default_enabled: false } }
end
around do |example| around do |example|
Feature::Definition.definitions[:namespaced_stub] = Feature::Definition.new('namespaced_stub.yml', feature_definition) Feature::Definition.definitions[:namespaced_stub] = Feature::Definition.new('namespaced_stub.yml', feature_definition)
...@@ -25,7 +24,7 @@ RSpec.describe ApplicationExperiment, :experiment do ...@@ -25,7 +24,7 @@ RSpec.describe ApplicationExperiment, :experiment do
expect { experiment('namespaced/stub') { } }.not_to raise_error expect { experiment('namespaced/stub') { } }.not_to raise_error
end end
describe "enabled" do describe "#enabled?" do
before do before do
allow(subject).to receive(:enabled?).and_call_original allow(subject).to receive(:enabled?).and_call_original
...@@ -57,103 +56,100 @@ RSpec.describe ApplicationExperiment, :experiment do ...@@ -57,103 +56,100 @@ RSpec.describe ApplicationExperiment, :experiment do
end end
end end
describe "publishing results" do describe "#publish" do
it "doesn't record, track or push data to the client if we shouldn't track", :snowplow do it "doesn't track or publish to the client or database if we can't track", :snowplow do
allow(subject).to receive(:should_track?).and_return(false) allow(subject).to receive(:should_track?).and_return(false)
subject.record!
expect(subject).not_to receive(:record_experiment) expect(subject).not_to receive(:publish_to_client)
expect(subject).not_to receive(:track) expect(subject).not_to receive(:publish_to_database)
expect(Gon).not_to receive(:push)
subject.publish(:action) subject.publish
expect_no_snowplow_event expect_no_snowplow_event
end end
describe 'recording the experiment' do
it 'does not record the experiment if we do not tell it to' do
expect(subject).not_to receive(:record_experiment)
subject.publish
end
it 'records the experiment if we tell it to' do
subject.record!
expect(subject).to receive(:record_experiment)
subject.publish
end
end
it "tracks the assignment" do it "tracks the assignment" do
expect(subject).to receive(:track).with(:assignment) expect(subject).to receive(:track).with(:assignment)
subject.publish subject.publish
end end
it "pushes the experiment knowledge into the client using Gon" do it "publishes the to the client" do
expect(Gon).to receive(:push).with({ experiment: { 'namespaced/stub' => subject.signature } }, true) expect(subject).to receive(:publish_to_client)
subject.publish subject.publish
end end
it "handles when Gon raises exceptions (like when it can't be pushed into)" do it "publishes to the database if we've opted for that" do
expect(Gon).to receive(:push).and_raise(NoMethodError) subject.record!
expect(subject).to receive(:publish_to_database)
expect { subject.publish }.not_to raise_error subject.publish
end end
end
it "can exclude from within the block" do describe "#publish_to_client" do
expect(described_class.new('namespaced/stub') { |e| e.exclude! }).to be_excluded it "adds the data into Gon" do
end signature = { key: '86208ac54ca798e11f127e8b23ec396a', variant: 'control' }
expect(Gon).to receive(:push).with({ experiment: { 'namespaced/stub' => hash_including(signature) } }, true)
describe 'recording the experiment subject' do subject.publish_to_client
using RSpec::Parameterized::TableSyntax end
subject { described_class.new('namespaced/stub', nil, **context) } it "handles when Gon raises exceptions (like when it can't be pushed into)" do
expect(Gon).to receive(:push).and_raise(NoMethodError)
before do expect { subject.publish_to_client }.not_to raise_error
subject.record! end
end end
context 'when providing a compatible context' do describe "#publish_to_database" do
where(:context_key, :object_type) do using RSpec::Parameterized::TableSyntax
:namespace | :namespace let(:context) { { context_key => context_value }}
:group | :namespace
:project | :project before do
:user | :user subject.record!
:actor | :user
end end
with_them do context "when there's a usable subject" do
let(:context) { { context_key => build(object_type) }} where(:context_key, :context_value, :object_type) do
:namespace | build(:namespace) | :namespace
:group | build(:namespace) | :namespace
:project | build(:project) | :project
:user | build(:user) | :user
:actor | build(:user) | :user
end
it 'records the experiment and the experiment subject from the context' do with_them do
expect { subject.publish }.to change(Experiment, :count).by(1) it "creates an experiment and experiment subject record" do
expect { subject.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])
end
end end
end end
end
context 'when providing an incompatible or no context' do context "when there's not a usable subject" do
where(context_hash: [{ foo: :bar }, {}]) where(:context_key, :context_value) do
:namespace | nil
:foo | :bar
end
with_them do with_them do
let(:context) { context_hash } it "doesn't create an experiment record" do
expect { subject.publish_to_database }.not_to change(Experiment, :count)
end
it 'does not record the experiment' do it "doesn't create an experiment subject record" do
expect { subject.publish }.not_to change(Experiment, :count) expect { subject.publish_to_database }.not_to change(ExperimentSubject, :count)
end
end end
end end
end end
end end
describe "tracking events", :snowplow do describe "#track", :snowplow do
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(subject).to receive(:should_track?).and_return(false)
...@@ -185,7 +181,13 @@ RSpec.describe ApplicationExperiment, :experiment do ...@@ -185,7 +181,13 @@ RSpec.describe ApplicationExperiment, :experiment do
end end
end end
describe "variant resolution" do describe "#key_for" do
it "generates MD5 hashes" do
expect(subject.key_for(foo: :bar)).to eq('6f9ac12afdb9b58c2f19a136d09f9153')
end
end
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', subject, type: :experiment, default_enabled: :yaml)
......
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