-
Notifications
You must be signed in to change notification settings - Fork 167
Proof-of-Concept: Add persistence option for A/B tests #11202
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| class OutdatedAbTestAssignmentCleanupJob < ApplicationJob | ||
| queue_as :low | ||
|
|
||
| def perform | ||
| AbTestAssignment.where.not(experiment: active_experiments).destroy_all | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def active_experiments | ||
| AbTests.all.values.map(&:experiment_name) | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| class AbTestAssignment < ApplicationRecord | ||
| class << self | ||
| def bucket(**) | ||
| find_by(**)&.bucket&.to_sym | ||
| end | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| class CreateAbTestAssignments < ActiveRecord::Migration[7.1] | ||
| def change | ||
| create_table :ab_test_assignments do |t| | ||
| t.string :experiment, null: false | ||
| t.string :discriminator, null: false | ||
| t.string :bucket, null: false | ||
| t.index [:experiment, :discriminator], unique: true | ||
| end | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,8 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| class AbTest | ||
| attr_reader :buckets, :experiment_name, :default_bucket, :should_log | ||
| attr_reader :buckets, :experiment_name, :default_bucket, :should_log, :persist | ||
| alias_method :experiment, :experiment_name | ||
|
|
||
| MAX_SHA = (16 ** 64) - 1 | ||
|
|
||
|
|
@@ -18,13 +19,15 @@ def initialize( | |
| buckets: {}, | ||
| should_log: nil, | ||
| default_bucket: :default, | ||
| persist: false, | ||
| &discriminator | ||
| ) | ||
| @buckets = buckets | ||
| @discriminator = discriminator | ||
| @experiment_name = experiment_name | ||
| @default_bucket = default_bucket | ||
| @should_log = should_log | ||
| @persist = persist | ||
| raise 'invalid bucket data structure' unless valid_bucket_data_structure? | ||
| ensure_numeric_percentages | ||
| raise 'bucket percentages exceed 100' unless within_100_percent? | ||
|
|
@@ -36,25 +39,44 @@ def initialize( | |
| # @params [Hash] session | ||
| # @param [User] user | ||
| # @param [Hash] user_session | ||
| def bucket(request:, service_provider:, session:, user:, user_session:) | ||
| return nil if no_percentages? | ||
|
|
||
| # @param [Boolean] persisted_read_only Avoid new bucket assignment if test is configured to be | ||
| # persisted but there is no persisted value. | ||
| def bucket( | ||
| request:, | ||
| service_provider:, | ||
| session:, | ||
| user:, | ||
| user_session:, | ||
| persisted_read_only: false | ||
| ) | ||
| discriminator = resolve_discriminator( | ||
| request:, service_provider:, session:, user:, | ||
| user_session: | ||
| ) | ||
| return nil if discriminator.blank? | ||
|
|
||
| persisted_value = AbTestAssignment.bucket(experiment:, discriminator:) if persist | ||
| return persisted_value if persisted_value || (persist && persisted_read_only) | ||
|
|
||
| return nil if no_percentages? | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wouldn't we want to return
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting question. This specific code existed previously, I had just rearranged it. But I'm not sure if |
||
|
|
||
| user_value = percent(discriminator) | ||
|
|
||
| bucket = @default_bucket | ||
|
|
||
| min = 0 | ||
| buckets.keys.each do |key| | ||
| max = min + buckets[key] | ||
| return key if user_value > min && user_value <= max | ||
| if user_value > min && user_value <= max | ||
| bucket = key | ||
| break | ||
| end | ||
| min = max | ||
| end | ||
|
|
||
| @default_bucket | ||
| AbTestAssignment.create(experiment:, discriminator:, bucket:) if persist | ||
|
|
||
| bucket | ||
| end | ||
|
|
||
| def include_in_analytics_event?(event_name) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe clearer name?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
some more ideas