Skip to content

Commit

Permalink
feat: Added new getFeatureVariableJson and getAllFeatureVariables Apis (
Browse files Browse the repository at this point in the history
#251)

* Implemented getFeatureVariableJson

* moved common logic to a separate function to support get_all_feature_variables

* added get_all_feature_variables and its tests

* added notifications for get_all_feature_variables and added unit tests

* added doc comments and updated copyright information

* removed whitespaces

* fixed a minor nit

* removed unnecessary order

* refactored a test

* added notification listener tests for get_feature_variable_json

* added source info verification to the tests for feature tests
  • Loading branch information
zashraf1985 authored May 13, 2020
1 parent 8c14502 commit c1d0cb7
Show file tree
Hide file tree
Showing 8 changed files with 733 additions and 47 deletions.
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 = {
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')
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

0 comments on commit c1d0cb7

Please sign in to comment.