Skip to content
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

feat: Added new getFeatureVariableJson and getAllFeatureVariables Apis #251

Merged
merged 13 commits into from
May 13, 2020
Merged
179 changes: 144 additions & 35 deletions lib/optimizely.rb
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,32 @@ def get_feature_variable_string(feature_flag_key, variable_key, user_id, attribu
variable_value
end

# Get the Json value of the specified variable in the feature flag in a Dict.
#
# @param feature_flag_key - String key of feature flag the variable belongs to
# @param variable_key - String key of variable for which we are getting the string value
# @param user_id - String user ID
# @param attributes - Hash representing visitor attributes and values which need to be recorded.
#
# @return [Dict] the Dict containing variable value.
# @return [nil] if the feature flag or variable are not found.

def get_feature_variable_json(feature_flag_key, variable_key, user_id, attributes = nil)
unless is_valid
@logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_feature_variable_json').message)
return nil
end
variable_value = get_feature_variable_for_type(
feature_flag_key,
variable_key,
Optimizely::Helpers::Constants::VARIABLE_TYPES['JSON'],
user_id,
attributes
)

variable_value
end

# Get the Boolean value of the specified variable in the feature flag.
#
# @param feature_flag_key - String key of feature flag the variable belongs to
Expand Down Expand Up @@ -484,6 +510,71 @@ def get_feature_variable_double(feature_flag_key, variable_key, user_id, attribu
variable_value
end

# Get values of all the variables in the feature flag and returns them in a Dict
#
# @param feature_flag_key - String key of feature flag
# @param user_id - String user ID
# @param attributes - Hash representing visitor attributes and values which need to be recorded.
#
# @return [Dict] the Dict containing all the varible values
# @return [nil] if the feature flag is not found.

def get_all_feature_variables(feature_flag_key, user_id, attributes = nil)
unless is_valid
@logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_all_feature_variables').message)
return nil
end

return nil unless Optimizely::Helpers::Validator.inputs_valid?(
{
feature_flag_key: feature_flag_key,
user_id: user_id
},
@logger, Logger::ERROR
)

return nil unless user_inputs_valid?(attributes)

config = project_config

feature_flag = config.get_feature_flag_from_key(feature_flag_key)
unless feature_flag
@logger.log(Logger::INFO, "No feature flag was found for key '#{feature_flag_key}'.")
return nil
end

decision = @decision_service.get_variation_for_feature(config, feature_flag, user_id, attributes)
variation = decision ? decision['variation'] : nil
feature_enabled = variation ? variation['featureEnabled'] : false
all_variables = {}

feature_flag['variables'].each do |variable|
variable_value = get_feature_variable_for_variation(feature_flag_key, feature_enabled, variation, variable, user_id)
all_variables[variable['key']] = Helpers::VariableType.cast_value_to_type(variable_value, variable['type'], @logger)
end

source_string = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
if decision && decision['source'] == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
source_info = {
Copy link
Contributor

Choose a reason for hiding this comment

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

isn't this out of scope?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No. It was decided to add a notification for all-feature-variables later. This is bein added in all the sdks now

experiment_key: decision.experiment['key'],
variation_key: variation['key']
}
source_string = Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
end

@notification_center.send_notifications(
NotificationCenter::NOTIFICATION_TYPES[:DECISION],
Helpers::Constants::DECISION_NOTIFICATION_TYPES['ALL_FEATURE_VARIABLES'], user_id, (attributes || {}),
feature_key: feature_flag_key,
feature_enabled: feature_enabled,
source: source_string,
variable_values: all_variables,
source_info: source_info || {}
)

all_variables
end

# Get the Integer value of the specified variable in the feature flag.
#
# @param feature_flag_key - String key of feature flag the variable belongs to
Expand Down Expand Up @@ -649,52 +740,31 @@ def get_feature_variable_for_type(feature_flag_key, variable_key, variable_type,
# Error message logged in DatafileProjectConfig- get_feature_flag_from_key
return nil if variable.nil?

feature_enabled = false

# If variable_type is nil, set it equal to variable['type']
variable_type ||= variable['type']
# Returns nil if type differs
if variable['type'] != variable_type
@logger.log(Logger::WARN,
"Requested variable as type '#{variable_type}' but variable '#{variable_key}' is of type '#{variable['type']}'.")
return nil
else
source_string = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
decision = @decision_service.get_variation_for_feature(config, feature_flag, user_id, attributes)
variable_value = variable['defaultValue']
if decision
variation = decision['variation']
if decision['source'] == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
source_info = {
experiment_key: decision.experiment['key'],
variation_key: variation['key']
}
source_string = Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
end
feature_enabled = variation['featureEnabled']
if feature_enabled == true
variation_variable_usages = config.variation_id_to_variable_usage_map[variation['id']]
variable_id = variable['id']
if variation_variable_usages&.key?(variable_id)
variable_value = variation_variable_usages[variable_id]['value']
@logger.log(Logger::INFO,
"Got variable value '#{variable_value}' for variable '#{variable_key}' of feature flag '#{feature_flag_key}'.")
else
@logger.log(Logger::DEBUG,
"Variable '#{variable_key}' is not used in variation '#{variation['key']}'. Returning the default variable value '#{variable_value}'.")
end
else
@logger.log(Logger::DEBUG,
"Feature '#{feature_flag_key}' for variation '#{variation['key']}' is not enabled. Returning the default variable value '#{variable_value}'.")
end
else
@logger.log(Logger::INFO,
"User '#{user_id}' was not bucketed into any variation for feature flag '#{feature_flag_key}'. Returning the default variable value '#{variable_value}'.")
end
end

decision = @decision_service.get_variation_for_feature(config, feature_flag, user_id, attributes)
variation = decision ? decision['variation'] : nil
feature_enabled = variation ? variation['featureEnabled'] : false

variable_value = get_feature_variable_for_variation(feature_flag_key, feature_enabled, variation, variable, user_id)
variable_value = Helpers::VariableType.cast_value_to_type(variable_value, variable_type, @logger)

source_string = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
if decision && decision['source'] == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
source_info = {
experiment_key: decision.experiment['key'],
variation_key: variation['key']
}
source_string = Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
end

@notification_center.send_notifications(
NotificationCenter::NOTIFICATION_TYPES[:DECISION],
Helpers::Constants::DECISION_NOTIFICATION_TYPES['FEATURE_VARIABLE'], user_id, (attributes || {}),
Expand All @@ -710,6 +780,45 @@ def get_feature_variable_for_type(feature_flag_key, variable_key, variable_type,
variable_value
end

def get_feature_variable_for_variation(feature_flag_key, feature_enabled, variation, variable, user_id)
# Helper method to get the non type-casted value for a variable attached to a
# feature flag. Returns appropriate variable value depending on whether there
# was a matching variation, feature was enabled or not or varible was part of the
# available variation or not. Also logs the appropriate message explaining how it
# evaluated the value of the variable.
#
# feature_flag_key - String key of feature flag the variable belongs to
# feature_enabled - Boolean indicating if feature is enabled or not
# variation - varition returned by decision service
# user_id - String user ID
#
# Returns string value of the variable.

config = project_config
variable_value = variable['defaultValue']
if variation
if feature_enabled == true
variation_variable_usages = config.variation_id_to_variable_usage_map[variation['id']]
variable_id = variable['id']
if variation_variable_usages&.key?(variable_id)
variable_value = variation_variable_usages[variable_id]['value']
@logger.log(Logger::INFO,
"Got variable value '#{variable_value}' for variable '#{variable['key']}' of feature flag '#{feature_flag_key}'.")
else
@logger.log(Logger::DEBUG,
"Variable '#{variable['key']}' is not used in variation '#{variation['key']}'. Returning the default variable value '#{variable_value}'.")
end
else
@logger.log(Logger::DEBUG,
"Feature '#{feature_flag_key}' for variation '#{variation['key']}' is not enabled. Returning the default variable value '#{variable_value}'.")
end
else
@logger.log(Logger::INFO,
"User '#{user_id}' was not bucketed into any variation for feature flag '#{feature_flag_key}'. Returning the default variable value '#{variable_value}'.")
end
variable_value
end

def user_inputs_valid?(attributes = nil, event_tags = nil)
# Helper method to validate user inputs.
#
Expand Down
13 changes: 12 additions & 1 deletion lib/optimizely/config/datafile_project_config.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

# Copyright 2019, Optimizely and contributors
# Copyright 2019-2020, Optimizely and contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -82,6 +82,17 @@ def initialize(datafile, logger, error_handler)
@revision = config['revision']
@rollouts = config.fetch('rollouts', [])

# Json type is represented in datafile as a subtype of string for the sake of backwards compatibility.
# Converting it to a first-class json type while creating Project Config
@feature_flags.each do |feature_flag|
feature_flag['variables'].each do |variable|
if variable['type'] == 'string' && variable['subType'] == 'json'
variable['type'] = 'json'
variable.delete('subType')
Copy link
Contributor

Choose a reason for hiding this comment

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

I realize why. I just :(

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah i agree. it looks ugly.

end
end
end

# Utility maps for quick lookup
@attribute_key_map = generate_key_map(@attributes, 'key')
@event_key_map = generate_key_map(@events, 'key')
Expand Down
8 changes: 5 additions & 3 deletions lib/optimizely/helpers/constants.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

#
# Copyright 2016-2019, Optimizely and contributors
# Copyright 2016-2020, Optimizely and contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -304,7 +304,8 @@ module Constants
'BOOLEAN' => 'boolean',
'DOUBLE' => 'double',
'INTEGER' => 'integer',
'STRING' => 'string'
'STRING' => 'string',
'JSON' => 'json'
}.freeze

INPUT_VARIABLES = {
Expand Down Expand Up @@ -357,7 +358,8 @@ module Constants
'AB_TEST' => 'ab-test',
'FEATURE' => 'feature',
'FEATURE_TEST' => 'feature-test',
'FEATURE_VARIABLE' => 'feature-variable'
'FEATURE_VARIABLE' => 'feature-variable',
'ALL_FEATURE_VARIABLES' => 'all-feature-variables'
}.freeze

CONFIG_MANAGER = {
Expand Down
9 changes: 8 additions & 1 deletion lib/optimizely/helpers/variable_type.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

#
# Copyright 2017, Optimizely and contributors
# Copyright 2017, 2020, Optimizely and contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -48,6 +48,13 @@ def cast_value_to_type(value, variable_type, logger)
logger.log(Logger::ERROR, "Unable to cast variable value '#{value}' to type "\
"'#{variable_type}': #{e.message}.")
end
when 'json'
begin
return_value = JSON.parse(value)
rescue => e
logger.log(Logger::ERROR, "Unable to cast variable value '#{value}' to type "\
"'#{variable_type}': #{e.message}.")
end
else
# default case is string
return_value = value
Expand Down
Loading