configcat 5.0.2 → 6.0.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.
@@ -0,0 +1,153 @@
1
+ require 'configcat/datagovernance'
2
+ require 'configcat/pollingmode'
3
+
4
+ module ConfigCat
5
+ class Hooks
6
+ #
7
+ # Events fired by [ConfigCatClient].
8
+ #
9
+
10
+ def initialize(on_client_ready: nil, on_config_changed: nil, on_flag_evaluated: nil, on_error: nil)
11
+ @_on_client_ready_callbacks = on_client_ready ? [on_client_ready] : []
12
+ @_on_config_changed_callbacks = on_config_changed ? [on_config_changed] : []
13
+ @_on_flag_evaluated_callbacks = on_flag_evaluated ? [on_flag_evaluated] : []
14
+ @_on_error_callbacks = on_error ? [on_error] : []
15
+ end
16
+
17
+ def add_on_client_ready(callback)
18
+ @_on_client_ready_callbacks.push(callback)
19
+ end
20
+
21
+ def add_on_config_changed(callback)
22
+ @_on_config_changed_callbacks.push(callback)
23
+ end
24
+
25
+ def add_on_flag_evaluated(callback)
26
+ @_on_flag_evaluated_callbacks.push(callback)
27
+ end
28
+
29
+ def add_on_error(callback)
30
+ @_on_error_callbacks.push(callback)
31
+ end
32
+
33
+ def invoke_on_client_ready
34
+ @_on_client_ready_callbacks.each { |callback|
35
+ begin
36
+ callback.()
37
+ rescue Exception => e
38
+ error = "Exception occurred during invoke_on_client_ready callback: #{e}"
39
+ invoke_on_error(error)
40
+ ConfigCat.logger.error(error)
41
+ end
42
+ }
43
+ end
44
+
45
+ def invoke_on_config_changed(config)
46
+ @_on_config_changed_callbacks.each { |callback|
47
+ begin
48
+ callback.(config)
49
+ rescue Exception => e
50
+ error = "Exception occurred during invoke_on_config_changed callback: #{e}"
51
+ invoke_on_error(error)
52
+ ConfigCat.logger.error(error)
53
+ end
54
+ }
55
+ end
56
+
57
+ def invoke_on_flag_evaluated(evaluation_details)
58
+ @_on_flag_evaluated_callbacks.each { |callback|
59
+ begin
60
+ callback.(evaluation_details)
61
+ rescue Exception => e
62
+ error = "Exception occurred during invoke_on_flag_evaluated callback: #{e}"
63
+ invoke_on_error(error)
64
+ ConfigCat.logger.error(error)
65
+ end
66
+ }
67
+ end
68
+
69
+ def invoke_on_error(error)
70
+ @_on_error_callbacks.each { |callback|
71
+ begin
72
+ callback.(error)
73
+ rescue Exception => e
74
+ ConfigCat.logger.error("Exception occurred during invoke_on_error callback: #{e}")
75
+ end
76
+ }
77
+ end
78
+
79
+ def clear
80
+ @_on_client_ready_callbacks.clear
81
+ @_on_config_changed_callbacks.clear
82
+ @_on_flag_evaluated_callbacks.clear
83
+ @_on_error_callbacks.clear
84
+ end
85
+ end
86
+
87
+ class ConfigCatOptions
88
+ # Configuration options for ConfigCatClient.
89
+ attr_reader :base_url, :polling_mode, :config_cache, :proxy_address, :proxy_port, :proxy_user, :proxy_pass,
90
+ :open_timeout_seconds, :read_timeout_seconds, :flag_overrides, :data_governance, :default_user,
91
+ :hooks, :offline
92
+
93
+ def initialize(base_url: nil,
94
+ polling_mode: PollingMode.auto_poll(),
95
+ config_cache: nil,
96
+ proxy_address: nil,
97
+ proxy_port: nil,
98
+ proxy_user: nil,
99
+ proxy_pass: nil,
100
+ open_timeout_seconds: 10,
101
+ read_timeout_seconds: 30,
102
+ flag_overrides: nil,
103
+ data_governance: DataGovernance::GLOBAL,
104
+ default_user: nil,
105
+ hooks: nil,
106
+ offline: false)
107
+ # The base ConfigCat CDN url.
108
+ @base_url = base_url
109
+
110
+ # The polling mode.
111
+ @polling_mode = polling_mode
112
+
113
+ # The cache implementation used to cache the downloaded config files.
114
+ @config_cache = config_cache
115
+
116
+ # Proxy address
117
+ @proxy_address = proxy_address
118
+
119
+ # Proxy port
120
+ @proxy_port = proxy_port
121
+
122
+ # username for proxy authentication
123
+ @proxy_user = proxy_user
124
+
125
+ # password for proxy authentication
126
+ @proxy_pass = proxy_pass
127
+
128
+ # The number of seconds to wait for the server to make the initial connection
129
+ # (i.e. completing the TCP connection handshake).
130
+ @open_timeout_seconds = open_timeout_seconds
131
+
132
+ # The number of seconds to wait for the server to respond before giving up.
133
+ @read_timeout_seconds = read_timeout_seconds
134
+
135
+ # Feature flag and setting overrides.
136
+ @flag_overrides = flag_overrides
137
+
138
+ # Default: `DataGovernance.Global`. Set this parameter to be in sync with the
139
+ # Data Governance preference on the [Dashboard](https://app.configcat.com/organization/data-governance).
140
+ # (Only Organization Admins have access)
141
+ @data_governance = data_governance
142
+
143
+ # The default user to be used for evaluating feature flags and getting settings.
144
+ @default_user = default_user
145
+
146
+ # The Hooks instance to subscribe to events.
147
+ @hooks = hooks
148
+
149
+ # Indicates whether the client should work in offline mode.
150
+ @offline = offline
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,40 @@
1
+ require 'configcat/utils'
2
+
3
+ module ConfigCat
4
+ class ConfigEntry
5
+ CONFIG = 'config'
6
+ ETAG = 'etag'
7
+ FETCH_TIME = 'fetch_time'
8
+
9
+ attr_accessor :config, :etag, :fetch_time
10
+
11
+ def initialize(config = {}, etag = '', fetch_time = Utils::DISTANT_PAST)
12
+ @config = config
13
+ @etag = etag
14
+ @fetch_time = fetch_time
15
+ end
16
+
17
+ def self.create_from_json(json)
18
+ return ConfigEntry::EMPTY if json.nil?
19
+ return ConfigEntry.new(
20
+ config = json.fetch(CONFIG, {}),
21
+ etag = json.fetch(ETAG, ''),
22
+ fetch_time = json.fetch(FETCH_TIME, Utils::DISTANT_PAST)
23
+ )
24
+ end
25
+
26
+ def empty?
27
+ self == ConfigEntry::EMPTY
28
+ end
29
+
30
+ def to_json
31
+ {
32
+ CONFIG => config,
33
+ ETAG => etag,
34
+ FETCH_TIME => fetch_time
35
+ }
36
+ end
37
+
38
+ EMPTY = ConfigEntry.new(etag: 'empty')
39
+ end
40
+ end
@@ -2,6 +2,7 @@ require 'configcat/interfaces'
2
2
  require 'configcat/version'
3
3
  require 'configcat/datagovernance'
4
4
  require 'configcat/constants'
5
+ require 'configcat/configentry'
5
6
  require 'net/http'
6
7
  require 'uri'
7
8
  require 'json'
@@ -18,41 +19,66 @@ module ConfigCat
18
19
  FORCE_REDIRECT = 2
19
20
  end
20
21
 
22
+ class Status
23
+ FETCHED = 0,
24
+ NOT_MODIFIED = 1,
25
+ FAILURE = 2
26
+ end
27
+
21
28
  class FetchResponse
22
- def initialize(response)
23
- @_response = response
29
+ attr_reader :entry, :error, :is_transient_error
30
+
31
+ def initialize(status, entry, error = nil, is_transient_error = false)
32
+ @status = status
33
+ @entry = entry
34
+ @error = error
35
+ @is_transient_error = is_transient_error
36
+ end
37
+
38
+ # Gets whether a new configuration value was fetched or not.
39
+ # :return [Boolean] true if a new configuration value was fetched, otherwise false.
40
+ def is_fetched
41
+ @status == Status::FETCHED
42
+ end
43
+
44
+ # Gets whether the fetch resulted a '304 Not Modified' or not.
45
+ # :return [Boolean] true if the fetch resulted a '304 Not Modified' code, otherwise false.
46
+ def is_not_modified
47
+ @status == Status::NOT_MODIFIED
24
48
  end
25
49
 
26
- # Returns the json-encoded content of a response, if any
27
- def json()
28
- return JSON.parse(@_response.body)
50
+ # Gets whether the fetch failed or not.
51
+ # :return [Boolean] true if the fetch failed, otherwise false.
52
+ def is_failed
53
+ @status == Status::FAILURE
29
54
  end
30
55
 
31
- # Gets whether a new configuration value was fetched or not
32
- def is_fetched()
33
- code = @_response.code.to_i
34
- return 200 <= code && code < 300
56
+ def self.success(entry)
57
+ FetchResponse.new(Status::FETCHED, entry)
35
58
  end
36
59
 
37
- # Gets whether the fetch resulted a '304 Not Modified' or not
38
- def is_not_modified()
39
- return @_response.code == "304"
60
+ def self.not_modified
61
+ FetchResponse.new(Status::NOT_MODIFIED, ConfigEntry::EMPTY)
62
+ end
63
+
64
+ def self.failure(error, is_transient_error)
65
+ FetchResponse.new(Status::FAILURE, ConfigEntry::EMPTY, error, is_transient_error)
40
66
  end
41
67
  end
42
68
 
43
- class CacheControlConfigFetcher < ConfigFetcher
44
- def initialize(sdk_key, mode, base_url:nil, proxy_address:nil, proxy_port:nil, proxy_user:nil, proxy_pass:nil,
45
- open_timeout:10, read_timeout:30,
46
- data_governance:DataGovernance::GLOBAL)
69
+ class ConfigFetcher
70
+ def initialize(sdk_key, log, mode, base_url: nil, proxy_address: nil, proxy_port: nil, proxy_user: nil, proxy_pass: nil,
71
+ open_timeout: 10, read_timeout: 30,
72
+ data_governance: DataGovernance::GLOBAL)
47
73
  @_sdk_key = sdk_key
74
+ @log = log
48
75
  @_proxy_address = proxy_address
49
76
  @_proxy_port = proxy_port
50
77
  @_proxy_user = proxy_user
51
78
  @_proxy_pass = proxy_pass
52
79
  @_open_timeout = open_timeout
53
80
  @_read_timeout = read_timeout
54
- @_etag = ""
55
- @_headers = {"User-Agent" => ((("ConfigCat-Ruby/") + mode) + ("-")) + VERSION, "X-ConfigCat-UserAgent" => ((("ConfigCat-Ruby/") + mode) + ("-")) + VERSION, "Content-Type" => "application/json"}
81
+ @_headers = { "User-Agent" => ((("ConfigCat-Ruby/") + mode) + ("-")) + VERSION, "X-ConfigCat-UserAgent" => ((("ConfigCat-Ruby/") + mode) + ("-")) + VERSION, "Content-Type" => "application/json" }
56
82
  if !base_url.equal?(nil)
57
83
  @_base_url_overridden = true
58
84
  @_base_url = base_url.chomp("/")
@@ -66,34 +92,24 @@ module ConfigCat
66
92
  end
67
93
  end
68
94
 
69
- def get_open_timeout()
95
+ def get_open_timeout
70
96
  return @_open_timeout
71
97
  end
72
98
 
73
- def get_read_timeout()
99
+ def get_read_timeout
74
100
  return @_read_timeout
75
101
  end
76
102
 
77
- # Returns the FetchResponse object contains configuration json Dictionary
78
- def get_configuration_json(retries=0)
79
- ConfigCat.logger.debug "Fetching configuration from ConfigCat"
80
- uri = URI.parse((((@_base_url + ("/")) + BASE_PATH) + @_sdk_key) + BASE_EXTENSION)
81
- headers = @_headers
82
- headers["If-None-Match"] = @_etag unless @_etag.empty?
83
- _create_http()
84
- request = Net::HTTP::Get.new(uri.request_uri, headers)
85
- response = @_http.request(request)
86
- etag = response["ETag"]
87
- @_etag = etag unless etag.nil? || etag.empty?
88
- ConfigCat.logger.debug "ConfigCat configuration json fetch response code:#{response.code} Cached:#{response['ETag']}"
89
- fetch_response = FetchResponse.new(response)
103
+ # Returns the FetchResponse object contains configuration entry
104
+ def get_configuration(etag = "", retries = 0)
105
+ fetch_response = _fetch(etag)
90
106
 
91
107
  # If there wasn't a config change, we return the response.
92
108
  if !fetch_response.is_fetched()
93
109
  return fetch_response
94
110
  end
95
111
 
96
- preferences = fetch_response.json().fetch(PREFERENCES, nil)
112
+ preferences = fetch_response.entry.config.fetch(PREFERENCES, nil)
97
113
  if preferences === nil
98
114
  return fetch_response
99
115
  end
@@ -107,7 +123,7 @@ module ConfigCat
107
123
 
108
124
  redirect = preferences.fetch(REDIRECT, nil)
109
125
  # If the base_url is overridden, and the redirect parameter is not 2 (force),
110
- # the SDK should not redirect the calls and it just have to return the response.
126
+ # the SDK should not redirect the calls, and it just has to return the response.
111
127
  if @_base_url_overridden && redirect != RedirectMode::FORCE_REDIRECT
112
128
  return fetch_response
113
129
  end
@@ -124,20 +140,20 @@ module ConfigCat
124
140
  # Try to download again with the new url
125
141
 
126
142
  if redirect == RedirectMode::SHOULD_REDIRECT
127
- ConfigCat.logger.warn("Your data_governance parameter at ConfigCatClient initialization is not in sync with your preferences on the ConfigCat Dashboard: https://app.configcat.com/organization/data-governance. Only Organization Admins can set this preference.")
143
+ @log.warn("Your data_governance parameter at ConfigCatClient initialization is not in sync with your preferences on the ConfigCat Dashboard: https://app.configcat.com/organization/data-governance. Only Organization Admins can set this preference.")
128
144
  end
129
145
 
130
146
  # To prevent loops we check if we retried at least 3 times with the new base_url
131
147
  if retries >= 2
132
- ConfigCat.logger.error("Redirect loop during config.json fetch. Please contact support@configcat.com.")
148
+ @log.error("Redirect loop during config.json fetch. Please contact support@configcat.com.")
133
149
  return fetch_response
134
150
  end
135
151
 
136
152
  # Retry the config download with the new base_url
137
- return get_configuration_json(retries + 1)
153
+ return get_configuration(etag, retries + 1)
138
154
  end
139
155
 
140
- def close()
156
+ def close
141
157
  if @_http
142
158
  @_http = nil
143
159
  end
@@ -145,7 +161,49 @@ module ConfigCat
145
161
 
146
162
  private
147
163
 
148
- def _create_http()
164
+ def _fetch(etag)
165
+ begin
166
+ @log.debug("Fetching configuration from ConfigCat")
167
+ uri = URI.parse((((@_base_url + ("/")) + BASE_PATH) + @_sdk_key) + BASE_EXTENSION)
168
+ headers = @_headers
169
+ headers["If-None-Match"] = etag.empty? ? nil : etag
170
+ _create_http()
171
+ request = Net::HTTP::Get.new(uri.request_uri, headers)
172
+ response = @_http.request(request)
173
+ case response
174
+ when Net::HTTPSuccess
175
+ @log.debug("ConfigCat configuration json fetch response code:#{response.code} Cached:#{response['ETag']}")
176
+ response_etag = response["ETag"]
177
+ if response_etag.nil?
178
+ response_etag = ""
179
+ end
180
+ config = JSON.parse(response.body)
181
+ return FetchResponse.success(ConfigEntry.new(config, response_etag, Utils.get_utc_now_seconds_since_epoch))
182
+ when Net::HTTPNotModified
183
+ return FetchResponse.not_modified
184
+ when Net::HTTPNotFound, Net::HTTPForbidden
185
+ error = "Double-check your SDK Key at https://app.configcat.com/sdkkey. Received unexpected response: #{response}"
186
+ @log.error(error)
187
+ return FetchResponse.failure(error, false)
188
+ else
189
+ raise Net::HTTPError.new("", response)
190
+ end
191
+ rescue Net::HTTPError => e
192
+ error = "Unexpected HTTP response was received: #{e}"
193
+ @log.error(error)
194
+ return FetchResponse.failure(error, true)
195
+ rescue Timeout::Error => e
196
+ error = "Request timed out. Timeout values: [connect: #{get_open_timeout()}s, read: #{get_read_timeout()}s]"
197
+ @log.error(error)
198
+ return FetchResponse.failure(error, true)
199
+ rescue Exception => e
200
+ error = "An exception occurred during fetching: #{e}"
201
+ @log.error(error)
202
+ return FetchResponse.failure(error, true)
203
+ end
204
+ end
205
+
206
+ def _create_http
149
207
  uri = URI.parse(@_base_url)
150
208
  use_ssl = true if uri.scheme == 'https'
151
209
  if @_http.equal?(nil) || @_http.address != uri.host || @_http.port != uri.port || @_http.use_ssl? != use_ssl
@@ -0,0 +1,212 @@
1
+ require 'concurrent'
2
+ require 'configcat/configentry'
3
+ require 'configcat/pollingmode'
4
+ require 'configcat/refreshresult'
5
+
6
+
7
+ module ConfigCat
8
+ class ConfigService
9
+ def initialize(sdk_key, polling_mode, hooks, config_fetcher, log, config_cache, is_offline)
10
+ @sdk_key = sdk_key
11
+ @cached_entry = ConfigEntry::EMPTY
12
+ @cached_entry_string = ''
13
+ @polling_mode = polling_mode
14
+ @log = log
15
+ @config_cache = config_cache
16
+ @hooks = hooks
17
+ @cache_key = Digest::SHA1.hexdigest("ruby_#{CONFIG_FILE_NAME}_#{@sdk_key}")
18
+ @config_fetcher = config_fetcher
19
+ @is_offline = is_offline
20
+ @response_future = nil
21
+ @initialized = Concurrent::Event.new
22
+ @lock = Mutex.new
23
+ @ongoing_fetch = false
24
+ @fetch_finished = Concurrent::Event.new
25
+ @start_time = Utils.get_utc_now_seconds_since_epoch
26
+
27
+ if @polling_mode.is_a?(AutoPollingMode)
28
+ start_poll
29
+ else
30
+ set_initialized
31
+ end
32
+ end
33
+
34
+ def get_settings
35
+ if @polling_mode.is_a?(LazyLoadingMode)
36
+ entry, _ = fetch_if_older(Utils.get_utc_now_seconds_since_epoch - @polling_mode.cache_refresh_interval_seconds)
37
+ return entry.config[FEATURE_FLAGS], entry.fetch_time
38
+ elsif @polling_mode.is_a?(AutoPollingMode) && !@initialized.set?
39
+ elapsed_time = Utils.get_utc_now_seconds_since_epoch - @start_time # Elapsed time in seconds
40
+ if elapsed_time < @polling_mode.max_init_wait_time_seconds
41
+ @initialized.wait(@polling_mode.max_init_wait_time_seconds - elapsed_time)
42
+
43
+ # Max wait time expired without result, notify subscribers with the cached config.
44
+ if !@initialized.set?
45
+ set_initialized
46
+ return @cached_entry.config[FEATURE_FLAGS], @cached_entry.fetch_time
47
+ end
48
+ end
49
+ end
50
+
51
+ entry, _ = fetch_if_older(Utils::DISTANT_PAST, prefer_cache: true)
52
+ return entry.config[FEATURE_FLAGS], entry.fetch_time
53
+ end
54
+
55
+ # :return [RefreshResult]
56
+ def refresh
57
+ _, error = fetch_if_older(Utils::DISTANT_FUTURE)
58
+ return RefreshResult.new(success = error.nil?, error = error)
59
+ end
60
+
61
+ def set_online
62
+ @lock.synchronize do
63
+ if !@is_offline
64
+ return
65
+ end
66
+
67
+ @is_offline = false
68
+ if @polling_mode.is_a?(AutoPollingMode)
69
+ start_poll
70
+ end
71
+ @log.debug('Switched to ONLINE mode.')
72
+ end
73
+ end
74
+
75
+ def set_offline
76
+ @lock.synchronize do
77
+ if @is_offline
78
+ return
79
+ end
80
+
81
+ @is_offline = true
82
+ if @polling_mode.is_a?(AutoPollingMode)
83
+ @stopped.set
84
+ @thread.join
85
+ end
86
+
87
+ @log.debug('Switched to OFFLINE mode.')
88
+ end
89
+ end
90
+
91
+ def offline?
92
+ return @is_offline
93
+ end
94
+
95
+ def close
96
+ if @polling_mode.is_a?(AutoPollingMode)
97
+ @stopped.set
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ # :return [ConfigEntry, String] Returns the ConfigEntry object and error message in case of any error.
104
+ def fetch_if_older(time, prefer_cache: false)
105
+ # Sync up with the cache and use it when it's not expired.
106
+ @lock.synchronize do
107
+ if @cached_entry.empty? || @cached_entry.fetch_time > time
108
+ entry = read_cache
109
+ if !entry.empty? && entry.etag != @cached_entry.etag
110
+ @cached_entry = entry
111
+ @hooks.invoke_on_config_changed(entry.config[FEATURE_FLAGS])
112
+ end
113
+
114
+ # Cache isn't expired
115
+ if @cached_entry.fetch_time > time
116
+ set_initialized
117
+ return @cached_entry, nil
118
+ end
119
+ end
120
+
121
+ # Use cache anyway (get calls on auto & manual poll must not initiate fetch).
122
+ # The initialized check ensures that we subscribe for the ongoing fetch during the
123
+ # max init wait time window in case of auto poll.
124
+ if prefer_cache && @initialized.set?
125
+ return @cached_entry, nil
126
+ end
127
+
128
+ # If we are in offline mode we are not allowed to initiate fetch.
129
+ if @is_offline
130
+ offline_warning = 'The SDK is in offline mode, it can not initiate HTTP calls.'
131
+ @log.warn(offline_warning)
132
+ return @cached_entry, offline_warning
133
+ end
134
+ end
135
+
136
+ # No fetch is running, initiate a new one.
137
+ # Ensure only one fetch request is running at a time.
138
+ # If there's an ongoing fetch running, we will wait for the ongoing fetch.
139
+ if @ongoing_fetch
140
+ @fetch_finished.wait
141
+ else
142
+ @ongoing_fetch = true
143
+ @fetch_finished.reset
144
+ response = @config_fetcher.get_configuration(@cached_entry.etag)
145
+
146
+ @lock.synchronize do
147
+ if response.is_fetched
148
+ @cached_entry = response.entry
149
+ write_cache(response.entry)
150
+ @hooks.invoke_on_config_changed(response.entry.config[FEATURE_FLAGS])
151
+ elsif (response.is_not_modified || !response.is_transient_error) && !@cached_entry.empty?
152
+ @cached_entry.fetch_time = Utils.get_utc_now_seconds_since_epoch
153
+ write_cache(@cached_entry)
154
+ end
155
+
156
+ set_initialized
157
+ end
158
+
159
+ @ongoing_fetch = false
160
+ @fetch_finished.set
161
+ end
162
+
163
+ return @cached_entry, nil
164
+ end
165
+
166
+ def start_poll
167
+ @started = Concurrent::Event.new
168
+ @thread = Thread.new { run() }
169
+ @started.wait()
170
+ end
171
+
172
+ def run
173
+ @stopped = Concurrent::Event.new
174
+ @started.set
175
+ loop do
176
+ fetch_if_older(Utils.get_utc_now_seconds_since_epoch - @polling_mode.poll_interval_seconds)
177
+ @stopped.wait(@polling_mode.poll_interval_seconds)
178
+ break if @stopped.set?
179
+ end
180
+ end
181
+
182
+ def set_initialized
183
+ if !@initialized.set?
184
+ @initialized.set
185
+ @hooks.invoke_on_client_ready
186
+ end
187
+ end
188
+
189
+ def read_cache
190
+ begin
191
+ json_string = @config_cache.get(@cache_key)
192
+ if !json_string || json_string == @cached_entry_string
193
+ return ConfigEntry::EMPTY
194
+ end
195
+
196
+ @cached_entry_string = json_string
197
+ return ConfigEntry.create_from_json(JSON.parse(json_string))
198
+ rescue Exception => e
199
+ @log.error("An error occurred during the cache read. #{e}")
200
+ return ConfigEntry::EMPTY
201
+ end
202
+ end
203
+
204
+ def write_cache(config_entry)
205
+ begin
206
+ @config_cache.set(@cache_key, config_entry.to_json.to_json)
207
+ rescue Exception => e
208
+ @log.error("An error occurred during the cache write. #{e}")
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,23 @@
1
+ module ConfigCat
2
+ class EvaluationDetails
3
+ attr_reader :key, :value, :variation_id, :fetch_time, :user, :is_default_value, :error,
4
+ :matched_evaluation_rule, :matched_evaluation_percentage_rule
5
+
6
+ def initialize(key:, value:, variation_id: nil, fetch_time: nil, user: nil, is_default_value: false, error: nil,
7
+ matched_evaluation_rule: nil, matched_evaluation_percentage_rule: nil)
8
+ @key = key
9
+ @value = value
10
+ @variation_id = variation_id
11
+ @fetch_time = fetch_time
12
+ @user = user
13
+ @is_default_value = is_default_value
14
+ @error = error
15
+ @matched_evaluation_rule = matched_evaluation_rule
16
+ @matched_evaluation_percentage_rule = matched_evaluation_percentage_rule
17
+ end
18
+
19
+ def self.from_error(key, value, error:, variation_id: nil)
20
+ EvaluationDetails.new(key: key, value: value, variation_id: variation_id, is_default_value: true, error: error)
21
+ end
22
+ end
23
+ end