diff --git a/lib/amplitude-experiment.rb b/lib/amplitude-experiment.rb index 60db186..59acd8c 100644 --- a/lib/amplitude-experiment.rb +++ b/lib/amplitude-experiment.rb @@ -8,10 +8,15 @@ require 'experiment/remote/client' require 'experiment/local/client' require 'experiment/local/config' +require 'experiment/local/evaluate_options' require 'experiment/local/assignment/assignment' require 'experiment/local/assignment/assignment_filter' require 'experiment/local/assignment/assignment_service' require 'experiment/local/assignment/assignment_config' +require 'experiment/local/exposure/exposure' +require 'experiment/local/exposure/exposure_filter' +require 'experiment/local/exposure/exposure_service' +require 'experiment/local/exposure/exposure_config' require 'experiment/util/lru_cache' require 'experiment/util/hash' require 'experiment/util/user' diff --git a/lib/experiment/local/assignment/assignment.rb b/lib/experiment/local/assignment/assignment.rb index f2d5fd4..fa012c0 100644 --- a/lib/experiment/local/assignment/assignment.rb +++ b/lib/experiment/local/assignment/assignment.rb @@ -1,6 +1,7 @@ module AmplitudeExperiment DAY_MILLIS = 86_400_000 # Assignment + # @deprecated Assignment tracking is deprecated. Use Exposure with ExposureService instead. class Assignment attr_accessor :user, :results, :timestamp diff --git a/lib/experiment/local/assignment/assignment_config.rb b/lib/experiment/local/assignment/assignment_config.rb index 061b921..fad51f5 100644 --- a/lib/experiment/local/assignment/assignment_config.rb +++ b/lib/experiment/local/assignment/assignment_config.rb @@ -1,5 +1,6 @@ module AmplitudeExperiment # AssignmentConfig + # @deprecated Assignment tracking is deprecated. Use ExposureConfig with ExposureService instead. class AssignmentConfig < AmplitudeAnalytics::Config attr_accessor :api_key, :cache_capacity diff --git a/lib/experiment/local/assignment/assignment_filter.rb b/lib/experiment/local/assignment/assignment_filter.rb index d09dfca..5746df1 100644 --- a/lib/experiment/local/assignment/assignment_filter.rb +++ b/lib/experiment/local/assignment/assignment_filter.rb @@ -1,5 +1,6 @@ module AmplitudeExperiment # AssignmentFilter + # @deprecated Assignment tracking is deprecated. Use ExposureFilter with ExposureService instead. class AssignmentFilter def initialize(size, ttl_millis = DAY_MILLIS) @cache = LRUCache.new(size, ttl_millis) diff --git a/lib/experiment/local/assignment/assignment_service.rb b/lib/experiment/local/assignment/assignment_service.rb index 8d40da1..0677d41 100644 --- a/lib/experiment/local/assignment/assignment_service.rb +++ b/lib/experiment/local/assignment/assignment_service.rb @@ -1,6 +1,7 @@ require_relative '../../../amplitude' module AmplitudeExperiment # AssignmentService + # @deprecated Assignment tracking is deprecated. Use ExposureService with Exposure tracking instead. class AssignmentService def initialize(amplitude, assignment_filter) @amplitude = amplitude diff --git a/lib/experiment/local/client.rb b/lib/experiment/local/client.rb index 2865888..a6af428 100644 --- a/lib/experiment/local/client.rb +++ b/lib/experiment/local/client.rb @@ -24,6 +24,10 @@ def initialize(api_key, config = nil) @assignment_service = nil @assignment_service = AssignmentService.new(AmplitudeAnalytics::Amplitude.new(config.assignment_config.api_key, configuration: config.assignment_config), AssignmentFilter.new(config.assignment_config.cache_capacity)) if config&.assignment_config + # Exposure service is always instantiated, using deployment key if no api key provided + @exposure_service = nil + @exposure_service = ExposureService.new(AmplitudeAnalytics::Amplitude.new(config.exposure_config.api_key, configuration: config.exposure_config), ExposureFilter.new(config.exposure_config.cache_capacity)) if config&.exposure_config + @cohort_storage = InMemoryCohortStorage.new @flag_config_storage = InMemoryFlagConfigStorage.new @flag_config_fetcher = LocalEvaluationFetcher.new(@api_key, @logger, @config.server_url) @@ -51,6 +55,7 @@ def evaluate(user, flag_keys = []) AmplitudeExperiment.filter_default_variants(variants) end + # TODO: ruby backwards compatibility for evaluate_v2 to be looked at again # Locally evaluates flag variants for a user. # This function will only evaluate flags for the keys specified in the flag_keys argument. If flag_keys is # missing or None, all flags are evaluated. This function differs from evaluate as it will return a default @@ -58,8 +63,9 @@ def evaluate(user, flag_keys = []) # # @param [User] user The user to evaluate # @param [String[]] flag_keys The flags to evaluate with the user, if empty all flags are evaluated + # @param [EvaluateOptions] options Optional evaluation options # @return [Hash[String, Variant]] The evaluated variants - def evaluate_v2(user, flag_keys = []) + def evaluate_v2(user, flag_keys = [], options = nil) flags = @flag_config_storage.flag_configs return {} if flags.nil? @@ -72,6 +78,8 @@ def evaluate_v2(user, flag_keys = []) result = @engine.evaluate(context, sorted_flags) @logger.debug("[Experiment] evaluate - result: #{result}") if @config.debug variants = AmplitudeExperiment.evaluation_variants_json_to_variants(result) + @exposure_service&.track(Exposure.new(user, variants)) if options&.tracks_exposure == true + # @deprecated Assignment tracking is deprecated. Use ExposureService with Exposure tracking instead. @assignment_service&.track(Assignment.new(user, variants)) variants end diff --git a/lib/experiment/local/config.rb b/lib/experiment/local/config.rb index ce1ee02..ee569b5 100644 --- a/lib/experiment/local/config.rb +++ b/lib/experiment/local/config.rb @@ -35,9 +35,14 @@ class LocalEvaluationConfig attr_accessor :flag_config_polling_interval_millis # Configuration for automatically tracking assignment events after an evaluation. + # @deprecated use exposure_config instead # @return [AssignmentConfig] the config instance attr_accessor :assignment_config + # Configuration for automatically tracking exposure events after an evaluation. + # @return [ExposureConfig] the config instance + attr_accessor :exposure_config + # Configuration for downloading cohorts required for flag evaluation # @return [CohortSyncConfig] the config instance attr_accessor :cohort_sync_config @@ -48,7 +53,8 @@ class LocalEvaluationConfig # @param [String] server_zone Location of the Amplitude data center to get flags and cohorts from, US or EU # @param [Hash] bootstrap The value of bootstrap. # @param [long] flag_config_polling_interval_millis The value of flag config polling interval in million seconds. - # @param [AssignmentConfig] assignment_config Configuration for automatically tracking assignment events after an evaluation. + # @param [AssignmentConfig] assignment_config Configuration for automatically tracking assignment events after an evaluation. @deprecated use exposure_config instead + # @param [ExposureConfig] exposure_config Configuration for automatically tracking exposure events after an evaluation. # @param [CohortSyncConfig] cohort_sync_config Configuration for downloading cohorts required for flag evaluation def initialize(server_url: DEFAULT_SERVER_URL, server_zone: ServerZone::US, @@ -57,6 +63,7 @@ def initialize(server_url: DEFAULT_SERVER_URL, debug: false, logger: nil, assignment_config: nil, + exposure_config: nil, cohort_sync_config: nil) @logger = logger if logger.nil? @@ -73,6 +80,7 @@ def initialize(server_url: DEFAULT_SERVER_URL, @bootstrap = bootstrap @flag_config_polling_interval_millis = flag_config_polling_interval_millis @assignment_config = assignment_config + @exposure_config = exposure_config end end end diff --git a/lib/experiment/local/evaluate_options.rb b/lib/experiment/local/evaluate_options.rb new file mode 100644 index 0000000..de69b5f --- /dev/null +++ b/lib/experiment/local/evaluate_options.rb @@ -0,0 +1,10 @@ +module AmplitudeExperiment + # Options for evaluating variants for a user. + class EvaluateOptions + attr_accessor :tracks_exposure + + def initialize(tracks_exposure: nil) + @tracks_exposure = tracks_exposure + end + end +end diff --git a/lib/experiment/local/exposure/exposure.rb b/lib/experiment/local/exposure/exposure.rb new file mode 100644 index 0000000..9907ae8 --- /dev/null +++ b/lib/experiment/local/exposure/exposure.rb @@ -0,0 +1,22 @@ +module AmplitudeExperiment + # Exposure is a class that represents a user's exposure to a set of flags. + class Exposure + attr_accessor :user, :results, :timestamp + + def initialize(user, results) + @user = user + @results = results + @timestamp = (Time.now.to_f * 1000).to_i + end + + def canonicalize + sb = "#{@user&.user_id&.strip} #{@user&.device_id&.strip} " + results.sort.to_h.each do |key, value| + next unless value.key + + sb += "#{key.strip} #{value.key&.strip} " + end + sb + end + end +end diff --git a/lib/experiment/local/exposure/exposure_config.rb b/lib/experiment/local/exposure/exposure_config.rb new file mode 100644 index 0000000..388151d --- /dev/null +++ b/lib/experiment/local/exposure/exposure_config.rb @@ -0,0 +1,12 @@ +module AmplitudeExperiment + # ExposureConfig + class ExposureConfig < AmplitudeAnalytics::Config + attr_accessor :api_key, :cache_capacity + + def initialize(api_key = nil, cache_capacity = 65_536, **kwargs) + super(**kwargs) + @api_key = api_key + @cache_capacity = cache_capacity + end + end +end diff --git a/lib/experiment/local/exposure/exposure_filter.rb b/lib/experiment/local/exposure/exposure_filter.rb new file mode 100644 index 0000000..0b6a8e9 --- /dev/null +++ b/lib/experiment/local/exposure/exposure_filter.rb @@ -0,0 +1,20 @@ +module AmplitudeExperiment + # ExposureFilter + class ExposureFilter + attr_accessor :ttl_millis + + def initialize(size, ttl_millis = DAY_MILLIS) + @cache = LRUCache.new(size, ttl_millis) + @ttl_millis = ttl_millis + end + + def should_track(exposure) + return false if exposure.results.empty? + + canonical_exposure = exposure.canonicalize + track = @cache.get(canonical_exposure).nil? + @cache.put(canonical_exposure, 0) if track + track + end + end +end diff --git a/lib/experiment/local/exposure/exposure_service.rb b/lib/experiment/local/exposure/exposure_service.rb new file mode 100644 index 0000000..b0b86b2 --- /dev/null +++ b/lib/experiment/local/exposure/exposure_service.rb @@ -0,0 +1,71 @@ +require_relative '../../../amplitude' +module AmplitudeExperiment + # ExposureService + class ExposureService + def initialize(amplitude, exposure_filter) + @amplitude = amplitude + @exposure_filter = exposure_filter + end + + def track(exposure) + return unless @exposure_filter.should_track(exposure) + + events = ExposureService.to_exposure_events(exposure, @exposure_filter.ttl_millis) + events.each do |event| + @amplitude.track(event) + end + end + + def self.to_exposure_events(exposure, ttl_millis) + events = [] + canonicalized = exposure.canonicalize + exposure.results.each do |flag_key, variant| + track_exposure = variant.metadata ? variant.metadata.fetch('trackExposure', true) : true + next unless track_exposure + + # Skip default variant exposures + is_default = variant.metadata ? variant.metadata.fetch('default', false) : false + next if is_default + + # Determine user properties to set and unset. + set_props = {} + unset_props = {} + flag_type = variant.metadata['flagType'] if variant.metadata + if flag_type != 'mutual-exclusion-group' + if variant.key + set_props["[Experiment] #{flag_key}"] = variant.key + elsif variant.value + set_props["[Experiment] #{flag_key}"] = variant.value + end + end + + # Build event properties. + event_properties = {} + event_properties['[Experiment] Flag Key'] = flag_key + if variant.key + event_properties['[Experiment] Variant'] = variant.key + elsif variant.value + event_properties['[Experiment] Variant'] = variant.value + end + event_properties['metadata'] = variant.metadata if variant.metadata + + # Build event. + event = AmplitudeAnalytics::BaseEvent.new( + '[Experiment] Exposure', + user_id: exposure.user.user_id, + device_id: exposure.user.device_id, + event_properties: event_properties, + user_properties: { + '$set' => set_props, + '$unset' => unset_props + }, + insert_id: "#{exposure.user.user_id} #{exposure.user.device_id} #{AmplitudeExperiment.hash_code("#{flag_key} #{canonicalized}")} #{exposure.timestamp / ttl_millis}" + ) + event.groups = exposure.user.groups if exposure.user.groups + + events << event + end + events + end + end +end diff --git a/spec/experiment/local/client_spec.rb b/spec/experiment/local/client_spec.rb index f454c60..05eacf0 100644 --- a/spec/experiment/local/client_spec.rb +++ b/spec/experiment/local/client_spec.rb @@ -1,3 +1,4 @@ +require 'set' module AmplitudeExperiment describe LocalEvaluationClient do let(:api_key) { 'client-DvWljIjiiuqLbyjqdvBaLFfEBrAvGuA3' } @@ -109,6 +110,55 @@ def setup_stub result = local_evaluation_client.evaluate(test_user2) expect(result['sdk-ci-local-dependencies-test-holdout']).to eq(nil) end + + it 'evaluate_v2 with tracks_exposure tracks non-default variants' do + setup_stub + + local_evaluation_client = LocalEvaluationClient.new(api_key, LocalEvaluationConfig.new(exposure_config: ExposureConfig.new('api_key'))) + local_evaluation_client.start + + # Mock the amplitude client's track method + mock_amplitude = local_evaluation_client.instance_variable_get(:@exposure_service).instance_variable_get(:@amplitude) + tracked_events = [] + allow(mock_amplitude).to receive(:track) do |event| + tracked_events << event + end + + # Perform evaluation with tracks_exposure=true + options = EvaluateOptions.new(tracks_exposure: true) + variants = local_evaluation_client.evaluate_v2(test_user2, [], options) + + # Verify that track was called + expect(tracked_events.length).to be > 0, 'Amplitude track should be called when tracks_exposure is true' + + # Count non-default variants + non_default_variants = variants.reject do |_flag_key, variant| + (variant.metadata && variant.metadata['default']) + end + + # Verify that we have one event per non-default variant + expect(tracked_events.length).to eq(non_default_variants.length), + "Expected #{non_default_variants.length} exposure events, got #{tracked_events.length}" + + # Verify each event has the correct structure + tracked_flag_keys = Set.new + tracked_events.each do |event| + expect(event.event_type).to eq('[Experiment] Exposure') + expect(event.user_id).to eq(test_user2.user_id) + flag_key = event.event_properties['[Experiment] Flag Key'] + expect(flag_key).not_to be_nil, 'Event should have flag key' + tracked_flag_keys.add(flag_key) + # Verify the variant is not default + variant = variants[flag_key] + expect(variant).not_to be_nil, "Variant for #{flag_key} should exist" + expect(variant.metadata && variant.metadata['default']).to be_falsy, + "Variant for #{flag_key} should not be default" + end + + # Verify all non-default variants were tracked + expect(tracked_flag_keys).to eq(Set.new(non_default_variants.keys)), + 'All non-default variants should be tracked' + end end end end diff --git a/spec/experiment/local/exposure/exposure_filter_spec.rb b/spec/experiment/local/exposure/exposure_filter_spec.rb new file mode 100644 index 0000000..d38c0a9 --- /dev/null +++ b/spec/experiment/local/exposure/exposure_filter_spec.rb @@ -0,0 +1,91 @@ +require 'spec_helper' +require 'experiment/local/exposure/exposure_filter' +require 'experiment/local/exposure/exposure' +require 'experiment/user' +require 'experiment/variant' + +describe AmplitudeExperiment::ExposureFilter do + let(:user) { AmplitudeExperiment::User.new(user_id: 'user', device_id: 'device') } + + describe '#should_track' do + it 'returns true for single exposure' do + filter = AmplitudeExperiment::ExposureFilter.new(100) + results = { + 'flag-key-1' => AmplitudeExperiment::Variant.new(key: 'on', value: 'on'), + 'flag-key-2' => AmplitudeExperiment::Variant.new(key: 'control', value: 'control') + } + exposure = AmplitudeExperiment::Exposure.new(user, results) + expect(filter.should_track(exposure)).to be true + end + + it 'returns false for duplicate exposure' do + filter = AmplitudeExperiment::ExposureFilter.new(100) + results = { + 'flag-key-1' => AmplitudeExperiment::Variant.new(key: 'on', value: 'on'), + 'flag-key-2' => AmplitudeExperiment::Variant.new(key: 'control', value: 'control') + } + exposure1 = AmplitudeExperiment::Exposure.new(user, results) + exposure2 = AmplitudeExperiment::Exposure.new(user, results) + expect(filter.should_track(exposure1)).to be true + expect(filter.should_track(exposure2)).to be false + end + + it 'returns true for same user different results' do + filter = AmplitudeExperiment::ExposureFilter.new(100) + results1 = { + 'flag-key-1' => AmplitudeExperiment::Variant.new(key: 'on', value: 'on'), + 'flag-key-2' => AmplitudeExperiment::Variant.new(key: 'control', value: 'control') + } + results2 = { + 'flag-key-1' => AmplitudeExperiment::Variant.new(key: 'control', value: 'control'), + 'flag-key-2' => AmplitudeExperiment::Variant.new(key: 'on', value: 'on') + } + exposure1 = AmplitudeExperiment::Exposure.new(user, results1) + exposure2 = AmplitudeExperiment::Exposure.new(user, results2) + expect(filter.should_track(exposure1)).to be true + expect(filter.should_track(exposure2)).to be true + end + + it 'returns true for same results different users' do + filter = AmplitudeExperiment::ExposureFilter.new(100) + user1 = AmplitudeExperiment::User.new(user_id: 'user', device_id: 'device') + user2 = AmplitudeExperiment::User.new(user_id: 'different user', device_id: 'device') + results = { + 'flag-key-1' => AmplitudeExperiment::Variant.new(key: 'on', value: 'on'), + 'flag-key-2' => AmplitudeExperiment::Variant.new(key: 'control', value: 'control') + } + exposure1 = AmplitudeExperiment::Exposure.new(user1, results) + exposure2 = AmplitudeExperiment::Exposure.new(user2, results) + expect(filter.should_track(exposure1)).to be true + expect(filter.should_track(exposure2)).to be true + end + + it 'returns false for empty results' do + filter = AmplitudeExperiment::ExposureFilter.new(100) + user1 = AmplitudeExperiment::User.new(user_id: 'user', device_id: 'device') + user2 = AmplitudeExperiment::User.new(user_id: 'different user', device_id: 'device') + exposure1 = AmplitudeExperiment::Exposure.new(user1, {}) + exposure2 = AmplitudeExperiment::Exposure.new(user1, {}) + exposure3 = AmplitudeExperiment::Exposure.new(user2, {}) + expect(filter.should_track(exposure1)).to be false + expect(filter.should_track(exposure2)).to be false + expect(filter.should_track(exposure3)).to be false + end + + it 'returns false for duplicate exposures with different ordering' do + filter = AmplitudeExperiment::ExposureFilter.new(100) + results1 = { + 'flag-key-1' => AmplitudeExperiment::Variant.new(key: 'on', value: 'on'), + 'flag-key-2' => AmplitudeExperiment::Variant.new(key: 'control', value: 'control') + } + results2 = { + 'flag-key-2' => AmplitudeExperiment::Variant.new(key: 'control', value: 'control'), + 'flag-key-1' => AmplitudeExperiment::Variant.new(key: 'on', value: 'on') + } + exposure1 = AmplitudeExperiment::Exposure.new(user, results1) + exposure2 = AmplitudeExperiment::Exposure.new(user, results2) + expect(filter.should_track(exposure1)).to be true + expect(filter.should_track(exposure2)).to be false + end + end +end diff --git a/spec/experiment/local/exposure/exposure_service_spec.rb b/spec/experiment/local/exposure/exposure_service_spec.rb new file mode 100644 index 0000000..c685670 --- /dev/null +++ b/spec/experiment/local/exposure/exposure_service_spec.rb @@ -0,0 +1,152 @@ +require 'spec_helper' +require 'experiment/local/exposure/exposure_service' +require 'experiment/local/exposure/exposure' +require 'experiment/user' +require 'experiment/variant' + +describe AmplitudeExperiment::ExposureService do + let(:user) { AmplitudeExperiment::User.new(user_id: 'user', device_id: 'device') } + let(:mock_amplitude) { double('Amplitude') } + let(:filter) { double('ExposureFilter', should_track: true, ttl_millis: AmplitudeExperiment::DAY_MILLIS) } + let(:service) { AmplitudeExperiment::ExposureService.new(mock_amplitude, filter) } + + describe '#track' do + it 'calls amplitude track for each event' do + results = { + 'flag-key-1' => AmplitudeExperiment::Variant.new(key: 'on', value: 'on'), + 'flag-key-2' => AmplitudeExperiment::Variant.new(key: 'control', value: 'control') + } + exposure = AmplitudeExperiment::Exposure.new(user, results) + expect(mock_amplitude).to receive(:track).at_least(:once) + service.track(exposure) + end + + it 'does not track if filter returns false' do + filter = double('ExposureFilter', should_track: false, ttl_millis: AmplitudeExperiment::DAY_MILLIS) + service = AmplitudeExperiment::ExposureService.new(mock_amplitude, filter) + results = { + 'flag-key-1' => AmplitudeExperiment::Variant.new(key: 'on', value: 'on') + } + exposure = AmplitudeExperiment::Exposure.new(user, results) + expect(mock_amplitude).not_to receive(:track) + service.track(exposure) + end + end + + describe '.to_exposure_events' do + it 'creates one event per flag with comprehensive variants' do + basic = AmplitudeExperiment::Variant.new( + key: 'control', + value: 'control', + metadata: { + 'segmentName' => 'All Other Users', + 'flagType' => 'experiment', + 'flagVersion' => 10, + 'default' => false + } + ) + different_value = AmplitudeExperiment::Variant.new( + key: 'on', + value: 'control', + metadata: { + 'segmentName' => 'All Other Users', + 'flagType' => 'experiment', + 'flagVersion' => 10, + 'default' => false + } + ) + default = AmplitudeExperiment::Variant.new( + key: 'off', + metadata: { + 'segmentName' => 'All Other Users', + 'flagType' => 'experiment', + 'flagVersion' => 10, + 'default' => true + } + ) + mutex = AmplitudeExperiment::Variant.new( + key: 'slot-1', + value: 'slot-1', + metadata: { + 'segmentName' => 'All Other Users', + 'flagType' => 'mutual-exclusion-group', + 'flagVersion' => 10, + 'default' => false + } + ) + holdout = AmplitudeExperiment::Variant.new( + key: 'holdout', + value: 'holdout', + metadata: { + 'segmentName' => 'All Other Users', + 'flagType' => 'holdout-group', + 'flagVersion' => 10, + 'default' => false + } + ) + partial_metadata = AmplitudeExperiment::Variant.new( + key: 'on', + value: 'on', + metadata: { + 'segmentName' => 'All Other Users', + 'flagType' => 'release' + } + ) + empty_metadata = AmplitudeExperiment::Variant.new( + key: 'on', + value: 'on' + ) + empty_variant = AmplitudeExperiment::Variant.new + results = { + 'basic' => basic, + 'different_value' => different_value, + 'default' => default, + 'mutex' => mutex, + 'holdout' => holdout, + 'partial_metadata' => partial_metadata, + 'empty_metadata' => empty_metadata, + 'empty_variant' => empty_variant + } + exposure = AmplitudeExperiment::Exposure.new(user, results) + events = AmplitudeExperiment::ExposureService.to_exposure_events(exposure, AmplitudeExperiment::DAY_MILLIS) + # Should exclude default (default=true) only + # basic, different_value, mutex, holdout, partial_metadata, empty_metadata, empty_variant = 7 events + expect(events.length).to eq(7) + + events.each do |event| + expect(event.event_type).to eq('[Experiment] Exposure') + expect(event.user_id).to eq(user.user_id) + expect(event.device_id).to eq(user.device_id) + + flag_key = event.event_properties['[Experiment] Flag Key'] + expect(results[flag_key]).to be_truthy + variant = results[flag_key] + + # Validate event properties + if variant.key + expect(event.event_properties['[Experiment] Variant']).to eq(variant.key) + elsif variant.value + expect(event.event_properties['[Experiment] Variant']).to eq(variant.value) + end + expect(event.event_properties['metadata']).to eq(variant.metadata) if variant.metadata + + # Validate user properties + flag_type = variant.metadata ? variant.metadata['flagType'] : nil + if flag_type == 'mutual-exclusion-group' + expect(event.user_properties['$set']).to eq({}) + expect(event.user_properties['$unset']).to eq({}) + elsif variant.metadata && variant.metadata['default'] + expect(event.user_properties['$set']).to eq({}) + expect(event.user_properties['$unset']).to have_key("[Experiment] #{flag_key}") + else + if variant.key + expect(event.user_properties['$set']["[Experiment] #{flag_key}"]).to eq(variant.key) + elsif variant.value + expect(event.user_properties['$set']["[Experiment] #{flag_key}"]).to eq(variant.value) + end + expect(event.user_properties['$unset']).to eq({}) + end + end + end + end +end