configcat 7.0.0 → 8.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.
- checksums.yaml +4 -4
- data/lib/configcat/config.rb +317 -0
- data/lib/configcat/configcatclient.rb +100 -52
- data/lib/configcat/configcatlogger.rb +4 -0
- data/lib/configcat/configentry.rb +1 -0
- data/lib/configcat/configfetcher.rb +7 -4
- data/lib/configcat/configservice.rb +26 -30
- data/lib/configcat/evaluationcontext.rb +14 -0
- data/lib/configcat/evaluationdetails.rb +22 -4
- data/lib/configcat/evaluationlogbuilder.rb +81 -0
- data/lib/configcat/localdictionarydatasource.rb +20 -4
- data/lib/configcat/localfiledatasource.rb +23 -7
- data/lib/configcat/rolloutevaluator.rb +714 -151
- data/lib/configcat/user.rb +43 -4
- data/lib/configcat/utils.rb +21 -1
- data/lib/configcat/version.rb +1 -1
- metadata +5 -3
- data/lib/configcat/constants.rb +0 -18
@@ -30,13 +30,12 @@ module ConfigCat
|
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
33
|
-
def
|
33
|
+
def get_config
|
34
34
|
if @polling_mode.is_a?(LazyLoadingMode)
|
35
35
|
entry, _ = fetch_if_older(Utils.get_utc_now_seconds_since_epoch - @polling_mode.cache_refresh_interval_seconds)
|
36
36
|
return !entry.empty? ?
|
37
|
-
[entry.config
|
37
|
+
[entry.config, entry.fetch_time] :
|
38
38
|
[nil, Utils::DISTANT_PAST]
|
39
|
-
|
40
39
|
elsif @polling_mode.is_a?(AutoPollingMode) && !@initialized.set?
|
41
40
|
elapsed_time = Utils.get_utc_now_seconds_since_epoch - @start_time # Elapsed time in seconds
|
42
41
|
if elapsed_time < @polling_mode.max_init_wait_time_seconds
|
@@ -46,20 +45,27 @@ module ConfigCat
|
|
46
45
|
if !@initialized.set?
|
47
46
|
set_initialized
|
48
47
|
return !@cached_entry.empty? ?
|
49
|
-
[@cached_entry.config
|
48
|
+
[@cached_entry.config, @cached_entry.fetch_time] :
|
50
49
|
[nil, Utils::DISTANT_PAST]
|
51
50
|
end
|
52
51
|
end
|
53
52
|
end
|
54
53
|
|
55
|
-
|
54
|
+
# If we are initialized, we prefer the cached results
|
55
|
+
entry, _ = fetch_if_older(Utils::DISTANT_PAST, prefer_cache: @initialized.set?)
|
56
56
|
return !entry.empty? ?
|
57
|
-
[entry.config
|
57
|
+
[entry.config, entry.fetch_time] :
|
58
58
|
[nil, Utils::DISTANT_PAST]
|
59
59
|
end
|
60
60
|
|
61
61
|
# :return [RefreshResult]
|
62
62
|
def refresh
|
63
|
+
if offline?
|
64
|
+
offline_warning = "Client is in offline mode, it cannot initiate HTTP calls."
|
65
|
+
@log.warn(3200, offline_warning)
|
66
|
+
return RefreshResult.new(success = false, error = offline_warning)
|
67
|
+
end
|
68
|
+
|
63
69
|
_, error = fetch_if_older(Utils::DISTANT_FUTURE)
|
64
70
|
return RefreshResult.new(success = error.nil?, error = error)
|
65
71
|
end
|
@@ -74,7 +80,7 @@ module ConfigCat
|
|
74
80
|
if @polling_mode.is_a?(AutoPollingMode)
|
75
81
|
start_poll
|
76
82
|
end
|
77
|
-
@log.info(5200,
|
83
|
+
@log.info(5200, "Switched to ONLINE mode.")
|
78
84
|
end
|
79
85
|
end
|
80
86
|
|
@@ -90,7 +96,7 @@ module ConfigCat
|
|
90
96
|
@thread.join
|
91
97
|
end
|
92
98
|
|
93
|
-
@log.info(5200,
|
99
|
+
@log.info(5200, "Switched to OFFLINE mode.")
|
94
100
|
end
|
95
101
|
end
|
96
102
|
|
@@ -111,35 +117,25 @@ module ConfigCat
|
|
111
117
|
end
|
112
118
|
|
113
119
|
# :return [ConfigEntry, String] Returns the ConfigEntry object and error message in case of any error.
|
114
|
-
def fetch_if_older(
|
120
|
+
def fetch_if_older(threshold, prefer_cache: false)
|
115
121
|
# Sync up with the cache and use it when it's not expired.
|
116
122
|
@lock.synchronize do
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
end
|
123
|
-
|
124
|
-
# Cache isn't expired
|
125
|
-
if @cached_entry.fetch_time > time
|
126
|
-
set_initialized
|
127
|
-
return @cached_entry, nil
|
128
|
-
end
|
123
|
+
# Sync up with the cache and use it when it's not expired.
|
124
|
+
from_cache = read_cache
|
125
|
+
if !from_cache.empty? && from_cache.etag != @cached_entry.etag
|
126
|
+
@cached_entry = from_cache
|
127
|
+
@hooks.invoke_on_config_changed(from_cache.config[FEATURE_FLAGS])
|
129
128
|
end
|
130
129
|
|
131
|
-
#
|
132
|
-
|
133
|
-
|
134
|
-
if prefer_cache && @initialized.set?
|
130
|
+
# Cache isn't expired
|
131
|
+
if @cached_entry.fetch_time > threshold
|
132
|
+
set_initialized
|
135
133
|
return @cached_entry, nil
|
136
134
|
end
|
137
135
|
|
138
|
-
# If we are in offline mode
|
139
|
-
if @is_offline
|
140
|
-
|
141
|
-
@log.warn(3200, offline_warning)
|
142
|
-
return @cached_entry, offline_warning
|
136
|
+
# If we are in offline mode or the caller prefers cached values, do not initiate fetch.
|
137
|
+
if @is_offline || prefer_cache
|
138
|
+
return @cached_entry, nil
|
143
139
|
end
|
144
140
|
end
|
145
141
|
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module ConfigCat
|
2
|
+
class EvaluationContext
|
3
|
+
attr_accessor :key, :setting_type, :user, :visited_keys, :is_missing_user_object_logged, :is_missing_user_object_attribute_logged
|
4
|
+
|
5
|
+
def initialize(key, setting_type, user, visited_keys = nil, is_missing_user_object_logged = false, is_missing_user_object_attribute_logged = false)
|
6
|
+
@key = key
|
7
|
+
@setting_type = setting_type
|
8
|
+
@user = user
|
9
|
+
@visited_keys = visited_keys || []
|
10
|
+
@is_missing_user_object_logged = is_missing_user_object_logged
|
11
|
+
@is_missing_user_object_attribute_logged = is_missing_user_object_attribute_logged
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -1,19 +1,37 @@
|
|
1
1
|
module ConfigCat
|
2
2
|
class EvaluationDetails
|
3
3
|
attr_reader :key, :value, :variation_id, :fetch_time, :user, :is_default_value, :error,
|
4
|
-
:
|
4
|
+
:matched_targeting_rule, :matched_percentage_option
|
5
5
|
|
6
6
|
def initialize(key:, value:, variation_id: nil, fetch_time: nil, user: nil, is_default_value: false, error: nil,
|
7
|
-
|
7
|
+
matched_targeting_rule: nil, matched_percentage_option: nil)
|
8
|
+
# Key of the feature flag or setting.
|
8
9
|
@key = key
|
10
|
+
|
11
|
+
# Evaluated value of the feature flag or setting.
|
9
12
|
@value = value
|
13
|
+
|
14
|
+
# Variation ID of the feature flag or setting (if available).
|
10
15
|
@variation_id = variation_id
|
16
|
+
|
17
|
+
# Time of last successful config download.
|
11
18
|
@fetch_time = fetch_time
|
19
|
+
|
20
|
+
# The User Object used for the evaluation (if available).
|
12
21
|
@user = user
|
22
|
+
|
23
|
+
# Indicates whether the default value passed to the setting evaluation methods like ConfigCatClient.get_value,
|
24
|
+
# ConfigCatClient.get_value_details, etc. is used as the result of the evaluation.
|
13
25
|
@is_default_value = is_default_value
|
26
|
+
|
27
|
+
# Error message in case evaluation failed.
|
14
28
|
@error = error
|
15
|
-
|
16
|
-
|
29
|
+
|
30
|
+
# The targeting rule (if any) that matched during the evaluation and was used to return the evaluated value.
|
31
|
+
@matched_targeting_rule = matched_targeting_rule
|
32
|
+
|
33
|
+
# The percentage option (if any) that was used to select the evaluated value.
|
34
|
+
@matched_percentage_option = matched_percentage_option
|
17
35
|
end
|
18
36
|
|
19
37
|
def self.from_error(key, value, error:, variation_id: nil)
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module ConfigCat
|
2
|
+
class EvaluationLogBuilder
|
3
|
+
def initialize
|
4
|
+
@indent_level = 0
|
5
|
+
@text = ''
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.trunc_comparison_value_if_needed(comparator, comparison_value)
|
9
|
+
if [
|
10
|
+
Comparator::IS_ONE_OF_HASHED,
|
11
|
+
Comparator::IS_NOT_ONE_OF_HASHED,
|
12
|
+
Comparator::EQUALS_HASHED,
|
13
|
+
Comparator::NOT_EQUALS_HASHED,
|
14
|
+
Comparator::STARTS_WITH_ANY_OF_HASHED,
|
15
|
+
Comparator::NOT_STARTS_WITH_ANY_OF_HASHED,
|
16
|
+
Comparator::ENDS_WITH_ANY_OF_HASHED,
|
17
|
+
Comparator::NOT_ENDS_WITH_ANY_OF_HASHED,
|
18
|
+
Comparator::ARRAY_CONTAINS_ANY_OF_HASHED,
|
19
|
+
Comparator::ARRAY_NOT_CONTAINS_ANY_OF_HASHED
|
20
|
+
].include?(comparator)
|
21
|
+
if comparison_value.is_a?(Array)
|
22
|
+
length = comparison_value.length
|
23
|
+
if length > 1
|
24
|
+
return "[<#{length} hashed values>]"
|
25
|
+
end
|
26
|
+
return "[<#{length} hashed value>]"
|
27
|
+
end
|
28
|
+
|
29
|
+
return "'<hashed value>'"
|
30
|
+
end
|
31
|
+
|
32
|
+
if comparison_value.is_a?(Array)
|
33
|
+
length_limit = 10
|
34
|
+
length = comparison_value.length
|
35
|
+
if length > length_limit
|
36
|
+
remaining = length - length_limit
|
37
|
+
more_text = remaining == 1 ? "<1 more value>" : "<#{remaining} more values>"
|
38
|
+
|
39
|
+
formatted_strings = comparison_value.first(length_limit).map { |str| "'#{str}'" }.join(", ")
|
40
|
+
return "[#{formatted_strings}, ... #{more_text}]"
|
41
|
+
end
|
42
|
+
|
43
|
+
# replace '"' with "'" in the string representation of the array
|
44
|
+
formatted_strings = comparison_value.map { |str| "'#{str}'" }.join(", ")
|
45
|
+
return "[#{formatted_strings}]"
|
46
|
+
end
|
47
|
+
|
48
|
+
if [Comparator::BEFORE_DATETIME, Comparator::AFTER_DATETIME].include?(comparator)
|
49
|
+
time = Utils.get_date_time(comparison_value)
|
50
|
+
return "'#{comparison_value}' (#{time.strftime('%Y-%m-%dT%H:%M:%S.%L')}Z UTC)"
|
51
|
+
end
|
52
|
+
|
53
|
+
"'#{comparison_value.to_s}'"
|
54
|
+
end
|
55
|
+
|
56
|
+
def increase_indent
|
57
|
+
@indent_level += 1
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
def decrease_indent
|
62
|
+
@indent_level = [@indent_level - 1, 0].max
|
63
|
+
self
|
64
|
+
end
|
65
|
+
|
66
|
+
def append(text)
|
67
|
+
@text += text
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
def new_line(text = nil)
|
72
|
+
@text += "\n" + ' ' * @indent_level
|
73
|
+
@text += text if text
|
74
|
+
self
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_s
|
78
|
+
@text
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'configcat/overridedatasource'
|
2
|
-
require 'configcat/
|
2
|
+
require 'configcat/config'
|
3
3
|
|
4
4
|
|
5
5
|
module ConfigCat
|
@@ -17,14 +17,30 @@ module ConfigCat
|
|
17
17
|
class LocalDictionaryDataSource < OverrideDataSource
|
18
18
|
def initialize(source, override_behaviour)
|
19
19
|
super(override_behaviour)
|
20
|
-
@
|
20
|
+
@_config = {}
|
21
21
|
source.each do |key, value|
|
22
|
-
|
22
|
+
value_type = case value
|
23
|
+
when TrueClass, FalseClass
|
24
|
+
BOOL_VALUE
|
25
|
+
when String
|
26
|
+
STRING_VALUE
|
27
|
+
when Integer
|
28
|
+
INT_VALUE
|
29
|
+
when Float
|
30
|
+
DOUBLE_VALUE
|
31
|
+
else
|
32
|
+
UNSUPPORTED_VALUE
|
33
|
+
end
|
34
|
+
|
35
|
+
@_config[FEATURE_FLAGS] ||= {}
|
36
|
+
@_config[FEATURE_FLAGS][key] = { VALUE => { value_type => value } }
|
37
|
+
setting_type = SettingType.from_type(value.class)
|
38
|
+
@_config[FEATURE_FLAGS][key][SETTING_TYPE] = setting_type.to_i unless setting_type.nil?
|
23
39
|
end
|
24
40
|
end
|
25
41
|
|
26
42
|
def get_overrides
|
27
|
-
return @
|
43
|
+
return @_config
|
28
44
|
end
|
29
45
|
end
|
30
46
|
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'configcat/overridedatasource'
|
2
|
-
require 'configcat/
|
2
|
+
require 'configcat/config'
|
3
3
|
|
4
4
|
|
5
5
|
module ConfigCat
|
@@ -18,17 +18,17 @@ module ConfigCat
|
|
18
18
|
def initialize(file_path, override_behaviour, log)
|
19
19
|
super(override_behaviour)
|
20
20
|
@log = log
|
21
|
-
if !File.
|
21
|
+
if !File.exist?(file_path)
|
22
22
|
@log.error(1300, "Cannot find the local config file '#{file_path}'. This is a path that your application provided to the ConfigCat SDK by passing it to the `LocalFileFlagOverrides.new()` method. Read more: https://configcat.com/docs/sdk-reference/ruby/#json-file")
|
23
23
|
end
|
24
24
|
@_file_path = file_path
|
25
|
-
@
|
25
|
+
@_config = nil
|
26
26
|
@_cached_file_stamp = 0
|
27
27
|
end
|
28
28
|
|
29
29
|
def get_overrides
|
30
30
|
reload_file_content()
|
31
|
-
return @
|
31
|
+
return @_config
|
32
32
|
end
|
33
33
|
|
34
34
|
private
|
@@ -41,13 +41,29 @@ module ConfigCat
|
|
41
41
|
file = File.read(@_file_path)
|
42
42
|
data = JSON.parse(file)
|
43
43
|
if data.key?("flags")
|
44
|
-
@
|
44
|
+
@_config = { FEATURE_FLAGS => {} }
|
45
45
|
source = data["flags"]
|
46
46
|
source.each do |key, value|
|
47
|
-
|
47
|
+
value_type = case value
|
48
|
+
when true, false
|
49
|
+
BOOL_VALUE
|
50
|
+
when String
|
51
|
+
STRING_VALUE
|
52
|
+
when Integer
|
53
|
+
INT_VALUE
|
54
|
+
when Float
|
55
|
+
DOUBLE_VALUE
|
56
|
+
else
|
57
|
+
UNSUPPORTED_VALUE
|
58
|
+
end
|
59
|
+
|
60
|
+
@_config[FEATURE_FLAGS][key] = { VALUE => { value_type => value } }
|
61
|
+
setting_type = SettingType.from_type(value.class)
|
62
|
+
@_config[FEATURE_FLAGS][key][SETTING_TYPE] = setting_type.to_i unless setting_type.nil?
|
48
63
|
end
|
49
64
|
else
|
50
|
-
|
65
|
+
Config.fixup_config_salt_and_segments(data)
|
66
|
+
@_config = data
|
51
67
|
end
|
52
68
|
end
|
53
69
|
rescue JSON::ParserError => e
|