configcat 5.0.2 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 967461c3bdc865f2911296ecc35117dac6646c80e82496d74811b0649df689dc
4
- data.tar.gz: 7dd1b2d93dd82b8d279b9e3ac110e9ba564a14c775dfa132a044e749aa2f686b
3
+ metadata.gz: 6979b91044d036414367912894396e332a9fdd10b4239e72294a74949d8e5c82
4
+ data.tar.gz: fd58f1ea297a2fce1cf47e4bfe521724677617a32163d7b5bde3f2f80234482e
5
5
  SHA512:
6
- metadata.gz: 056f50b2304c5fdbcaaf39fc207d1f145d62892732f987a15fb8ad2e7267e57c92eb3ad6aeb2434424c7b26f4fa013350a26156d26fa2434969193e8f9d98d62
7
- data.tar.gz: 7ef97dac81e1a79d856880798bdfe897bc6de02bdf4ab4d7196320ef2c00d84ba69b6c4e6e08077de9f65c1191a1cb66220d2ecf4ad1e9e03f0e889cc00d83c1
6
+ metadata.gz: ae734ae5e04ae73c6ea7eb62c48a5633a943ac009c2f7b59729028a141932cdfdfceabbdef207d774153f586c0c418df939c0042ea288debe6bfccf877765ff7
7
+ data.tar.gz: 50b222dd87b113807f72ab24f3e0dfd01bb752639ab394094e40c34461e57ca0637d59038883b78d02dcdcfcd0469bfc21c6b6a5f74ab0b4658c17fcd8d7e8bc
@@ -1,17 +1,32 @@
1
1
  require 'configcat/interfaces'
2
2
 
3
3
  module ConfigCat
4
+ class NullConfigCache < ConfigCache
5
+ def initialize
6
+ @value = {}
7
+ end
8
+
9
+ def get(key)
10
+ return nil
11
+ end
12
+
13
+ def set(key, value)
14
+ # do nothing
15
+ end
16
+ end
17
+
4
18
  class InMemoryConfigCache < ConfigCache
5
- def initialize()
6
- @_value = {}
19
+ attr_reader :value
20
+ def initialize
21
+ @value = {}
7
22
  end
8
23
 
9
24
  def get(key)
10
- return @_value.fetch(key, nil)
25
+ return @value.fetch(key, nil)
11
26
  end
12
27
 
13
28
  def set(key, value)
14
- @_value[key] = value
29
+ @value[key] = value
15
30
  end
16
31
  end
17
32
  end
@@ -1,117 +1,189 @@
1
1
  require 'configcat/interfaces'
2
2
  require 'configcat/configcache'
3
+ require 'configcat/configcatoptions'
3
4
  require 'configcat/configfetcher'
4
- require 'configcat/autopollingcachepolicy'
5
- require 'configcat/manualpollingcachepolicy'
6
- require 'configcat/lazyloadingcachepolicy'
7
5
  require 'configcat/rolloutevaluator'
8
- require 'configcat/datagovernance'
6
+ require 'configcat/utils'
7
+ require 'configcat/configcatlogger'
8
+ require 'configcat/overridedatasource'
9
+ require 'configcat/configservice'
10
+ require 'configcat/evaluationdetails'
9
11
 
10
12
 
11
13
  module ConfigCat
12
14
  KeyValue = Struct.new(:key, :value)
13
15
  class ConfigCatClient
14
- @@sdk_keys = []
15
-
16
- def initialize(sdk_key,
17
- poll_interval_seconds: 60,
18
- max_init_wait_time_seconds: 5,
19
- on_configuration_changed_callback: nil,
20
- cache_time_to_live_seconds: 60,
21
- config_cache_class: nil,
22
- base_url: nil,
23
- proxy_address: nil,
24
- proxy_port: nil,
25
- proxy_user: nil,
26
- proxy_pass: nil,
27
- open_timeout: 10,
28
- read_timeout: 30,
29
- flag_overrides: nil,
30
- data_governance: DataGovernance::GLOBAL)
31
- if sdk_key === nil
32
- raise ConfigCatClientException, "SDK Key is required."
16
+ attr_reader :log, :hooks
17
+
18
+ @@lock = Mutex.new
19
+ @@instances = {}
20
+
21
+ # Creates a new or gets an already existing `ConfigCatClient` for the given `sdk_key`.
22
+ #
23
+ # :param sdk_key [String] ConfigCat SDK Key to access your configuration.
24
+ # :param options [ConfigCatOptions] Configuration for `ConfigCatClient`.
25
+ # :return [ConfigCatClient] the `ConfigCatClient` instance.
26
+ def self.get(sdk_key, options = nil)
27
+ @@lock.synchronize do
28
+ client = @@instances[sdk_key]
29
+ if client
30
+ if options
31
+ client.log.warn(3000, "There is an existing client instance for the specified SDK Key. " \
32
+ "No new client instance will be created and the specified options are ignored. " \
33
+ "Returning the existing client instance. SDK Key: '#{sdk_key}'.")
34
+ end
35
+ return client
36
+ end
37
+
38
+ options ||= ConfigCatOptions.new
39
+ client = ConfigCatClient.new(sdk_key, options)
40
+ @@instances[sdk_key] = client
41
+ return client
33
42
  end
43
+ end
34
44
 
35
- if @@sdk_keys.include?(sdk_key)
36
- ConfigCat.logger.warn("A ConfigCat Client is already initialized with sdk_key %s. "\
37
- "We strongly recommend you to use the ConfigCat Client as "\
38
- "a Singleton object in your application." % sdk_key)
39
- else
40
- @@sdk_keys.push(sdk_key)
45
+ # Closes all ConfigCatClient instances.
46
+ def self.close_all
47
+ @@lock.synchronize do
48
+ @@instances.each do |key, value|
49
+ value.send(:_close_resources)
50
+ end
51
+ @@instances.clear
41
52
  end
53
+ end
42
54
 
43
- @_sdk_key = sdk_key
44
- @_override_data_source = flag_overrides
55
+ private def initialize(sdk_key, options = ConfigCatOptions.new)
56
+ @hooks = options.hooks || Hooks.new
57
+ @log = ConfigCatLogger.new(@hooks)
58
+
59
+ if sdk_key === nil
60
+ raise ConfigCatClientException, "SDK Key is required."
61
+ end
45
62
 
46
- if config_cache_class
47
- @_config_cache = config_cache_class.new()
63
+ @_sdk_key = sdk_key
64
+ @_default_user = options.default_user
65
+ @_rollout_evaluator = RolloutEvaluator.new(@log)
66
+ if options.flag_overrides
67
+ @_override_data_source = options.flag_overrides.create_data_source(@log)
48
68
  else
49
- @_config_cache = InMemoryConfigCache.new()
69
+ @_override_data_source = nil
50
70
  end
51
71
 
52
- if !@_override_data_source.equal?(nil) && @_override_data_source.get_behaviour() == OverrideBehaviour::LOCAL_ONLY
72
+ config_cache = options.config_cache.nil? ? NullConfigCache.new : options.config_cache
73
+
74
+ if @_override_data_source && @_override_data_source.get_behaviour() == OverrideBehaviour::LOCAL_ONLY
53
75
  @_config_fetcher = nil
54
- @_cache_policy = nil
55
- elsif poll_interval_seconds > 0
56
- @_config_fetcher = CacheControlConfigFetcher.new(sdk_key, "a", base_url: base_url,
57
- proxy_address: proxy_address, proxy_port: proxy_port, proxy_user: proxy_user, proxy_pass: proxy_pass,
58
- open_timeout: open_timeout, read_timeout: read_timeout,
59
- data_governance: data_governance)
60
- @_cache_policy = AutoPollingCachePolicy.new(@_config_fetcher, @_config_cache, _get_cache_key(), poll_interval_seconds, max_init_wait_time_seconds, on_configuration_changed_callback)
61
- elsif cache_time_to_live_seconds > 0
62
- @_config_fetcher = CacheControlConfigFetcher.new(sdk_key, "l", base_url: base_url,
63
- proxy_address: proxy_address, proxy_port: proxy_port, proxy_user: proxy_user, proxy_pass: proxy_pass,
64
- open_timeout: open_timeout, read_timeout: read_timeout,
65
- data_governance: data_governance)
66
- @_cache_policy = LazyLoadingCachePolicy.new(@_config_fetcher, @_config_cache, _get_cache_key(), cache_time_to_live_seconds)
76
+ @_config_service = nil
67
77
  else
68
- @_config_fetcher = CacheControlConfigFetcher.new(sdk_key, "m", base_url: base_url,
69
- proxy_address: proxy_address, proxy_port: proxy_port, proxy_user: proxy_user, proxy_pass: proxy_pass,
70
- open_timeout: open_timeout, read_timeout: read_timeout,
71
- data_governance: data_governance)
72
- @_cache_policy = ManualPollingCachePolicy.new(@_config_fetcher, @_config_cache, _get_cache_key())
78
+ @_config_fetcher = ConfigFetcher.new(@_sdk_key,
79
+ @log,
80
+ options.polling_mode.identifier,
81
+ base_url: options.base_url,
82
+ proxy_address: options.proxy_address,
83
+ proxy_port: options.proxy_port,
84
+ proxy_user: options.proxy_user,
85
+ proxy_pass: options.proxy_pass,
86
+ open_timeout: options.open_timeout_seconds,
87
+ read_timeout: options.read_timeout_seconds,
88
+ data_governance: options.data_governance)
89
+
90
+ @_config_service = ConfigService.new(@sdk_key,
91
+ options.polling_mode,
92
+ @hooks,
93
+ @_config_fetcher,
94
+ @log,
95
+ config_cache,
96
+ options.offline)
73
97
  end
74
98
  end
75
99
 
76
- def get_value(key, default_value, user=nil)
77
- config = _get_settings()
78
- if config === nil
79
- ConfigCat.logger.warn("Evaluating get_value('%s') failed. Cache is empty. "\
80
- "Returning default_value in your get_value call: [%s]." % [key, default_value.to_s])
100
+ # Gets the value of a feature flag or setting identified by the given `key`.
101
+ #
102
+ # :param key [String] the identifier of the feature flag or setting.
103
+ # :param default_value in case of any failure, this value will be returned.
104
+ # :param user [User] the user object to identify the caller.
105
+ # :return the value.
106
+ def get_value(key, default_value, user = nil)
107
+ settings, fetch_time = _get_settings()
108
+ if settings.nil?
109
+ message = "Config JSON is not present when evaluating setting '#{key}'. Returning the `default_value` parameter that you specified in your application: '#{default_value}'."
110
+ @log.error(1000, message)
111
+ @hooks.invoke_on_flag_evaluated(EvaluationDetails.from_error(key, default_value, error: message))
81
112
  return default_value
82
113
  end
83
- value, variation_id = RolloutEvaluator.evaluate(key, user, default_value, nil, config)
84
- return value
114
+ details = _evaluate(key, user, default_value, nil, settings, fetch_time)
115
+ return details.value
85
116
  end
86
117
 
87
- def get_all_keys()
88
- config = _get_settings()
89
- if config === nil
90
- return []
118
+ # Gets the value and evaluation details of a feature flag or setting identified by the given `key`.
119
+ #
120
+ # :param key [String] the identifier of the feature flag or setting.
121
+ # :param default_value in case of any failure, this value will be returned.
122
+ # :param user [User] the user object to identify the caller.
123
+ # :return [EvaluationDetails] the evaluation details.
124
+ def get_value_details(key, default_value, user = nil)
125
+ settings, fetch_time = _get_settings()
126
+ if settings.nil?
127
+ message = "Config JSON is not present when evaluating setting '#{key}'. Returning the `default_value` parameter that you specified in your application: '#{default_value}'."
128
+ @log.error(1000, message)
129
+ details = EvaluationDetails.from_error(key, default_value, error: message)
130
+ @hooks.invoke_on_flag_evaluated(details)
131
+ return details
91
132
  end
92
- feature_flags = config.fetch(FEATURE_FLAGS, nil)
93
- if feature_flags === nil
133
+ details = _evaluate(key, user, default_value, nil, settings, fetch_time)
134
+ return details
135
+ end
136
+
137
+ # Gets all setting keys.
138
+ #
139
+ # :return list of keys.
140
+ def get_all_keys
141
+ settings, _ = _get_settings()
142
+ if settings === nil
143
+ @log.error(1000, "Config JSON is not present. Returning empty list.")
94
144
  return []
95
145
  end
96
- return feature_flags.keys
146
+ return settings.keys
97
147
  end
98
148
 
99
- def get_variation_id(key, default_variation_id, user=nil)
100
- config = _get_settings()
101
- if config === nil
102
- ConfigCat.logger.warn("Evaluating get_variation_id('%s') failed. Cache is empty. "\
103
- "Returning default_variation_id in your get_variation_id call: [%s]." %
104
- [key, default_variation_id.to_s])
149
+ # Gets the Variation ID (analytics) of a feature flag or setting based on it's key.
150
+ #
151
+ # :param key [String] the identifier of the feature flag or setting.
152
+ # :param default_variation_id in case of any failure, this value will be returned.
153
+ # :param user [User] the user object to identify the caller.
154
+ # :return the variation ID.
155
+ def get_variation_id(key, default_variation_id, user = nil)
156
+ ConfigCat.logger.warn("get_variation_id is deprecated and will be removed in a future major version. " \
157
+ "Please use [get_value_details] instead.")
158
+
159
+ settings, fetch_time = _get_settings()
160
+ if settings === nil
161
+ message = "Config JSON is not present when evaluating setting '#{key}'. Returning the `default_variation_id` parameter that you specified in your application: '#{default_variation_id}'."
162
+ @log.error(1000, message)
163
+ @hooks.invoke_on_flag_evaluated(EvaluationDetails.from_error(key, nil, error: message,
164
+ variation_id: default_variation_id))
105
165
  return default_variation_id
106
166
  end
107
- value, variation_id = RolloutEvaluator.evaluate(key, user, nil, default_variation_id, config)
108
- return variation_id
167
+ details = _evaluate(key, user, nil, default_variation_id, settings, fetch_time)
168
+ return details.variation_id
109
169
  end
110
170
 
111
- def get_all_variation_ids(user: nil)
112
- keys = get_all_keys()
171
+ # Gets the Variation IDs (analytics) of all feature flags or settings.
172
+ #
173
+ # :param user [User] the user object to identify the caller.
174
+ # :return list of variation IDs
175
+ def get_all_variation_ids(user = nil)
176
+ ConfigCat.logger.warn("get_all_variation_ids is deprecated and will be removed in a future major version. " \
177
+ "Please use [get_value_details] instead.")
178
+
179
+ settings, _ = _get_settings()
180
+ if settings === nil
181
+ @log.error(1000, "Config JSON is not present. Returning empty list.")
182
+ return []
183
+ end
184
+
113
185
  variation_ids = []
114
- for key in keys
186
+ for key in settings.keys
115
187
  variation_id = get_variation_id(key, nil, user)
116
188
  if !variation_id.equal?(nil)
117
189
  variation_ids.push(variation_id)
@@ -120,20 +192,18 @@ module ConfigCat
120
192
  return variation_ids
121
193
  end
122
194
 
195
+ # Gets the key of a setting, and it's value identified by the given Variation ID (analytics)
196
+ #
197
+ # :param variation_id [String] variation ID
198
+ # :return key and value
123
199
  def get_key_and_value(variation_id)
124
- config = _get_settings()
125
- if config === nil
126
- ConfigCat.logger.warn("Evaluating get_variation_id('%s') failed. Cache is empty. Returning nil." % variation_id)
127
- return nil
128
- end
129
-
130
- feature_flags = config.fetch(FEATURE_FLAGS, nil)
131
- if feature_flags === nil
132
- ConfigCat.logger.warn("Evaluating get_key_and_value('%s') failed. Cache is empty. Returning None." % variation_id)
200
+ settings, _ = _get_settings()
201
+ if settings === nil
202
+ @log.error(1000, "Config JSON is not present. Returning nil.")
133
203
  return nil
134
204
  end
135
205
 
136
- for key, value in feature_flags
206
+ for key, value in settings
137
207
  if variation_id == value.fetch(VARIATION_ID, nil)
138
208
  return KeyValue.new(key, value[VALUE])
139
209
  end
@@ -152,12 +222,23 @@ module ConfigCat
152
222
  end
153
223
  end
154
224
  end
225
+
226
+ @log.error(2011, "Could not find the setting for the specified variation ID: '#{variation_id}'.")
155
227
  end
156
228
 
157
- def get_all_values(user: nil)
158
- keys = get_all_keys()
229
+ # Evaluates and returns the values of all feature flags and settings.
230
+ #
231
+ # :param user [User] the user object to identify the caller.
232
+ # :return dictionary of values
233
+ def get_all_values(user = nil)
234
+ settings, _ = _get_settings()
235
+ if settings === nil
236
+ @log.error(1000, "Config JSON is not present. Returning empty dictionary.")
237
+ return {}
238
+ end
239
+
159
240
  all_values = {}
160
- for key in keys
241
+ for key in settings.keys
161
242
  value = get_value(key, nil, user)
162
243
  if !value.equal?(nil)
163
244
  all_values[key] = value
@@ -166,47 +247,133 @@ module ConfigCat
166
247
  return all_values
167
248
  end
168
249
 
169
- def force_refresh()
170
- @_cache_policy.force_refresh()
250
+ # Gets the values along with evaluation details of all feature flags and settings.
251
+ #
252
+ # :param user [User] the user object to identify the caller.
253
+ # :return list of all evaluation details
254
+ def get_all_value_details(user = nil)
255
+ settings, fetch_time = _get_settings()
256
+ if settings.nil?
257
+ @log.error(1000, "Config JSON is not present. Returning empty list.")
258
+ return []
259
+ end
260
+
261
+ details_result = []
262
+ for key in settings.keys
263
+ details = _evaluate(key, user, nil, nil, settings, fetch_time)
264
+ details_result.push(details)
265
+ end
266
+
267
+ return details_result
268
+ end
269
+
270
+ # Initiates a force refresh on the cached configuration.
271
+ #
272
+ # :return [RefreshResult]
273
+ def force_refresh
274
+ return @_config_service.refresh if @_config_service
275
+
276
+ return RefreshResult(false,
277
+ "The SDK uses the LocalOnly flag override behavior which prevents making HTTP requests.")
278
+ end
279
+
280
+ # Sets the default user.
281
+ #
282
+ # :param user [User] the user object to identify the caller.
283
+ def set_default_user(user)
284
+ @_default_user = user
285
+ end
286
+
287
+ # Sets the default user to nil.
288
+ def clear_default_user
289
+ @_default_user = nil
171
290
  end
172
291
 
173
- def stop()
174
- @_cache_policy.stop() if @_cache_policy
175
- @_config_fetcher.close() if @_config_fetcher
176
- @@sdk_keys.delete(@_sdk_key)
292
+ # Configures the SDK to allow HTTP requests.
293
+ def set_online
294
+ if @_config_service
295
+ @_config_service.set_online
296
+ else
297
+ @log.warn(3202, "Client is configured to use the `LOCAL_ONLY` override behavior, thus `set_online()` has no effect.")
298
+ end
299
+ end
300
+
301
+ # Configures the SDK to not initiate HTTP requests and work only from its cache.
302
+ def set_offline
303
+ @_config_service.set_offline if @_config_service
304
+ end
305
+
306
+ # Returns true when the SDK is configured not to initiate HTTP requests, otherwise false.
307
+ def offline?
308
+ return @_config_service ? @_config_service.offline? : true
309
+ end
310
+
311
+ # Closes the underlying resources.
312
+ def close
313
+ @@lock.synchronize do
314
+ _close_resources
315
+ @@instances.delete(@_sdk_key)
316
+ end
177
317
  end
178
318
 
179
319
  private
180
320
 
181
- def _get_settings()
321
+ def _close_resources
322
+ @_config_service.close if @_config_service
323
+ @_config_fetcher.close if @_config_fetcher
324
+ @hooks.clear
325
+ end
326
+
327
+ def _get_settings
182
328
  if !@_override_data_source.nil?
183
329
  behaviour = @_override_data_source.get_behaviour()
184
330
  if behaviour == OverrideBehaviour::LOCAL_ONLY
185
- return @_override_data_source.get_overrides()
331
+ return @_override_data_source.get_overrides(), Utils::DISTANT_PAST
186
332
  elsif behaviour == OverrideBehaviour::REMOTE_OVER_LOCAL
187
- remote_settings = @_cache_policy.get()
333
+ remote_settings, fetch_time = @_config_service.get_settings()
188
334
  local_settings = @_override_data_source.get_overrides()
335
+ remote_settings ||= {}
336
+ local_settings ||= {}
189
337
  result = local_settings.clone()
190
- if remote_settings.key?(FEATURE_FLAGS) && local_settings.key?(FEATURE_FLAGS)
191
- result[FEATURE_FLAGS] = result[FEATURE_FLAGS].merge(remote_settings[FEATURE_FLAGS])
192
- end
193
- return result
338
+ result.update(remote_settings)
339
+ return result, fetch_time
194
340
  elsif behaviour == OverrideBehaviour::LOCAL_OVER_REMOTE
195
- remote_settings = @_cache_policy.get()
341
+ remote_settings, fetch_time = @_config_service.get_settings()
196
342
  local_settings = @_override_data_source.get_overrides()
343
+ remote_settings ||= {}
344
+ local_settings ||= {}
197
345
  result = remote_settings.clone()
198
- if remote_settings.key?(FEATURE_FLAGS) && local_settings.key?(FEATURE_FLAGS)
199
- result[FEATURE_FLAGS] = result[FEATURE_FLAGS].merge(local_settings[FEATURE_FLAGS])
200
- end
201
- return result
346
+ result.update(local_settings)
347
+ return result, fetch_time
202
348
  end
203
349
  end
204
- return @_cache_policy.get()
350
+ return @_config_service.get_settings()
205
351
  end
206
352
 
207
- def _get_cache_key()
353
+ def _get_cache_key
208
354
  return Digest::SHA1.hexdigest("ruby_" + CONFIG_FILE_NAME + "_" + @_sdk_key)
209
355
  end
210
356
 
357
+ def _evaluate(key, user, default_value, default_variation_id, settings, fetch_time)
358
+ user = user || @_default_user
359
+ value, variation_id, rule, percentage_rule, error = @_rollout_evaluator.evaluate(
360
+ key: key,
361
+ user: user,
362
+ default_value: default_value,
363
+ default_variation_id: default_variation_id,
364
+ settings: settings)
365
+
366
+ details = EvaluationDetails.new(key: key,
367
+ value: value,
368
+ variation_id: variation_id,
369
+ fetch_time: !fetch_time.nil? ? Time.at(fetch_time).utc : nil,
370
+ user: user,
371
+ is_default_value: error.nil? || error.empty? ? false : true,
372
+ error: error,
373
+ matched_evaluation_rule: rule,
374
+ matched_evaluation_percentage_rule: percentage_rule)
375
+ @hooks.invoke_on_flag_evaluated(details)
376
+ return details
377
+ end
211
378
  end
212
379
  end
@@ -0,0 +1,24 @@
1
+ module ConfigCat
2
+ class ConfigCatLogger
3
+ def initialize(hooks)
4
+ @hooks = hooks
5
+ end
6
+
7
+ def debug(message)
8
+ ConfigCat.logger.debug("[0] " + message)
9
+ end
10
+
11
+ def info(event_id, message)
12
+ ConfigCat.logger.info("[" + event_id.to_s + "] " + message)
13
+ end
14
+
15
+ def warn(event_id, message)
16
+ ConfigCat.logger.warn("[" + event_id.to_s + "] " + message)
17
+ end
18
+
19
+ def error(event_id, message)
20
+ @hooks.invoke_on_error(message)
21
+ ConfigCat.logger.error("[" + event_id.to_s + "] " + message)
22
+ end
23
+ end
24
+ end