optimizely-sdk 5.1.0 → 5.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,12 +27,14 @@ module Optimizely
27
27
  attr_reader :datafile, :account_id, :attributes, :audiences, :typed_audiences, :events,
28
28
  :experiments, :feature_flags, :groups, :project_id, :bot_filtering, :revision,
29
29
  :sdk_key, :environment_key, :rollouts, :version, :send_flag_decisions,
30
- :attribute_key_map, :audience_id_map, :event_key_map, :experiment_feature_map,
30
+ :attribute_key_map, :attribute_id_to_key_map, :attribute_id_map,
31
+ :audience_id_map, :event_key_map, :experiment_feature_map,
31
32
  :experiment_id_map, :experiment_key_map, :feature_flag_key_map, :feature_variable_key_map,
32
33
  :group_id_map, :rollout_id_map, :rollout_experiment_id_map, :variation_id_map,
33
34
  :variation_id_to_variable_usage_map, :variation_key_map, :variation_id_map_by_experiment_id,
34
35
  :variation_key_map_by_experiment_id, :flag_variation_map, :integration_key_map, :integrations,
35
- :public_key_for_odp, :host_for_odp, :all_segments
36
+ :public_key_for_odp, :host_for_odp, :all_segments, :region, :holdouts, :holdout_id_map,
37
+ :global_holdouts, :included_holdouts, :excluded_holdouts, :flag_holdouts_map
36
38
  # Boolean - denotes if Optimizely should remove the last block of visitors' IP address before storing event data
37
39
  attr_reader :anonymize_ip
38
40
 
@@ -68,6 +70,11 @@ module Optimizely
68
70
  @rollouts = config.fetch('rollouts', [])
69
71
  @send_flag_decisions = config.fetch('sendFlagDecisions', false)
70
72
  @integrations = config.fetch('integrations', [])
73
+ @region = config.fetch('region', 'US')
74
+ @holdouts = config.fetch('holdouts', [])
75
+
76
+ # Default to US region if not specified
77
+ @region = 'US' if @region.nil? || @region.empty?
71
78
 
72
79
  # Json type is represented in datafile as a subtype of string for the sake of backwards compatibility.
73
80
  # Converting it to a first-class json type while creating Project Config
@@ -82,6 +89,11 @@ module Optimizely
82
89
 
83
90
  # Utility maps for quick lookup
84
91
  @attribute_key_map = generate_key_map(@attributes, 'key')
92
+ @attribute_id_map = generate_key_map(@attributes, 'id')
93
+ @attribute_id_to_key_map = {}
94
+ @attributes.each do |attribute|
95
+ @attribute_id_to_key_map[attribute['id']] = attribute['key']
96
+ end
85
97
  @event_key_map = generate_key_map(@events, 'key')
86
98
  @group_id_map = generate_key_map(@groups, 'id')
87
99
  @group_id_map.each do |key, group|
@@ -102,6 +114,45 @@ module Optimizely
102
114
  @variation_id_to_variable_usage_map = {}
103
115
  @variation_id_to_experiment_map = {}
104
116
  @flag_variation_map = {}
117
+ @holdout_id_map = {}
118
+ @global_holdouts = []
119
+ @included_holdouts = {}
120
+ @excluded_holdouts = {}
121
+ @flag_holdouts_map = {}
122
+
123
+ @holdouts.each do |holdout|
124
+ next unless holdout['status'] == 'Running'
125
+
126
+ # Ensure holdout has layerId field (holdouts don't have campaigns)
127
+ holdout['layerId'] ||= ''
128
+
129
+ @holdout_id_map[holdout['id']] = holdout
130
+
131
+ included_flags = holdout['includedFlags'] || []
132
+ excluded_flags = holdout['excludedFlags'] || []
133
+
134
+ case [included_flags.empty?, excluded_flags.empty?]
135
+ when [true, true]
136
+ # No included or excluded flags - this is a global holdout
137
+ @global_holdouts << holdout
138
+
139
+ when [false, true], [false, false]
140
+ # Has included flags - add to included_holdouts map
141
+ included_flags.each do |flag_id|
142
+ @included_holdouts[flag_id] ||= []
143
+ @included_holdouts[flag_id] << holdout
144
+ end
145
+
146
+ when [true, false]
147
+ # No included flags but has excluded flags - global with exclusions
148
+ @global_holdouts << holdout
149
+
150
+ excluded_flags.each do |flag_id|
151
+ @excluded_holdouts[flag_id] ||= []
152
+ @excluded_holdouts[flag_id] << holdout
153
+ end
154
+ end
155
+ end
105
156
 
106
157
  @experiment_id_map.each_value do |exp|
107
158
  # Excludes experiments from rollouts
@@ -155,6 +206,31 @@ module Optimizely
155
206
  @experiment_feature_map[experiment_id] = [feature_flag['id']]
156
207
  end
157
208
  end
209
+
210
+ # Adding Holdout variations in variation id and key maps
211
+ return unless @holdouts && !@holdouts.empty?
212
+
213
+ @holdouts.each do |holdout|
214
+ next unless holdout['status'] == 'Running'
215
+
216
+ holdout_key = holdout['key']
217
+ holdout_id = holdout['id']
218
+
219
+ @variation_key_map[holdout_key] = {}
220
+ @variation_id_map[holdout_key] = {}
221
+ @variation_id_map_by_experiment_id[holdout_id] = {}
222
+ @variation_key_map_by_experiment_id[holdout_id] = {}
223
+
224
+ variations = holdout['variations']
225
+ next unless variations && !variations.empty?
226
+
227
+ variations.each do |variation|
228
+ @variation_key_map[holdout_key][variation['key']] = variation
229
+ @variation_id_map[holdout_key][variation['id']] = variation
230
+ @variation_key_map_by_experiment_id[holdout_id][variation['key']] = variation
231
+ @variation_id_map_by_experiment_id[holdout_id][variation['id']] = variation
232
+ end
233
+ end
158
234
  end
159
235
 
160
236
  def get_rules_for_flag(feature_flag)
@@ -440,6 +516,40 @@ module Optimizely
440
516
  nil
441
517
  end
442
518
 
519
+ def get_attribute_by_key(attribute_key)
520
+ # Get attribute for the provided attribute key.
521
+ #
522
+ # Args:
523
+ # Attribute key for which attribute is to be fetched.
524
+ #
525
+ # Returns:
526
+ # Attribute corresponding to the provided attribute key.
527
+ attribute = @attribute_key_map[attribute_key]
528
+ return attribute if attribute
529
+
530
+ invalid_attribute_error = InvalidAttributeError.new(attribute_key)
531
+ @logger.log Logger::ERROR, invalid_attribute_error.message
532
+ @error_handler.handle_error invalid_attribute_error
533
+ nil
534
+ end
535
+
536
+ def get_attribute_key_by_id(attribute_id)
537
+ # Get attribute key for the provided attribute ID.
538
+ #
539
+ # Args:
540
+ # Attribute ID for which attribute is to be fetched.
541
+ #
542
+ # Returns:
543
+ # Attribute key corresponding to the provided attribute ID.
544
+ attribute = @attribute_id_to_key_map[attribute_id]
545
+ return attribute if attribute
546
+
547
+ invalid_attribute_error = InvalidAttributeError.new(attribute_id)
548
+ @logger.log Logger::ERROR, invalid_attribute_error.message
549
+ @error_handler.handle_error invalid_attribute_error
550
+ nil
551
+ end
552
+
443
553
  def variation_id_exists?(experiment_id, variation_id)
444
554
  # Determines if a given experiment ID / variation ID pair exists in the datafile
445
555
  #
@@ -524,6 +634,60 @@ module Optimizely
524
634
  @rollout_experiment_id_map.key?(experiment_id)
525
635
  end
526
636
 
637
+ def get_holdouts_for_flag(flag_id)
638
+ # Helper method to get holdouts from an applied feature flag
639
+ #
640
+ # flag_id - (REQUIRED) ID of the feature flag
641
+ # This parameter is required and should not be null/nil
642
+ #
643
+ # Returns the holdouts that apply for a specific flag
644
+
645
+ return [] if @holdouts.nil? || @holdouts.empty?
646
+
647
+ # Check cache first (before validation, so we cache the validation result too)
648
+ return @flag_holdouts_map[flag_id] if @flag_holdouts_map.key?(flag_id)
649
+
650
+ # Validate that the flag exists in the datafile
651
+ flag_exists = @feature_flags.any? { |flag| flag['id'] == flag_id }
652
+ unless flag_exists
653
+ # Cache the empty result for non-existent flags
654
+ @flag_holdouts_map[flag_id] = []
655
+ return []
656
+ end
657
+
658
+ # Prioritize global holdouts first
659
+ excluded = @excluded_holdouts[flag_id] || []
660
+
661
+ active_holdouts = if excluded.any?
662
+ @global_holdouts.reject { |holdout| excluded.include?(holdout) }
663
+ else
664
+ @global_holdouts.dup
665
+ end
666
+
667
+ # Append included holdouts
668
+ included = @included_holdouts[flag_id] || []
669
+ active_holdouts.concat(included)
670
+
671
+ # Cache the result
672
+ @flag_holdouts_map[flag_id] = active_holdouts
673
+
674
+ @flag_holdouts_map[flag_id] || []
675
+ end
676
+
677
+ def get_holdout(holdout_id)
678
+ # Helper method to get holdout from holdout ID
679
+ #
680
+ # holdout_id - ID of the holdout
681
+ #
682
+ # Returns the holdout
683
+
684
+ holdout = @holdout_id_map[holdout_id]
685
+ return holdout if holdout
686
+
687
+ @logger.log Logger::ERROR, "Holdout with ID '#{holdout_id}' not found."
688
+ nil
689
+ end
690
+
527
691
  private
528
692
 
529
693
  def generate_feature_variation_map(feature_flags)
@@ -326,7 +326,7 @@ module Optimizely
326
326
  unless url
327
327
  url_template ||= @access_token.nil? ? Helpers::Constants::CONFIG_MANAGER['DATAFILE_URL_TEMPLATE'] : Helpers::Constants::CONFIG_MANAGER['AUTHENTICATED_DATAFILE_URL_TEMPLATE']
328
328
  begin
329
- return (url_template % sdk_key)
329
+ return url_template % sdk_key
330
330
  rescue
331
331
  error_msg = "Invalid url_template #{url_template} provided."
332
332
  @logger.log(Logger::ERROR, error_msg)
@@ -23,6 +23,9 @@ module Optimizely
23
23
  IGNORE_USER_PROFILE_SERVICE = 'IGNORE_USER_PROFILE_SERVICE'
24
24
  INCLUDE_REASONS = 'INCLUDE_REASONS'
25
25
  EXCLUDE_VARIABLES = 'EXCLUDE_VARIABLES'
26
+ IGNORE_CMAB_CACHE = 'IGNORE_CMAB_CACHE'
27
+ RESET_CMAB_CACHE = 'RESET_CMAB_CACHE'
28
+ INVALIDATE_USER_CMAB_CACHE = 'INVALIDATE_USER_CMAB_CACHE'
26
29
  end
27
30
  end
28
31
  end
@@ -55,6 +55,25 @@ module Optimizely
55
55
  def to_json(*args)
56
56
  as_json.to_json(*args)
57
57
  end
58
+
59
+ # Create a new OptimizelyDecision representing an error state.
60
+ #
61
+ # @param key [String] The flag key
62
+ # @param user [OptimizelyUserContext] The user context
63
+ # @param reasons [Array<String>] List of reasons explaining the error
64
+ #
65
+ # @return [OptimizelyDecision] OptimizelyDecision with error state values
66
+ def self.new_error_decision(key, user, reasons = [])
67
+ new(
68
+ variation_key: nil,
69
+ enabled: false,
70
+ variables: {},
71
+ rule_key: nil,
72
+ flag_key: key,
73
+ user_context: user,
74
+ reasons: reasons
75
+ )
76
+ end
58
77
  end
59
78
  end
60
79
  end