configcat 5.0.2 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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