Skip to content

Commit c1d0cb7

Browse files
authored
feat: Added new getFeatureVariableJson and getAllFeatureVariables Apis (#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
1 parent 8c14502 commit c1d0cb7

File tree

8 files changed

+733
-47
lines changed

8 files changed

+733
-47
lines changed

lib/optimizely.rb

Lines changed: 144 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,32 @@ def get_feature_variable_string(feature_flag_key, variable_key, user_id, attribu
430430
variable_value
431431
end
432432

433+
# Get the Json value of the specified variable in the feature flag in a Dict.
434+
#
435+
# @param feature_flag_key - String key of feature flag the variable belongs to
436+
# @param variable_key - String key of variable for which we are getting the string value
437+
# @param user_id - String user ID
438+
# @param attributes - Hash representing visitor attributes and values which need to be recorded.
439+
#
440+
# @return [Dict] the Dict containing variable value.
441+
# @return [nil] if the feature flag or variable are not found.
442+
443+
def get_feature_variable_json(feature_flag_key, variable_key, user_id, attributes = nil)
444+
unless is_valid
445+
@logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_feature_variable_json').message)
446+
return nil
447+
end
448+
variable_value = get_feature_variable_for_type(
449+
feature_flag_key,
450+
variable_key,
451+
Optimizely::Helpers::Constants::VARIABLE_TYPES['JSON'],
452+
user_id,
453+
attributes
454+
)
455+
456+
variable_value
457+
end
458+
433459
# Get the Boolean value of the specified variable in the feature flag.
434460
#
435461
# @param feature_flag_key - String key of feature flag the variable belongs to
@@ -484,6 +510,71 @@ def get_feature_variable_double(feature_flag_key, variable_key, user_id, attribu
484510
variable_value
485511
end
486512

513+
# Get values of all the variables in the feature flag and returns them in a Dict
514+
#
515+
# @param feature_flag_key - String key of feature flag
516+
# @param user_id - String user ID
517+
# @param attributes - Hash representing visitor attributes and values which need to be recorded.
518+
#
519+
# @return [Dict] the Dict containing all the varible values
520+
# @return [nil] if the feature flag is not found.
521+
522+
def get_all_feature_variables(feature_flag_key, user_id, attributes = nil)
523+
unless is_valid
524+
@logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_all_feature_variables').message)
525+
return nil
526+
end
527+
528+
return nil unless Optimizely::Helpers::Validator.inputs_valid?(
529+
{
530+
feature_flag_key: feature_flag_key,
531+
user_id: user_id
532+
},
533+
@logger, Logger::ERROR
534+
)
535+
536+
return nil unless user_inputs_valid?(attributes)
537+
538+
config = project_config
539+
540+
feature_flag = config.get_feature_flag_from_key(feature_flag_key)
541+
unless feature_flag
542+
@logger.log(Logger::INFO, "No feature flag was found for key '#{feature_flag_key}'.")
543+
return nil
544+
end
545+
546+
decision = @decision_service.get_variation_for_feature(config, feature_flag, user_id, attributes)
547+
variation = decision ? decision['variation'] : nil
548+
feature_enabled = variation ? variation['featureEnabled'] : false
549+
all_variables = {}
550+
551+
feature_flag['variables'].each do |variable|
552+
variable_value = get_feature_variable_for_variation(feature_flag_key, feature_enabled, variation, variable, user_id)
553+
all_variables[variable['key']] = Helpers::VariableType.cast_value_to_type(variable_value, variable['type'], @logger)
554+
end
555+
556+
source_string = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
557+
if decision && decision['source'] == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
558+
source_info = {
559+
experiment_key: decision.experiment['key'],
560+
variation_key: variation['key']
561+
}
562+
source_string = Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
563+
end
564+
565+
@notification_center.send_notifications(
566+
NotificationCenter::NOTIFICATION_TYPES[:DECISION],
567+
Helpers::Constants::DECISION_NOTIFICATION_TYPES['ALL_FEATURE_VARIABLES'], user_id, (attributes || {}),
568+
feature_key: feature_flag_key,
569+
feature_enabled: feature_enabled,
570+
source: source_string,
571+
variable_values: all_variables,
572+
source_info: source_info || {}
573+
)
574+
575+
all_variables
576+
end
577+
487578
# Get the Integer value of the specified variable in the feature flag.
488579
#
489580
# @param feature_flag_key - String key of feature flag the variable belongs to
@@ -649,52 +740,31 @@ def get_feature_variable_for_type(feature_flag_key, variable_key, variable_type,
649740
# Error message logged in DatafileProjectConfig- get_feature_flag_from_key
650741
return nil if variable.nil?
651742

652-
feature_enabled = false
653-
654743
# If variable_type is nil, set it equal to variable['type']
655744
variable_type ||= variable['type']
656745
# Returns nil if type differs
657746
if variable['type'] != variable_type
658747
@logger.log(Logger::WARN,
659748
"Requested variable as type '#{variable_type}' but variable '#{variable_key}' is of type '#{variable['type']}'.")
660749
return nil
661-
else
662-
source_string = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
663-
decision = @decision_service.get_variation_for_feature(config, feature_flag, user_id, attributes)
664-
variable_value = variable['defaultValue']
665-
if decision
666-
variation = decision['variation']
667-
if decision['source'] == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
668-
source_info = {
669-
experiment_key: decision.experiment['key'],
670-
variation_key: variation['key']
671-
}
672-
source_string = Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
673-
end
674-
feature_enabled = variation['featureEnabled']
675-
if feature_enabled == true
676-
variation_variable_usages = config.variation_id_to_variable_usage_map[variation['id']]
677-
variable_id = variable['id']
678-
if variation_variable_usages&.key?(variable_id)
679-
variable_value = variation_variable_usages[variable_id]['value']
680-
@logger.log(Logger::INFO,
681-
"Got variable value '#{variable_value}' for variable '#{variable_key}' of feature flag '#{feature_flag_key}'.")
682-
else
683-
@logger.log(Logger::DEBUG,
684-
"Variable '#{variable_key}' is not used in variation '#{variation['key']}'. Returning the default variable value '#{variable_value}'.")
685-
end
686-
else
687-
@logger.log(Logger::DEBUG,
688-
"Feature '#{feature_flag_key}' for variation '#{variation['key']}' is not enabled. Returning the default variable value '#{variable_value}'.")
689-
end
690-
else
691-
@logger.log(Logger::INFO,
692-
"User '#{user_id}' was not bucketed into any variation for feature flag '#{feature_flag_key}'. Returning the default variable value '#{variable_value}'.")
693-
end
694750
end
695751

752+
decision = @decision_service.get_variation_for_feature(config, feature_flag, user_id, attributes)
753+
variation = decision ? decision['variation'] : nil
754+
feature_enabled = variation ? variation['featureEnabled'] : false
755+
756+
variable_value = get_feature_variable_for_variation(feature_flag_key, feature_enabled, variation, variable, user_id)
696757
variable_value = Helpers::VariableType.cast_value_to_type(variable_value, variable_type, @logger)
697758

759+
source_string = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
760+
if decision && decision['source'] == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
761+
source_info = {
762+
experiment_key: decision.experiment['key'],
763+
variation_key: variation['key']
764+
}
765+
source_string = Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
766+
end
767+
698768
@notification_center.send_notifications(
699769
NotificationCenter::NOTIFICATION_TYPES[:DECISION],
700770
Helpers::Constants::DECISION_NOTIFICATION_TYPES['FEATURE_VARIABLE'], user_id, (attributes || {}),
@@ -710,6 +780,45 @@ def get_feature_variable_for_type(feature_flag_key, variable_key, variable_type,
710780
variable_value
711781
end
712782

783+
def get_feature_variable_for_variation(feature_flag_key, feature_enabled, variation, variable, user_id)
784+
# Helper method to get the non type-casted value for a variable attached to a
785+
# feature flag. Returns appropriate variable value depending on whether there
786+
# was a matching variation, feature was enabled or not or varible was part of the
787+
# available variation or not. Also logs the appropriate message explaining how it
788+
# evaluated the value of the variable.
789+
#
790+
# feature_flag_key - String key of feature flag the variable belongs to
791+
# feature_enabled - Boolean indicating if feature is enabled or not
792+
# variation - varition returned by decision service
793+
# user_id - String user ID
794+
#
795+
# Returns string value of the variable.
796+
797+
config = project_config
798+
variable_value = variable['defaultValue']
799+
if variation
800+
if feature_enabled == true
801+
variation_variable_usages = config.variation_id_to_variable_usage_map[variation['id']]
802+
variable_id = variable['id']
803+
if variation_variable_usages&.key?(variable_id)
804+
variable_value = variation_variable_usages[variable_id]['value']
805+
@logger.log(Logger::INFO,
806+
"Got variable value '#{variable_value}' for variable '#{variable['key']}' of feature flag '#{feature_flag_key}'.")
807+
else
808+
@logger.log(Logger::DEBUG,
809+
"Variable '#{variable['key']}' is not used in variation '#{variation['key']}'. Returning the default variable value '#{variable_value}'.")
810+
end
811+
else
812+
@logger.log(Logger::DEBUG,
813+
"Feature '#{feature_flag_key}' for variation '#{variation['key']}' is not enabled. Returning the default variable value '#{variable_value}'.")
814+
end
815+
else
816+
@logger.log(Logger::INFO,
817+
"User '#{user_id}' was not bucketed into any variation for feature flag '#{feature_flag_key}'. Returning the default variable value '#{variable_value}'.")
818+
end
819+
variable_value
820+
end
821+
713822
def user_inputs_valid?(attributes = nil, event_tags = nil)
714823
# Helper method to validate user inputs.
715824
#

lib/optimizely/config/datafile_project_config.rb

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# frozen_string_literal: true
22

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

85+
# Json type is represented in datafile as a subtype of string for the sake of backwards compatibility.
86+
# Converting it to a first-class json type while creating Project Config
87+
@feature_flags.each do |feature_flag|
88+
feature_flag['variables'].each do |variable|
89+
if variable['type'] == 'string' && variable['subType'] == 'json'
90+
variable['type'] = 'json'
91+
variable.delete('subType')
92+
end
93+
end
94+
end
95+
8596
# Utility maps for quick lookup
8697
@attribute_key_map = generate_key_map(@attributes, 'key')
8798
@event_key_map = generate_key_map(@events, 'key')

lib/optimizely/helpers/constants.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# frozen_string_literal: true
22

33
#
4-
# Copyright 2016-2019, Optimizely and contributors
4+
# Copyright 2016-2020, Optimizely and contributors
55
#
66
# Licensed under the Apache License, Version 2.0 (the "License");
77
# you may not use this file except in compliance with the License.
@@ -304,7 +304,8 @@ module Constants
304304
'BOOLEAN' => 'boolean',
305305
'DOUBLE' => 'double',
306306
'INTEGER' => 'integer',
307-
'STRING' => 'string'
307+
'STRING' => 'string',
308+
'JSON' => 'json'
308309
}.freeze
309310

310311
INPUT_VARIABLES = {
@@ -357,7 +358,8 @@ module Constants
357358
'AB_TEST' => 'ab-test',
358359
'FEATURE' => 'feature',
359360
'FEATURE_TEST' => 'feature-test',
360-
'FEATURE_VARIABLE' => 'feature-variable'
361+
'FEATURE_VARIABLE' => 'feature-variable',
362+
'ALL_FEATURE_VARIABLES' => 'all-feature-variables'
361363
}.freeze
362364

363365
CONFIG_MANAGER = {

lib/optimizely/helpers/variable_type.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# frozen_string_literal: true
22

33
#
4-
# Copyright 2017, Optimizely and contributors
4+
# Copyright 2017, 2020, Optimizely and contributors
55
#
66
# Licensed under the Apache License, Version 2.0 (the "License");
77
# you may not use this file except in compliance with the License.
@@ -48,6 +48,13 @@ def cast_value_to_type(value, variable_type, logger)
4848
logger.log(Logger::ERROR, "Unable to cast variable value '#{value}' to type "\
4949
"'#{variable_type}': #{e.message}.")
5050
end
51+
when 'json'
52+
begin
53+
return_value = JSON.parse(value)
54+
rescue => e
55+
logger.log(Logger::ERROR, "Unable to cast variable value '#{value}' to type "\
56+
"'#{variable_type}': #{e.message}.")
57+
end
5158
else
5259
# default case is string
5360
return_value = value

0 commit comments

Comments
 (0)