Skip to content

feat: Duplicate experiment key issue with multi feature flag #282

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

Merged
merged 17 commits into from
Jul 26, 2021

Conversation

ozayr-zaviar
Copy link
Contributor

@ozayr-zaviar ozayr-zaviar commented Jul 12, 2021

Summary

  • Fixed an issue related to duplicate experiment_key.
  • While trying to get variation from the variation_key_map, it was unable to find because the latest experimentKey was overriding the previous one.

TestPlan

@ozayr-zaviar ozayr-zaviar changed the title init feat: Duplicate experiment key issue with multi feature flag Jul 12, 2021
@coveralls
Copy link

coveralls commented Jul 12, 2021

Coverage Status

Coverage decreased (-0.01%) to 99.477% when pulling 326b771 on uzair/uniqueexperimentkey into c6e659e on master.

Copy link
Contributor

@msohailhussain msohailhussain left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please review the logic, you can extract experiment_key_map from the experiment_id_map, other way will cause issues.

@@ -117,6 +119,8 @@ def initialize(datafile, logger, error_handler)
@audience_id_map = @audience_id_map.merge(generate_key_map(@typed_audiences, 'id')) unless @typed_audiences.empty?
@variation_id_map = {}
@variation_key_map = {}
@variation_id_map_by_experiment_id = {}
@variation_key_map_by_experiment_id = {}
@variation_id_to_variable_usage_map = {}
@variation_id_to_experiment_map = {}
@experiment_key_map.each_value do |exp|
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't do it using experiment_map, do it using experiment_id

@@ -132,9 +136,9 @@ def initialize(datafile, logger, error_handler)
@rollout_experiment_key_map = {}
@rollout_id_map.each_value do |rollout|
exps = rollout.fetch('experiments')
@rollout_experiment_key_map = @rollout_experiment_key_map.merge(generate_key_map(exps, 'key'))
@rollout_experiment_key_map = @rollout_experiment_key_map.merge(generate_key_map(exps, 'id'))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add all of your logics based on experiment_ids, not on experiment_keys.

@@ -736,7 +736,7 @@
'177776' => config_body['rollouts'][0]['experiments'][2],
'177774' => config_body['rollouts'][1]['experiments'][0],
'177779' => config_body['rollouts'][1]['experiments'][1],
'rollout_exp_with_diff_id_and_key' => config_body['rollouts'][1]['experiments'][2]
'177780' => config_body['rollouts'][1]['experiments'][2]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this change is needed?

Copy link
Contributor

@mnoman09 mnoman09 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall LGTM, Just update headers.

Copy link
Contributor

@jaeopt jaeopt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes the delivery rule-key conflict. I still see potential issue with experiment key conflicts. I believe they'll covered by new FSC test cases.

@@ -304,17 +371,17 @@ def get_variation_id_from_key(experiment_key, variation_key)
nil
end

def get_whitelisted_variations(experiment_key)
def get_whitelisted_variations(experiment_id)
# Retrieves whitelisted variations for a given experiment Key
#
# experiment_key - String Key representing the experiment
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix this comment

@@ -347,29 +346,30 @@ def get_forced_variation(project_config, experiment_key, user_id)
end

experiment_to_variation_map = @forced_variation_map[user_id]
# to be corrected
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason of this comment?

Copy link
Contributor

@jaeopt jaeopt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to change experiment-key to -id based.
For log and decision messages, please keep all 'experiment-key'as is (instead of using 'experiment-id' which is not helpful to users).

@@ -1095,11 +1097,11 @@ def send_impression(config, experiment, variation_key, flag_key, rule_key, enabl
@event_processor.process(user_event)
return unless @notification_center.notification_count(NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE]).positive?

@logger.log(Logger::INFO, "Activating user '#{user_id}' in experiment '#{experiment_key}'.")
@logger.log(Logger::INFO, "Activating user '#{user_id}' in experiment '#{experiment_id}'.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keep experiment_key in log

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @jaeopt on this. Also adding experiment_id can actually help too

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having both will be helpful, but for consistency with all other SDKs, let's stick to "key" only in logs and messages

unless project_config.experiment_running?(experiment)
message = "Experiment '#{experiment_key}' is not running."
message = "Experiment '#{experiment_id}' is not running."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keep experiment_key for log and message

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or Keep both.

@@ -108,7 +107,7 @@ def get_variation(project_config, experiment_key, user_id, attributes = nil, dec
user_meets_audience_conditions, reasons_received = Audience.user_meets_audience_conditions?(project_config, experiment, attributes, @logger)
decide_reasons.push(*reasons_received)
unless user_meets_audience_conditions
message = "User '#{user_id}' does not meet the conditions to be in experiment '#{experiment_key}'."
message = "User '#{user_id}' does not meet the conditions to be in experiment '#{experiment_id}'."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keep experiment_key in all messages here and others

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, Agree! An i think you should keep both id and key in the messages

@@ -1095,11 +1097,11 @@ def send_impression(config, experiment, variation_key, flag_key, rule_key, enabl
@event_processor.process(user_event)
return unless @notification_center.notification_count(NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE]).positive?

@logger.log(Logger::INFO, "Activating user '#{user_id}' in experiment '#{experiment_key}'.")
@logger.log(Logger::INFO, "Activating user '#{user_id}' in experiment '#{experiment_id}'.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @jaeopt on this. Also adding experiment_id can actually help too

Comment on lines +152 to +155
@variation_id_map[exp['key']] = generate_key_map(variations, 'id')
@variation_key_map[exp['key']] = generate_key_map(variations, 'key')
@variation_id_map_by_experiment_id[id] = generate_key_map(variations, 'id')
@variation_key_map_by_experiment_id[id] = generate_key_map(variations, 'key')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These variable names are really confusing. Looking at variation_id_map, it looks like its a map of variations with variation ids as keys but looks like the key is the experiment id which contains all the variations for that experiment. We might want to consider refactoring it later for better and more intuitive naming. Or if possible and if it makes sense to you , can you rename these variables to reflect more of what they actually are. variation_id_map can probably be variation_map_by_experiment_id etc. You can certainly think of a better variable name or can also leave it like that to refactor it later, i wont block the review on this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's talk offline.

@@ -281,6 +302,52 @@ def get_variation_from_id(experiment_key, variation_id)
nil
end

def get_variation_from_id_by_experiment_id(experiment_id, variation_id)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can variation ids duplicate across experiments? why do we need experiment_id if we have variation_id. shouldn't variation_id be enought to uniquely identify and fetch a variation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes as far as I know variation Ids are unique but I was following this method def get_variation_from_id(experiment_key, variation_id)

nil
end

def get_variation_id_from_key_by_experiment_id(experiment_id, variation_key)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense because variation_key can probably duplicate across experiments.

return experiment['forcedVariations'] if experiment

@logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
@logger.log Logger::ERROR, "Experiment key '#{experiment_id}' is not in datafile."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be helpful to keep both experiment_id and experiment_key in the log

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cant use experiment key here as we are no longer sending key in get_whitelisted_variations instead we are sending the ID. If the given ID is wrong we cannot get an experiment and hence we can not get the key.

unless project_config.experiment_running?(experiment)
message = "Experiment '#{experiment_key}' is not running."
message = "Experiment '#{experiment_id}' is not running."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or Keep both.

@@ -108,7 +107,7 @@ def get_variation(project_config, experiment_key, user_id, attributes = nil, dec
user_meets_audience_conditions, reasons_received = Audience.user_meets_audience_conditions?(project_config, experiment, attributes, @logger)
decide_reasons.push(*reasons_received)
unless user_meets_audience_conditions
message = "User '#{user_id}' does not meet the conditions to be in experiment '#{experiment_key}'."
message = "User '#{user_id}' does not meet the conditions to be in experiment '#{experiment_id}'."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, Agree! An i think you should keep both id and key in the messages

decide_reasons.push(*reasons_received)

next unless variation_id

variation = project_config.variation_id_map[experiment_key][variation_id]
variation = project_config.variation_id_map_by_experiment_id[experiment_id][variation_id]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why accessing it directly from the map when you have an accessor method?

@variation_key_map[key] = generate_key_map(variations, 'key')
@variation_id_map[exp['key']] = generate_key_map(variations, 'id')
@variation_key_map[exp['key']] = generate_key_map(variations, 'key')
@variation_id_map_by_experiment_id[id] = generate_key_map(variations, 'id')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need it by experiment id? i assume variation ids are unique? if yes, then a simple map with variation ids as keys and variations as values would suffice? It was probably required in case of variation keys because keys can be reused across the experiments and you needed to specify which experiment to look into, but for variation id, i am not sure if experiment info is needed at all?

@msohailhussain msohailhussain marked this pull request as ready for review July 21, 2021 23:31
@msohailhussain msohailhussain requested a review from a team as a code owner July 21, 2021 23:31
Copy link
Contributor

@jaeopt jaeopt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not see the last reviews addressed yet. Added some more comments.

#
# Returns the variation or nil if not found

variation_key_map_by_experiment_id = @variation_key_map_by_experiment_id[experiment_id]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
variation_key_map_by_experiment_id = @variation_key_map_by_experiment_id[experiment_id]
variation_key_map = @variation_key_map_by_experiment_id[experiment_id]

same name as the map will be confusing


variation_key_map_by_experiment_id = @variation_key_map_by_experiment_id[experiment_id]
if variation_key_map_by_experiment_id
variation = variation_key_map_by_experiment_id[variation_key]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
variation = variation_key_map_by_experiment_id[variation_key]
variation = variation_key_map[variation_key]

@@ -1095,11 +1097,11 @@ def send_impression(config, experiment, variation_key, flag_key, rule_key, enabl
@event_processor.process(user_event)
return unless @notification_center.notification_count(NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE]).positive?

@logger.log(Logger::INFO, "Activating user '#{user_id}' in experiment '#{experiment_key}'.")
@logger.log(Logger::INFO, "Activating user '#{user_id}' in experiment '#{experiment_id}'.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having both will be helpful, but for consistency with all other SDKs, let's stick to "key" only in logs and messages

Copy link
Contributor

@jaeopt jaeopt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Contributor

@msohailhussain msohailhussain left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@jaeopt jaeopt merged commit 6f844ca into master Jul 26, 2021
@jaeopt jaeopt deleted the uzair/uniqueexperimentkey branch July 26, 2021 23:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants