optimizely-sdk 5.1.0 → 5.2.0

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.
@@ -201,6 +201,12 @@ module Optimizely
201
201
  },
202
202
  'forcedVariations' => {
203
203
  'type' => 'object'
204
+ },
205
+ 'cmab' => {
206
+ 'type' => 'object'
207
+ },
208
+ 'holdouts' => {
209
+ 'type' => 'array'
204
210
  }
205
211
  },
206
212
  'required' => %w[
@@ -303,6 +309,43 @@ module Optimizely
303
309
  },
304
310
  'required' => %w[key]
305
311
  }
312
+ },
313
+ 'cmab' => {
314
+ 'type' => 'object',
315
+ 'properties' => {
316
+ 'attributeIds' => {
317
+ 'type' => 'array',
318
+ 'items' => {'type' => 'string'}
319
+ },
320
+ 'trafficAllocation' => {
321
+ 'type' => 'integer'
322
+ }
323
+ }
324
+ },
325
+ 'holdouts' => {
326
+ 'type' => 'array',
327
+ 'items' => {
328
+ 'type' => 'object',
329
+ 'properties' => {
330
+ 'id' => {
331
+ 'type' => 'string'
332
+ },
333
+ 'key' => {
334
+ 'type' => 'string'
335
+ },
336
+ 'status' => {
337
+ 'type' => 'string'
338
+ },
339
+ 'includedFlags' => {
340
+ 'type' => 'array',
341
+ 'items' => {'type' => 'string'}
342
+ },
343
+ 'excludedFlags' => {
344
+ 'type' => 'array',
345
+ 'items' => {'type' => 'string'}
346
+ }
347
+ }
348
+ }
306
349
  }
307
350
  },
308
351
  'required' => %w[
@@ -454,6 +497,9 @@ module Optimizely
454
497
  'IF_MODIFIED_SINCE' => 'If-Modified-Since',
455
498
  'LAST_MODIFIED' => 'Last-Modified'
456
499
  }.freeze
500
+
501
+ CMAB_FETCH_FAILED = 'CMAB decision fetch failed (%s).'
502
+ INVALID_CMAB_FETCH_RESPONSE = 'Invalid CMAB fetch response'
457
503
  end
458
504
  end
459
505
  end
@@ -22,7 +22,7 @@ module Optimizely
22
22
  module Helpers
23
23
  class OptimizelySdkSettings
24
24
  attr_accessor :odp_disabled, :segments_cache_size, :segments_cache_timeout_in_secs, :odp_segments_cache, :odp_segment_manager,
25
- :odp_event_manager, :fetch_segments_timeout, :odp_event_timeout, :odp_flush_interval
25
+ :odp_event_manager, :fetch_segments_timeout, :odp_event_timeout, :odp_flush_interval, :cmab_prediction_endpoint
26
26
 
27
27
  # Contains configuration used for Optimizely Project initialization.
28
28
  #
@@ -35,6 +35,7 @@ module Optimizely
35
35
  # @param odp_segment_request_timeout - Time to wait in seconds for fetch_qualified_segments (optional. default = 10).
36
36
  # @param odp_event_request_timeout - Time to wait in seconds for send_odp_events (optional. default = 10).
37
37
  # @param odp_event_flush_interval - Time to wait in seconds for odp events to accumulate before sending (optional. default = 1).
38
+ # @param cmab_prediction_endpoint - Custom CMAB prediction endpoint URL template (optional). Use %s as placeholder for rule_id. Defaults to production endpoint if not provided.
38
39
  def initialize(
39
40
  disable_odp: false,
40
41
  segments_cache_size: Constants::ODP_SEGMENTS_CACHE_CONFIG[:DEFAULT_CAPACITY],
@@ -44,7 +45,8 @@ module Optimizely
44
45
  odp_event_manager: nil,
45
46
  odp_segment_request_timeout: nil,
46
47
  odp_event_request_timeout: nil,
47
- odp_event_flush_interval: nil
48
+ odp_event_flush_interval: nil,
49
+ cmab_prediction_endpoint: nil
48
50
  )
49
51
  @odp_disabled = disable_odp
50
52
  @segments_cache_size = segments_cache_size
@@ -55,6 +57,7 @@ module Optimizely
55
57
  @fetch_segments_timeout = odp_segment_request_timeout
56
58
  @odp_event_timeout = odp_event_request_timeout
57
59
  @odp_flush_interval = odp_event_flush_interval
60
+ @cmab_prediction_endpoint = cmab_prediction_endpoint
58
61
  end
59
62
  end
60
63
  end
@@ -122,11 +122,11 @@ module Optimizely
122
122
 
123
123
  return false unless variables.respond_to?(:each) && !variables.empty?
124
124
 
125
- is_valid = true # rubocop:disable Lint/UselessAssignment
125
+ is_valid = true
126
126
  if variables.include? :user_id
127
127
  # Empty str is a valid user ID.
128
128
  unless variables[:user_id].is_a?(String)
129
- is_valid = false # rubocop:disable Lint/UselessAssignment
129
+ is_valid = false
130
130
  logger.log(level, "#{Constants::INPUT_VARIABLES['USER_ID']} is invalid")
131
131
  end
132
132
  variables.delete :user_id
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2022, Optimizely and contributors
4
+ # Copyright 2022-2025, Optimizely and contributors
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
7
7
  # you may not use this file except in compliance with the License.
@@ -91,6 +91,19 @@ module Optimizely
91
91
 
92
92
  @cache_mutex.synchronize { @map[key]&.value }
93
93
  end
94
+
95
+ # Remove the element associated with the provided key from the cache
96
+ #
97
+ # @param key - The key to remove
98
+
99
+ def remove(key)
100
+ return if @capacity <= 0
101
+
102
+ @cache_mutex.synchronize do
103
+ @map.delete(key)
104
+ end
105
+ nil
106
+ end
94
107
  end
95
108
 
96
109
  class CacheElement
@@ -22,6 +22,8 @@ require 'optimizely/event_dispatcher'
22
22
  require 'optimizely/event/batch_event_processor'
23
23
  require 'optimizely/logger'
24
24
  require 'optimizely/notification_center'
25
+ require 'optimizely/cmab/cmab_client'
26
+ require 'optimizely/cmab/cmab_service'
25
27
 
26
28
  module Optimizely
27
29
  class OptimizelyFactory
@@ -83,6 +85,46 @@ module Optimizely
83
85
  @blocking_timeout = blocking_timeout
84
86
  end
85
87
 
88
+ # Convenience method for setting CMAB cache size.
89
+ # @param cache_size Integer - Maximum number of items in CMAB cache.
90
+ # @param logger - Optional LoggerInterface Provides a log method to log messages.
91
+ def self.cmab_cache_size(cache_size, logger = NoOpLogger.new)
92
+ unless cache_size.is_a?(Integer) && cache_size.positive?
93
+ logger.log(
94
+ Logger::ERROR,
95
+ "CMAB cache size is invalid, setting to default size #{Optimizely::DefaultCmabCacheOptions::DEFAULT_CMAB_CACHE_SIZE}."
96
+ )
97
+ return
98
+ end
99
+ @cmab_cache_size = cache_size
100
+ end
101
+
102
+ # Convenience method for setting CMAB cache TTL.
103
+ # @param cache_ttl Numeric - Time in seconds for cache entries to live.
104
+ # @param logger - Optional LoggerInterface Provides a log method to log messages.
105
+ def self.cmab_cache_ttl(cache_ttl, logger = NoOpLogger.new)
106
+ unless cache_ttl.is_a?(Numeric) && cache_ttl.positive?
107
+ logger.log(
108
+ Logger::ERROR,
109
+ "CMAB cache TTL is invalid, setting to default TTL #{Optimizely::DefaultCmabCacheOptions::DEFAULT_CMAB_CACHE_TIMEOUT}."
110
+ )
111
+ return
112
+ end
113
+ @cmab_cache_ttl = cache_ttl
114
+ end
115
+
116
+ # Convenience method for setting custom CMAB cache.
117
+ # @param custom_cache - Cache implementation responding to lookup, save, remove, and reset methods.
118
+ def self.cmab_custom_cache(custom_cache)
119
+ @cmab_custom_cache = custom_cache
120
+ end
121
+
122
+ # Convenience method for setting custom CMAB prediction endpoint.
123
+ # @param prediction_endpoint String - Custom URL template for CMAB prediction API. Use %s as placeholder for rule_id.
124
+ def self.cmab_prediction_endpoint(prediction_endpoint)
125
+ @cmab_prediction_endpoint = prediction_endpoint
126
+ end
127
+
86
128
  # Returns a new optimizely instance.
87
129
  #
88
130
  # @params sdk_key - Required String uniquely identifying the fallback datafile corresponding to project.
@@ -165,6 +207,18 @@ module Optimizely
165
207
  notification_center: notification_center
166
208
  )
167
209
 
210
+ # Initialize CMAB components
211
+ cmab_prediction_endpoint = nil
212
+ cmab_prediction_endpoint = settings.cmab_prediction_endpoint if settings&.cmab_prediction_endpoint
213
+ cmab_prediction_endpoint ||= @cmab_prediction_endpoint
214
+
215
+ cmab_client = DefaultCmabClient.new(logger: logger, prediction_endpoint: cmab_prediction_endpoint)
216
+ cmab_cache = @cmab_custom_cache || LRUCache.new(
217
+ @cmab_cache_size || Optimizely::DefaultCmabCacheOptions::DEFAULT_CMAB_CACHE_SIZE,
218
+ @cmab_cache_ttl || Optimizely::DefaultCmabCacheOptions::DEFAULT_CMAB_CACHE_TIMEOUT
219
+ )
220
+ cmab_service = DefaultCmabService.new(cmab_cache, cmab_client, logger)
221
+
168
222
  Optimizely::Project.new(
169
223
  datafile: datafile,
170
224
  event_dispatcher: event_dispatcher,
@@ -176,7 +230,8 @@ module Optimizely
176
230
  config_manager: config_manager,
177
231
  notification_center: notification_center,
178
232
  event_processor: event_processor,
179
- settings: settings
233
+ settings: settings,
234
+ cmab_service: cmab_service
180
235
  )
181
236
  end
182
237
  end
@@ -62,6 +62,8 @@ module Optimizely
62
62
 
63
63
  def all_segments; end
64
64
 
65
+ def region; end
66
+
65
67
  def experiment_running?(experiment); end
66
68
 
67
69
  def get_experiment_from_key(experiment_key); end
@@ -86,6 +88,10 @@ module Optimizely
86
88
 
87
89
  def get_attribute_id(attribute_key); end
88
90
 
91
+ def get_attribute_by_key(attribute_key); end
92
+
93
+ def get_attribute_key_by_id(attribute_id); end
94
+
89
95
  def variation_id_exists?(experiment_id, variation_id); end
90
96
 
91
97
  def get_feature_flag_from_key(feature_flag_key); end
@@ -17,5 +17,5 @@
17
17
  #
18
18
  module Optimizely
19
19
  CLIENT_ENGINE = 'ruby-sdk'
20
- VERSION = '5.1.0'
20
+ VERSION = '5.2.0'
21
21
  end
data/lib/optimizely.rb CHANGED
@@ -43,6 +43,8 @@ require_relative 'optimizely/odp/lru_cache'
43
43
  require_relative 'optimizely/odp/odp_manager'
44
44
  require_relative 'optimizely/helpers/sdk_settings'
45
45
  require_relative 'optimizely/user_profile_tracker'
46
+ require_relative 'optimizely/cmab/cmab_client'
47
+ require_relative 'optimizely/cmab/cmab_service'
46
48
 
47
49
  module Optimizely
48
50
  class Project
@@ -84,7 +86,8 @@ module Optimizely
84
86
  event_processor: nil,
85
87
  default_decide_options: [],
86
88
  event_processor_options: {},
87
- settings: nil
89
+ settings: nil,
90
+ cmab_service: nil
88
91
  )
89
92
  @logger = logger || NoOpLogger.new
90
93
  @error_handler = error_handler || NoOpErrorHandler.new
@@ -131,7 +134,24 @@ module Optimizely
131
134
 
132
135
  setup_odp!(@config_manager.sdk_key)
133
136
 
134
- @decision_service = DecisionService.new(@logger, @user_profile_service)
137
+ # Initialize CMAB components if cmab service is nil
138
+ if cmab_service.nil?
139
+ @cmab_client = DefaultCmabClient.new(
140
+ http_client: nil,
141
+ retry_config: CmabRetryConfig.new,
142
+ logger: @logger
143
+ )
144
+ @cmab_cache = LRUCache.new(Optimizely::DefaultCmabCacheOptions::DEFAULT_CMAB_CACHE_SIZE, Optimizely::DefaultCmabCacheOptions::DEFAULT_CMAB_CACHE_TIMEOUT)
145
+ @cmab_service = DefaultCmabService.new(
146
+ @cmab_cache,
147
+ @cmab_client,
148
+ @logger
149
+ )
150
+ else
151
+ @cmab_service = cmab_service
152
+ end
153
+
154
+ @decision_service = DecisionService.new(@logger, @cmab_service, @user_profile_service)
135
155
 
136
156
  @event_processor = if event_processor.respond_to?(:process)
137
157
  event_processor
@@ -185,18 +205,23 @@ module Optimizely
185
205
  feature_flag = config.get_feature_flag_from_key(flag_key)
186
206
  experiment = nil
187
207
  decision_source = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
208
+ experiment_id = nil
209
+ variation_id = nil
210
+
188
211
  # Send impression event if Decision came from a feature test and decide options doesn't include disableDecisionEvent
189
212
  if decision.is_a?(Optimizely::DecisionService::Decision)
190
213
  experiment = decision.experiment
191
214
  rule_key = experiment ? experiment['key'] : nil
192
- variation = decision['variation']
215
+ experiment_id = experiment ? experiment['id'] : nil
216
+ variation = decision.variation
193
217
  variation_key = variation ? variation['key'] : nil
218
+ variation_id = variation ? variation['id'] : nil
194
219
  feature_enabled = variation ? variation['featureEnabled'] : false
195
220
  decision_source = decision.source
196
221
  end
197
222
 
198
223
  if !decide_options.include?(OptimizelyDecideOption::DISABLE_DECISION_EVENT) && (decision_source == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST'] || config.send_flag_decisions)
199
- send_impression(config, experiment, variation_key || '', flag_key, rule_key || '', feature_enabled, decision_source, user_id, attributes)
224
+ send_impression(config, experiment, variation_key || '', flag_key, rule_key || '', feature_enabled, decision_source, user_id, attributes, decision&.cmab_uuid)
200
225
  decision_event_dispatched = true
201
226
  end
202
227
 
@@ -214,14 +239,16 @@ module Optimizely
214
239
  @notification_center.send_notifications(
215
240
  NotificationCenter::NOTIFICATION_TYPES[:DECISION],
216
241
  Helpers::Constants::DECISION_NOTIFICATION_TYPES['FLAG'],
217
- user_id, (attributes || {}),
242
+ user_id, attributes || {},
218
243
  flag_key: flag_key,
219
244
  enabled: feature_enabled,
220
245
  variables: all_variables,
221
246
  variation_key: variation_key,
222
247
  rule_key: rule_key,
223
248
  reasons: should_include_reasons ? reasons : [],
224
- decision_event_dispatched: decision_event_dispatched
249
+ decision_event_dispatched: decision_event_dispatched,
250
+ experiment_id: experiment_id,
251
+ variation_id: variation_id
225
252
  )
226
253
 
227
254
  OptimizelyDecision.new(
@@ -330,7 +357,7 @@ module Optimizely
330
357
 
331
358
  # If the feature flag is nil, create a default OptimizelyDecision and move to the next key
332
359
  if feature_flag.nil?
333
- decisions[key] = OptimizelyDecision.new(nil, false, nil, nil, key, user_context, [])
360
+ decisions[key] = OptimizelyDecision.new(variation_key: nil, enabled: false, variables: nil, rule_key: nil, flag_key: key, user_context: user_context, reasons: [])
334
361
  next
335
362
  end
336
363
  valid_keys.push(key)
@@ -351,9 +378,17 @@ module Optimizely
351
378
  decision_list = @decision_service.get_variations_for_feature_list(config, flags_without_forced_decision, user_context, decide_options)
352
379
 
353
380
  flags_without_forced_decision.each_with_index do |flag, i|
354
- decision = decision_list[i][0]
355
- reasons = decision_list[i][1]
381
+ decision = decision_list[i].decision
382
+ reasons = decision_list[i].reasons
383
+ error = decision_list[i].error
356
384
  flag_key = flag['key']
385
+ # store error decision against key and remove key from valid keys
386
+ if error
387
+ optimizely_decision = OptimizelyDecision.new_error_decision(flag_key, user_context, reasons)
388
+ decisions[flag_key] = optimizely_decision
389
+ valid_keys.delete(flag_key) if valid_keys.include?(flag_key)
390
+ next
391
+ end
357
392
  flag_decisions[flag_key] = decision
358
393
  decision_reasons_dict[flag_key] ||= []
359
394
  decision_reasons_dict[flag_key].push(*reasons)
@@ -592,14 +627,14 @@ module Optimizely
592
627
  end
593
628
 
594
629
  user_context = OptimizelyUserContext.new(self, user_id, attributes, identify: false)
595
- decision, = @decision_service.get_variation_for_feature(config, feature_flag, user_context)
596
-
630
+ decision_result = @decision_service.get_variation_for_feature(config, feature_flag, user_context)
631
+ decision = decision_result.decision
597
632
  feature_enabled = false
598
633
  source_string = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
599
634
  if decision.is_a?(Optimizely::DecisionService::Decision)
600
635
  variation = decision['variation']
601
636
  feature_enabled = variation['featureEnabled']
602
- if decision.source == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
637
+ if decision.source == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST'] || decision.source == Optimizely::DecisionService::DECISION_SOURCES['HOLDOUT']
603
638
  source_string = Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
604
639
  source_info = {
605
640
  experiment_key: decision.experiment['key'],
@@ -625,7 +660,7 @@ module Optimizely
625
660
  @notification_center.send_notifications(
626
661
  NotificationCenter::NOTIFICATION_TYPES[:DECISION],
627
662
  Helpers::Constants::DECISION_NOTIFICATION_TYPES['FEATURE'],
628
- user_id, (attributes || {}),
663
+ user_id, attributes || {},
629
664
  feature_key: feature_flag_key,
630
665
  feature_enabled: feature_enabled,
631
666
  source: source_string,
@@ -832,7 +867,8 @@ module Optimizely
832
867
  end
833
868
 
834
869
  user_context = OptimizelyUserContext.new(self, user_id, attributes, identify: false)
835
- decision, = @decision_service.get_variation_for_feature(config, feature_flag, user_context)
870
+ decision_result = @decision_service.get_variation_for_feature(config, feature_flag, user_context)
871
+ decision = decision_result.decision
836
872
  variation = decision ? decision['variation'] : nil
837
873
  feature_enabled = variation ? variation['featureEnabled'] : false
838
874
  all_variables = {}
@@ -853,7 +889,7 @@ module Optimizely
853
889
 
854
890
  @notification_center.send_notifications(
855
891
  NotificationCenter::NOTIFICATION_TYPES[:DECISION],
856
- Helpers::Constants::DECISION_NOTIFICATION_TYPES['ALL_FEATURE_VARIABLES'], user_id, (attributes || {}),
892
+ Helpers::Constants::DECISION_NOTIFICATION_TYPES['ALL_FEATURE_VARIABLES'], user_id, attributes || {},
857
893
  feature_key: feature_flag_key,
858
894
  feature_enabled: feature_enabled,
859
895
  source: source_string,
@@ -1022,7 +1058,8 @@ module Optimizely
1022
1058
  user_context = OptimizelyUserContext.new(self, user_id, attributes, identify: false)
1023
1059
  user_profile_tracker = UserProfileTracker.new(user_id, @user_profile_service, @logger)
1024
1060
  user_profile_tracker.load_user_profile
1025
- variation_id, = @decision_service.get_variation(config, experiment_id, user_context, user_profile_tracker)
1061
+ variation_result = @decision_service.get_variation(config, experiment_id, user_context, user_profile_tracker)
1062
+ variation_id = variation_result.variation_id
1026
1063
  user_profile_tracker.save_user_profile
1027
1064
  variation = config.get_variation_from_id(experiment_key, variation_id) unless variation_id.nil?
1028
1065
  variation_key = variation['key'] if variation
@@ -1033,7 +1070,7 @@ module Optimizely
1033
1070
  end
1034
1071
  @notification_center.send_notifications(
1035
1072
  NotificationCenter::NOTIFICATION_TYPES[:DECISION],
1036
- decision_notification_type, user_id, (attributes || {}),
1073
+ decision_notification_type, user_id, attributes || {},
1037
1074
  experiment_key: experiment_key,
1038
1075
  variation_key: variation_key
1039
1076
  )
@@ -1090,7 +1127,8 @@ module Optimizely
1090
1127
  end
1091
1128
 
1092
1129
  user_context = OptimizelyUserContext.new(self, user_id, attributes, identify: false)
1093
- decision, = @decision_service.get_variation_for_feature(config, feature_flag, user_context)
1130
+ decision_result = @decision_service.get_variation_for_feature(config, feature_flag, user_context)
1131
+ decision = decision_result.decision
1094
1132
  variation = decision ? decision['variation'] : nil
1095
1133
  feature_enabled = variation ? variation['featureEnabled'] : false
1096
1134
 
@@ -1108,7 +1146,7 @@ module Optimizely
1108
1146
 
1109
1147
  @notification_center.send_notifications(
1110
1148
  NotificationCenter::NOTIFICATION_TYPES[:DECISION],
1111
- Helpers::Constants::DECISION_NOTIFICATION_TYPES['FEATURE_VARIABLE'], user_id, (attributes || {}),
1149
+ Helpers::Constants::DECISION_NOTIFICATION_TYPES['FEATURE_VARIABLE'], user_id, attributes || {},
1112
1150
  feature_key: feature_flag_key,
1113
1151
  feature_enabled: feature_enabled,
1114
1152
  source: source_string,
@@ -1208,7 +1246,7 @@ module Optimizely
1208
1246
  raise InvalidInputError, 'event_dispatcher'
1209
1247
  end
1210
1248
 
1211
- def send_impression(config, experiment, variation_key, flag_key, rule_key, enabled, rule_type, user_id, attributes = nil)
1249
+ def send_impression(config, experiment, variation_key, flag_key, rule_key, enabled, rule_type, user_id, attributes = nil, cmab_uuid = nil)
1212
1250
  if experiment.nil?
1213
1251
  experiment = {
1214
1252
  'id' => '',
@@ -1240,6 +1278,7 @@ module Optimizely
1240
1278
  variation_key: variation_key,
1241
1279
  enabled: enabled
1242
1280
  }
1281
+ metadata[:cmab_uuid] = cmab_uuid unless cmab_uuid.nil?
1243
1282
 
1244
1283
  user_event = UserEventFactory.create_impression_event(config, experiment, variation_id, metadata, user_id, attributes)
1245
1284
  @event_processor.process(user_event)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: optimizely-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.1.0
4
+ version: 5.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Optimizely
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-01-10 00:00:00.000000000 Z
11
+ date: 2025-11-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -134,6 +134,8 @@ files:
134
134
  - lib/optimizely.rb
135
135
  - lib/optimizely/audience.rb
136
136
  - lib/optimizely/bucketer.rb
137
+ - lib/optimizely/cmab/cmab_client.rb
138
+ - lib/optimizely/cmab/cmab_service.rb
137
139
  - lib/optimizely/condition_tree_evaluator.rb
138
140
  - lib/optimizely/config/datafile_project_config.rb
139
141
  - lib/optimizely/config/proxy_config.rb