launchdarkly-server-sdk 6.3.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/README.md +3 -4
- data/lib/ldclient-rb/config.rb +112 -62
- data/lib/ldclient-rb/context.rb +444 -0
- data/lib/ldclient-rb/evaluation_detail.rb +26 -22
- data/lib/ldclient-rb/events.rb +256 -146
- data/lib/ldclient-rb/flags_state.rb +26 -15
- data/lib/ldclient-rb/impl/big_segments.rb +18 -18
- data/lib/ldclient-rb/impl/broadcaster.rb +78 -0
- data/lib/ldclient-rb/impl/context.rb +96 -0
- data/lib/ldclient-rb/impl/context_filter.rb +145 -0
- data/lib/ldclient-rb/impl/data_source.rb +188 -0
- data/lib/ldclient-rb/impl/data_store.rb +59 -0
- data/lib/ldclient-rb/impl/dependency_tracker.rb +102 -0
- data/lib/ldclient-rb/impl/diagnostic_events.rb +9 -10
- data/lib/ldclient-rb/impl/evaluator.rb +386 -142
- data/lib/ldclient-rb/impl/evaluator_bucketing.rb +40 -41
- data/lib/ldclient-rb/impl/evaluator_helpers.rb +50 -0
- data/lib/ldclient-rb/impl/evaluator_operators.rb +26 -55
- data/lib/ldclient-rb/impl/event_sender.rb +7 -6
- data/lib/ldclient-rb/impl/event_summarizer.rb +68 -0
- data/lib/ldclient-rb/impl/event_types.rb +136 -0
- data/lib/ldclient-rb/impl/flag_tracker.rb +58 -0
- data/lib/ldclient-rb/impl/integrations/consul_impl.rb +19 -7
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +38 -30
- data/lib/ldclient-rb/impl/integrations/file_data_source.rb +24 -11
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +109 -12
- data/lib/ldclient-rb/impl/migrations/migrator.rb +287 -0
- data/lib/ldclient-rb/impl/migrations/tracker.rb +136 -0
- data/lib/ldclient-rb/impl/model/clause.rb +45 -0
- data/lib/ldclient-rb/impl/model/feature_flag.rb +255 -0
- data/lib/ldclient-rb/impl/model/preprocessed_data.rb +64 -0
- data/lib/ldclient-rb/impl/model/segment.rb +132 -0
- data/lib/ldclient-rb/impl/model/serialization.rb +54 -44
- data/lib/ldclient-rb/impl/repeating_task.rb +3 -4
- data/lib/ldclient-rb/impl/sampler.rb +25 -0
- data/lib/ldclient-rb/impl/store_client_wrapper.rb +102 -8
- data/lib/ldclient-rb/impl/store_data_set_sorter.rb +2 -2
- data/lib/ldclient-rb/impl/unbounded_pool.rb +1 -1
- data/lib/ldclient-rb/impl/util.rb +59 -1
- data/lib/ldclient-rb/in_memory_store.rb +9 -2
- data/lib/ldclient-rb/integrations/consul.rb +2 -2
- data/lib/ldclient-rb/integrations/dynamodb.rb +2 -2
- data/lib/ldclient-rb/integrations/file_data.rb +4 -4
- data/lib/ldclient-rb/integrations/redis.rb +5 -5
- data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +287 -62
- data/lib/ldclient-rb/integrations/test_data.rb +18 -14
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +20 -9
- data/lib/ldclient-rb/interfaces.rb +600 -14
- data/lib/ldclient-rb/ldclient.rb +314 -134
- data/lib/ldclient-rb/memoized_value.rb +1 -1
- data/lib/ldclient-rb/migrations.rb +230 -0
- data/lib/ldclient-rb/non_blocking_thread_pool.rb +1 -1
- data/lib/ldclient-rb/polling.rb +52 -6
- data/lib/ldclient-rb/reference.rb +274 -0
- data/lib/ldclient-rb/requestor.rb +9 -11
- data/lib/ldclient-rb/stream.rb +96 -34
- data/lib/ldclient-rb/util.rb +97 -14
- data/lib/ldclient-rb/version.rb +1 -1
- data/lib/ldclient-rb.rb +3 -4
- metadata +65 -23
- data/lib/ldclient-rb/event_summarizer.rb +0 -55
- data/lib/ldclient-rb/file_data_source.rb +0 -23
- data/lib/ldclient-rb/impl/event_factory.rb +0 -126
- data/lib/ldclient-rb/newrelic.rb +0 -17
- data/lib/ldclient-rb/redis_store.rb +0 -88
- data/lib/ldclient-rb/user_filter.rb +0 -52
@@ -1,4 +1,3 @@
|
|
1
|
-
|
2
1
|
module LaunchDarkly
|
3
2
|
module Impl
|
4
3
|
# Encapsulates the logic for percentage rollouts.
|
@@ -6,64 +5,64 @@ module LaunchDarkly
|
|
6
5
|
# Applies either a fixed variation or a rollout for a rule (or the fallthrough rule).
|
7
6
|
#
|
8
7
|
# @param flag [Object] the feature flag
|
9
|
-
# @param
|
10
|
-
# @param
|
11
|
-
# @return [Number] the variation index, or nil if there is an error
|
12
|
-
|
13
|
-
|
14
|
-
variation =
|
15
|
-
return variation, false
|
16
|
-
rollout =
|
8
|
+
# @param vr [LaunchDarkly::Impl::Model::VariationOrRollout] the variation/rollout properties
|
9
|
+
# @param context [LaunchDarkly::LDContext] the context properties
|
10
|
+
# @return [Array<[Number, nil], Boolean>] the variation index, or nil if there is an error
|
11
|
+
# @raise [InvalidReferenceException]
|
12
|
+
def self.variation_index_for_context(flag, vr, context)
|
13
|
+
variation = vr.variation
|
14
|
+
return variation, false unless variation.nil? # fixed variation
|
15
|
+
rollout = vr.rollout
|
17
16
|
return nil, false if rollout.nil?
|
18
|
-
variations = rollout
|
17
|
+
variations = rollout.variations
|
19
18
|
if !variations.nil? && variations.length > 0 # percentage rollout
|
20
|
-
|
19
|
+
rollout_is_experiment = rollout.is_experiment
|
20
|
+
bucket_by = rollout_is_experiment ? nil : rollout.bucket_by
|
21
|
+
bucket_by = 'key' if bucket_by.nil?
|
21
22
|
|
22
|
-
seed = rollout
|
23
|
-
bucket =
|
24
|
-
|
23
|
+
seed = rollout.seed
|
24
|
+
bucket = bucket_context(context, rollout.context_kind, flag.key, bucket_by, flag.salt, seed) # may not be present
|
25
|
+
in_experiment = rollout_is_experiment && !bucket.nil?
|
26
|
+
sum = 0
|
25
27
|
variations.each do |variate|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
sum += variate[:weight].to_f / 100000.0
|
31
|
-
if bucket < sum
|
32
|
-
return variate[:variation], !!in_experiment
|
28
|
+
sum += variate.weight.to_f / 100000.0
|
29
|
+
if bucket.nil? || bucket < sum
|
30
|
+
return variate.variation, in_experiment && !variate.untracked
|
33
31
|
end
|
34
32
|
end
|
35
|
-
# The
|
33
|
+
# The context's bucket value was greater than or equal to the end of the last bucket. This could happen due
|
36
34
|
# to a rounding error, or due to the fact that we are scaling to 100000 rather than 99999, or the flag
|
37
35
|
# data could contain buckets that don't actually add up to 100000. Rather than returning an error in
|
38
|
-
# this case (or changing the scaling, which would potentially change the results for *all*
|
39
|
-
# will simply put the
|
36
|
+
# this case (or changing the scaling, which would potentially change the results for *all* contexts), we
|
37
|
+
# will simply put the context in the last bucket.
|
40
38
|
last_variation = variations[-1]
|
41
|
-
|
42
|
-
|
43
|
-
[last_variation[:variation], in_experiment]
|
39
|
+
[last_variation.variation, in_experiment && !last_variation.untracked]
|
44
40
|
else # the rule isn't well-formed
|
45
41
|
[nil, false]
|
46
42
|
end
|
47
43
|
end
|
48
44
|
|
49
|
-
# Returns a
|
45
|
+
# Returns a context's bucket value as a floating-point value in `[0, 1)`.
|
50
46
|
#
|
51
|
-
# @param
|
47
|
+
# @param context [LDContext] the context properties
|
48
|
+
# @param context_kind [String, nil] the context kind to match against
|
52
49
|
# @param key [String] the feature flag key (or segment key, if this is for a segment rule)
|
53
|
-
# @param bucket_by [String|Symbol] the name of the
|
50
|
+
# @param bucket_by [String|Symbol] the name of the context attribute to be used for bucketing
|
54
51
|
# @param salt [String] the feature flag's or segment's salt value
|
55
|
-
# @return [
|
56
|
-
|
57
|
-
|
52
|
+
# @return [Float, nil] the bucket value, from 0 inclusive to 1 exclusive
|
53
|
+
# @raise [InvalidReferenceException] Raised if the clause.attribute is an invalid reference
|
54
|
+
def self.bucket_context(context, context_kind, key, bucket_by, salt, seed)
|
55
|
+
matched_context = context.individual_context(context_kind || LaunchDarkly::LDContext::KIND_DEFAULT)
|
56
|
+
return nil if matched_context.nil?
|
58
57
|
|
59
|
-
|
60
|
-
|
61
|
-
return 0.0
|
62
|
-
end
|
58
|
+
reference = (context_kind.nil? || context_kind.empty?) ? Reference.create_literal(bucket_by) : Reference.create(bucket_by)
|
59
|
+
raise InvalidReferenceException.new(reference.error) unless reference.error.nil?
|
63
60
|
|
64
|
-
|
65
|
-
|
66
|
-
|
61
|
+
context_value = matched_context.get_value_for_reference(reference)
|
62
|
+
return 0.0 if context_value.nil?
|
63
|
+
|
64
|
+
id_hash = bucketable_string_value(context_value)
|
65
|
+
return 0.0 if id_hash.nil?
|
67
66
|
|
68
67
|
if seed
|
69
68
|
hash_key = "%d.%s" % [seed, id_hash]
|
@@ -71,7 +70,7 @@ module LaunchDarkly
|
|
71
70
|
hash_key = "%s.%s.%s" % [key, salt, id_hash]
|
72
71
|
end
|
73
72
|
|
74
|
-
hash_val =
|
73
|
+
hash_val = Digest::SHA1.hexdigest(hash_key)[0..14]
|
75
74
|
hash_val.to_i(16) / Float(0xFFFFFFFFFFFFFFF)
|
76
75
|
end
|
77
76
|
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require "ldclient-rb/evaluation_detail"
|
2
|
+
|
3
|
+
# This file contains any pieces of low-level evaluation logic that don't need to be inside the Evaluator
|
4
|
+
# class, because they don't depend on any SDK state outside of their input parameters.
|
5
|
+
|
6
|
+
module LaunchDarkly
|
7
|
+
module Impl
|
8
|
+
module EvaluatorHelpers
|
9
|
+
#
|
10
|
+
# @param flag [LaunchDarkly::Impl::Model::FeatureFlag]
|
11
|
+
# @param reason [LaunchDarkly::EvaluationReason]
|
12
|
+
#
|
13
|
+
def self.evaluation_detail_for_off_variation(flag, reason)
|
14
|
+
index = flag.off_variation
|
15
|
+
index.nil? ? EvaluationDetail.new(nil, nil, reason) : evaluation_detail_for_variation(flag, index, reason)
|
16
|
+
end
|
17
|
+
|
18
|
+
#
|
19
|
+
# @param flag [LaunchDarkly::Impl::Model::FeatureFlag]
|
20
|
+
# @param index [Integer]
|
21
|
+
# @param reason [LaunchDarkly::EvaluationReason]
|
22
|
+
#
|
23
|
+
def self.evaluation_detail_for_variation(flag, index, reason)
|
24
|
+
vars = flag.variations
|
25
|
+
if index < 0 || index >= vars.length
|
26
|
+
EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
|
27
|
+
# This error condition has already been logged at the time we received the flag data - see model/feature_flag.rb
|
28
|
+
else
|
29
|
+
EvaluationDetail.new(vars[index], index, reason)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
#
|
34
|
+
# @param context [LaunchDarkly::LDContext]
|
35
|
+
# @param kind [String, nil]
|
36
|
+
# @param keys [Enumerable<String>]
|
37
|
+
# @return [Boolean]
|
38
|
+
#
|
39
|
+
def self.context_key_in_target_list(context, kind, keys)
|
40
|
+
return false unless keys.is_a? Enumerable
|
41
|
+
return false if keys.empty?
|
42
|
+
|
43
|
+
matched_context = context.individual_context(kind || LaunchDarkly::LDContext::KIND_DEFAULT)
|
44
|
+
return false if matched_context.nil?
|
45
|
+
|
46
|
+
keys.include? matched_context.key
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -9,24 +9,24 @@ module LaunchDarkly
|
|
9
9
|
# Applies an operator to produce a boolean result.
|
10
10
|
#
|
11
11
|
# @param op [Symbol] one of the supported LaunchDarkly operators, as a symbol
|
12
|
-
# @param
|
12
|
+
# @param context_value the value of the context attribute that is referenced in the current clause (left-hand
|
13
13
|
# side of the expression)
|
14
|
-
# @param clause_value the constant value that `
|
14
|
+
# @param clause_value the constant value that `context_value` is being compared to (right-hand side of the
|
15
15
|
# expression)
|
16
16
|
# @return [Boolean] true if the expression should be considered a match; false if it is not a match, or
|
17
17
|
# if the values cannot be compared because they are of the wrong types, or if the operator is unknown
|
18
|
-
def self.apply(op,
|
18
|
+
def self.apply(op, context_value, clause_value)
|
19
19
|
case op
|
20
20
|
when :in
|
21
|
-
|
21
|
+
context_value == clause_value
|
22
22
|
when :startsWith
|
23
|
-
string_op(
|
23
|
+
string_op(context_value, clause_value, lambda { |a, b| a.start_with? b })
|
24
24
|
when :endsWith
|
25
|
-
string_op(
|
25
|
+
string_op(context_value, clause_value, lambda { |a, b| a.end_with? b })
|
26
26
|
when :contains
|
27
|
-
string_op(
|
27
|
+
string_op(context_value, clause_value, lambda { |a, b| a.include? b })
|
28
28
|
when :matches
|
29
|
-
string_op(
|
29
|
+
string_op(context_value, clause_value, lambda { |a, b|
|
30
30
|
begin
|
31
31
|
re = Regexp.new b
|
32
32
|
!re.match(a).nil?
|
@@ -35,76 +35,47 @@ module LaunchDarkly
|
|
35
35
|
end
|
36
36
|
})
|
37
37
|
when :lessThan
|
38
|
-
numeric_op(
|
38
|
+
numeric_op(context_value, clause_value, lambda { |a, b| a < b })
|
39
39
|
when :lessThanOrEqual
|
40
|
-
numeric_op(
|
40
|
+
numeric_op(context_value, clause_value, lambda { |a, b| a <= b })
|
41
41
|
when :greaterThan
|
42
|
-
numeric_op(
|
42
|
+
numeric_op(context_value, clause_value, lambda { |a, b| a > b })
|
43
43
|
when :greaterThanOrEqual
|
44
|
-
numeric_op(
|
44
|
+
numeric_op(context_value, clause_value, lambda { |a, b| a >= b })
|
45
45
|
when :before
|
46
|
-
date_op(
|
46
|
+
date_op(context_value, clause_value, lambda { |a, b| a < b })
|
47
47
|
when :after
|
48
|
-
date_op(
|
48
|
+
date_op(context_value, clause_value, lambda { |a, b| a > b })
|
49
49
|
when :semVerEqual
|
50
|
-
semver_op(
|
50
|
+
semver_op(context_value, clause_value, lambda { |a, b| a == b })
|
51
51
|
when :semVerLessThan
|
52
|
-
semver_op(
|
52
|
+
semver_op(context_value, clause_value, lambda { |a, b| a < b })
|
53
53
|
when :semVerGreaterThan
|
54
|
-
semver_op(
|
54
|
+
semver_op(context_value, clause_value, lambda { |a, b| a > b })
|
55
55
|
when :segmentMatch
|
56
56
|
# We should never reach this; it can't be evaluated based on just two parameters, because it requires
|
57
|
-
# looking up the segment from the data store. Instead, we special-case this operator in
|
57
|
+
# looking up the segment from the data store. Instead, we special-case this operator in clause_match_context.
|
58
58
|
false
|
59
59
|
else
|
60
60
|
false
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
64
|
-
# Retrieves the value of a user attribute by name.
|
65
|
-
#
|
66
|
-
# Built-in attributes correspond to top-level properties in the user object. They are treated as strings and
|
67
|
-
# non-string values are coerced to strings, except for `anonymous` which is meant to be a boolean if present
|
68
|
-
# and is not currently coerced. This behavior is consistent with earlier versions of the Ruby SDK, but is not
|
69
|
-
# guaranteed to be consistent with other SDKs, since the evaluator specification is based on the strongly-typed
|
70
|
-
# SDKs where it is not possible for an attribute to have the wrong type.
|
71
|
-
#
|
72
|
-
# Custom attributes correspond to properties within the `custom` property, if any, and can be of any type.
|
73
|
-
#
|
74
|
-
# @param user [Object] the user properties
|
75
|
-
# @param attribute [String|Symbol] the attribute to get, for instance `:key` or `:name` or `:some_custom_attr`
|
76
|
-
# @return the attribute value, or nil if the attribute is unknown
|
77
|
-
def self.user_value(user, attribute)
|
78
|
-
attribute = attribute.to_sym
|
79
|
-
if BUILTINS.include? attribute
|
80
|
-
value = user[attribute]
|
81
|
-
return nil if value.nil?
|
82
|
-
(attribute == :anonymous) ? value : value.to_s
|
83
|
-
elsif !user[:custom].nil?
|
84
|
-
user[:custom][attribute]
|
85
|
-
else
|
86
|
-
nil
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
64
|
private
|
91
65
|
|
92
|
-
BUILTINS = Set[:key, :ip, :country, :email, :firstName, :lastName, :avatar, :name, :anonymous]
|
93
66
|
NUMERIC_VERSION_COMPONENTS_REGEX = Regexp.new("^[0-9.]*")
|
94
|
-
|
95
|
-
private_constant :BUILTINS
|
96
67
|
private_constant :NUMERIC_VERSION_COMPONENTS_REGEX
|
97
68
|
|
98
|
-
def self.string_op(
|
99
|
-
(
|
69
|
+
def self.string_op(context_value, clause_value, fn)
|
70
|
+
(context_value.is_a? String) && (clause_value.is_a? String) && fn.call(context_value, clause_value)
|
100
71
|
end
|
101
72
|
|
102
|
-
def self.numeric_op(
|
103
|
-
(
|
73
|
+
def self.numeric_op(context_value, clause_value, fn)
|
74
|
+
(context_value.is_a? Numeric) && (clause_value.is_a? Numeric) && fn.call(context_value, clause_value)
|
104
75
|
end
|
105
76
|
|
106
|
-
def self.date_op(
|
107
|
-
ud = to_date(
|
77
|
+
def self.date_op(context_value, clause_value, fn)
|
78
|
+
ud = to_date(context_value)
|
108
79
|
if !ud.nil?
|
109
80
|
cd = to_date(clause_value)
|
110
81
|
!cd.nil? && fn.call(ud, cd)
|
@@ -113,8 +84,8 @@ module LaunchDarkly
|
|
113
84
|
end
|
114
85
|
end
|
115
86
|
|
116
|
-
def self.semver_op(
|
117
|
-
uv = to_semver(
|
87
|
+
def self.semver_op(context_value, clause_value, fn)
|
88
|
+
uv = to_semver(context_value)
|
118
89
|
if !uv.nil?
|
119
90
|
cv = to_semver(clause_value)
|
120
91
|
!cv.nil? && fn.call(uv, cv)
|
@@ -8,7 +8,7 @@ module LaunchDarkly
|
|
8
8
|
EventSenderResult = Struct.new(:success, :must_shutdown, :time_from_server)
|
9
9
|
|
10
10
|
class EventSender
|
11
|
-
CURRENT_SCHEMA_VERSION =
|
11
|
+
CURRENT_SCHEMA_VERSION = 4
|
12
12
|
DEFAULT_RETRY_INTERVAL = 1
|
13
13
|
|
14
14
|
def initialize(sdk_key, config, http_client = nil, retry_interval = DEFAULT_RETRY_INTERVAL)
|
@@ -33,7 +33,7 @@ module LaunchDarkly
|
|
33
33
|
begin
|
34
34
|
http_client = @http_client_pool.acquire()
|
35
35
|
response = nil
|
36
|
-
|
36
|
+
2.times do |attempt|
|
37
37
|
if attempt > 0
|
38
38
|
@logger.warn { "[LDClient] Will retry posting events after #{@retry_interval} second" }
|
39
39
|
sleep(@retry_interval)
|
@@ -43,13 +43,13 @@ module LaunchDarkly
|
|
43
43
|
headers = {}
|
44
44
|
headers["content-type"] = "application/json"
|
45
45
|
Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v| headers[k] = v }
|
46
|
-
|
46
|
+
unless is_diagnostic
|
47
47
|
headers["X-LaunchDarkly-Event-Schema"] = CURRENT_SCHEMA_VERSION.to_s
|
48
48
|
headers["X-LaunchDarkly-Payload-ID"] = payload_id
|
49
49
|
end
|
50
50
|
response = http_client.request("POST", uri, {
|
51
51
|
headers: headers,
|
52
|
-
body: event_data
|
52
|
+
body: event_data,
|
53
53
|
})
|
54
54
|
rescue StandardError => exn
|
55
55
|
@logger.warn { "[LDClient] Error sending events: #{exn.inspect}." }
|
@@ -60,10 +60,11 @@ module LaunchDarkly
|
|
60
60
|
body = response.to_s
|
61
61
|
if status >= 200 && status < 300
|
62
62
|
res_time = nil
|
63
|
-
|
63
|
+
unless response.headers["date"].nil?
|
64
64
|
begin
|
65
65
|
res_time = Time.httpdate(response.headers["date"])
|
66
66
|
rescue ArgumentError
|
67
|
+
# Ignored
|
67
68
|
end
|
68
69
|
end
|
69
70
|
return EventSenderResult.new(true, false, res_time)
|
@@ -77,7 +78,7 @@ module LaunchDarkly
|
|
77
78
|
end
|
78
79
|
end
|
79
80
|
# used up our retries
|
80
|
-
|
81
|
+
EventSenderResult.new(false, false, nil)
|
81
82
|
ensure
|
82
83
|
@http_client_pool.release(http_client)
|
83
84
|
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require "ldclient-rb/impl/event_types"
|
2
|
+
require "set"
|
3
|
+
|
4
|
+
module LaunchDarkly
|
5
|
+
module Impl
|
6
|
+
EventSummary = Struct.new(:start_date, :end_date, :counters)
|
7
|
+
|
8
|
+
EventSummaryFlagInfo = Struct.new(:default, :versions, :context_kinds)
|
9
|
+
|
10
|
+
EventSummaryFlagVariationCounter = Struct.new(:value, :count)
|
11
|
+
|
12
|
+
# Manages the state of summarizable information for the EventProcessor, including the
|
13
|
+
# event counters and context deduplication. Note that the methods of this class are
|
14
|
+
# deliberately not thread-safe; the EventProcessor is responsible for enforcing
|
15
|
+
# synchronization across both the summarizer and the event queue.
|
16
|
+
class EventSummarizer
|
17
|
+
class Counter
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
clear
|
22
|
+
end
|
23
|
+
|
24
|
+
# Adds this event to our counters, if it is a type of event we need to count.
|
25
|
+
def summarize_event(event)
|
26
|
+
return unless event.is_a?(LaunchDarkly::Impl::EvalEvent)
|
27
|
+
|
28
|
+
counters_for_flag = @counters[event.key]
|
29
|
+
if counters_for_flag.nil?
|
30
|
+
counters_for_flag = EventSummaryFlagInfo.new(event.default, Hash.new, Set.new)
|
31
|
+
@counters[event.key] = counters_for_flag
|
32
|
+
end
|
33
|
+
|
34
|
+
counters_for_flag_version = counters_for_flag.versions[event.version]
|
35
|
+
if counters_for_flag_version.nil?
|
36
|
+
counters_for_flag_version = Hash.new
|
37
|
+
counters_for_flag.versions[event.version] = counters_for_flag_version
|
38
|
+
end
|
39
|
+
|
40
|
+
counters_for_flag.context_kinds.merge(event.context.kinds)
|
41
|
+
|
42
|
+
variation_counter = counters_for_flag_version[event.variation]
|
43
|
+
if variation_counter.nil?
|
44
|
+
counters_for_flag_version[event.variation] = EventSummaryFlagVariationCounter.new(event.value, 1)
|
45
|
+
else
|
46
|
+
variation_counter.count = variation_counter.count + 1
|
47
|
+
end
|
48
|
+
|
49
|
+
time = event.timestamp
|
50
|
+
unless time.nil?
|
51
|
+
@start_date = time if @start_date == 0 || time < @start_date
|
52
|
+
@end_date = time if time > @end_date
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns a snapshot of the current summarized event data, and resets this state.
|
57
|
+
def snapshot
|
58
|
+
EventSummary.new(@start_date, @end_date, @counters)
|
59
|
+
end
|
60
|
+
|
61
|
+
def clear
|
62
|
+
@start_date = 0
|
63
|
+
@end_date = 0
|
64
|
+
@counters = {}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module LaunchDarkly
|
4
|
+
module Impl
|
5
|
+
class Event
|
6
|
+
# @param timestamp [Integer]
|
7
|
+
# @param context [LaunchDarkly::LDContext]
|
8
|
+
# @param sampling_ratio [Integer, nil]
|
9
|
+
# @param exclude_from_summaries [Boolean]
|
10
|
+
def initialize(timestamp, context, sampling_ratio = nil, exclude_from_summaries = false)
|
11
|
+
@timestamp = timestamp
|
12
|
+
@context = context
|
13
|
+
@sampling_ratio = sampling_ratio
|
14
|
+
@exclude_from_summaries = exclude_from_summaries
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [Integer]
|
18
|
+
attr_reader :timestamp
|
19
|
+
# @return [LaunchDarkly::LDContext]
|
20
|
+
attr_reader :context
|
21
|
+
# @return [Integer, nil]
|
22
|
+
attr_reader :sampling_ratio
|
23
|
+
# @return [Boolean]
|
24
|
+
attr_reader :exclude_from_summaries
|
25
|
+
end
|
26
|
+
|
27
|
+
class EvalEvent < Event
|
28
|
+
def initialize(timestamp, context, key, version = nil, variation = nil, value = nil, reason = nil, default = nil,
|
29
|
+
track_events = false, debug_until = nil, prereq_of = nil, sampling_ratio = nil, exclude_from_summaries = false)
|
30
|
+
super(timestamp, context, sampling_ratio, exclude_from_summaries)
|
31
|
+
@key = key
|
32
|
+
@version = version
|
33
|
+
@variation = variation
|
34
|
+
@value = value
|
35
|
+
@reason = reason
|
36
|
+
@default = default
|
37
|
+
# avoid setting rarely-used attributes if they have no value - this saves a little space per instance
|
38
|
+
@track_events = track_events if track_events
|
39
|
+
@debug_until = debug_until if debug_until
|
40
|
+
@prereq_of = prereq_of if prereq_of
|
41
|
+
end
|
42
|
+
|
43
|
+
attr_reader :key
|
44
|
+
attr_reader :version
|
45
|
+
attr_reader :variation
|
46
|
+
attr_reader :value
|
47
|
+
attr_reader :reason
|
48
|
+
attr_reader :default
|
49
|
+
attr_reader :track_events
|
50
|
+
attr_reader :debug_until
|
51
|
+
attr_reader :prereq_of
|
52
|
+
end
|
53
|
+
|
54
|
+
class MigrationOpEvent < Event
|
55
|
+
#
|
56
|
+
# A migration op event represents the results of a migration-assisted read or write operation.
|
57
|
+
#
|
58
|
+
# The event includes optional measurements reporting on consistency checks, error reporting, and operation latency
|
59
|
+
# values.
|
60
|
+
#
|
61
|
+
# @param timestamp [Integer]
|
62
|
+
# @param context [LaunchDarkly::LDContext]
|
63
|
+
# @param key [string]
|
64
|
+
# @param flag [LaunchDarkly::Impl::Model::FeatureFlag, nil]
|
65
|
+
# @param operation [Symbol]
|
66
|
+
# @param default_stage [Symbol]
|
67
|
+
# @param evaluation [LaunchDarkly::EvaluationDetail]
|
68
|
+
# @param invoked [Set]
|
69
|
+
# @param consistency_check [Boolean, nil]
|
70
|
+
# @param consistency_check_ratio [Integer, nil]
|
71
|
+
# @param errors [Set]
|
72
|
+
# @param latencies [Hash<Symbol, Float>]
|
73
|
+
#
|
74
|
+
def initialize(timestamp, context, key, flag, operation, default_stage, evaluation, invoked, consistency_check, consistency_check_ratio, errors, latencies)
|
75
|
+
super(timestamp, context)
|
76
|
+
@operation = operation
|
77
|
+
@key = key
|
78
|
+
@version = flag&.version
|
79
|
+
@sampling_ratio = flag&.sampling_ratio
|
80
|
+
@default = default_stage
|
81
|
+
@evaluation = evaluation
|
82
|
+
@consistency_check = consistency_check
|
83
|
+
@consistency_check_ratio = consistency_check.nil? ? nil : consistency_check_ratio
|
84
|
+
@invoked = invoked
|
85
|
+
@errors = errors
|
86
|
+
@latencies = latencies
|
87
|
+
end
|
88
|
+
|
89
|
+
attr_reader :operation
|
90
|
+
attr_reader :key
|
91
|
+
attr_reader :version
|
92
|
+
attr_reader :sampling_ratio
|
93
|
+
attr_reader :default
|
94
|
+
attr_reader :evaluation
|
95
|
+
attr_reader :consistency_check
|
96
|
+
attr_reader :consistency_check_ratio
|
97
|
+
attr_reader :invoked
|
98
|
+
attr_reader :errors
|
99
|
+
attr_reader :latencies
|
100
|
+
end
|
101
|
+
|
102
|
+
class IdentifyEvent < Event
|
103
|
+
def initialize(timestamp, context)
|
104
|
+
super(timestamp, context)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
class CustomEvent < Event
|
109
|
+
def initialize(timestamp, context, key, data = nil, metric_value = nil)
|
110
|
+
super(timestamp, context)
|
111
|
+
@key = key
|
112
|
+
@data = data unless data.nil?
|
113
|
+
@metric_value = metric_value unless metric_value.nil?
|
114
|
+
end
|
115
|
+
|
116
|
+
attr_reader :key
|
117
|
+
attr_reader :data
|
118
|
+
attr_reader :metric_value
|
119
|
+
end
|
120
|
+
|
121
|
+
class IndexEvent < Event
|
122
|
+
def initialize(timestamp, context)
|
123
|
+
super(timestamp, context)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
class DebugEvent < Event
|
128
|
+
def initialize(eval_event)
|
129
|
+
super(eval_event.timestamp, eval_event.context)
|
130
|
+
@eval_event = eval_event
|
131
|
+
end
|
132
|
+
|
133
|
+
attr_reader :eval_event
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require "concurrent"
|
2
|
+
require "ldclient-rb/interfaces"
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
module LaunchDarkly
|
6
|
+
module Impl
|
7
|
+
class FlagTracker
|
8
|
+
include LaunchDarkly::Interfaces::FlagTracker
|
9
|
+
|
10
|
+
extend Forwardable
|
11
|
+
def_delegators :@broadcaster, :add_listener, :remove_listener
|
12
|
+
|
13
|
+
def initialize(broadcaster, eval_fn)
|
14
|
+
@broadcaster = broadcaster
|
15
|
+
@eval_fn = eval_fn
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_flag_value_change_listener(key, context, listener)
|
19
|
+
flag_change_listener = FlagValueChangeAdapter.new(key, context, listener, @eval_fn)
|
20
|
+
add_listener(flag_change_listener)
|
21
|
+
|
22
|
+
flag_change_listener
|
23
|
+
end
|
24
|
+
|
25
|
+
#
|
26
|
+
# An adapter which turns a normal flag change listener into a flag value change listener.
|
27
|
+
#
|
28
|
+
class FlagValueChangeAdapter
|
29
|
+
# @param [Symbol] flag_key
|
30
|
+
# @param [LaunchDarkly::LDContext] context
|
31
|
+
# @param [#update] listener
|
32
|
+
# @param [#call] eval_fn
|
33
|
+
def initialize(flag_key, context, listener, eval_fn)
|
34
|
+
@flag_key = flag_key
|
35
|
+
@context = context
|
36
|
+
@listener = listener
|
37
|
+
@eval_fn = eval_fn
|
38
|
+
@value = Concurrent::AtomicReference.new(@eval_fn.call(@flag_key, @context))
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# @param [LaunchDarkly::Interfaces::FlagChange] flag_change
|
43
|
+
#
|
44
|
+
def update(flag_change)
|
45
|
+
return unless flag_change.key == @flag_key
|
46
|
+
|
47
|
+
new_eval = @eval_fn.call(@flag_key, @context)
|
48
|
+
old_eval = @value.get_and_set(new_eval)
|
49
|
+
|
50
|
+
return if new_eval == old_eval
|
51
|
+
|
52
|
+
@listener.update(
|
53
|
+
LaunchDarkly::Interfaces::FlagValueChange.new(@flag_key, old_eval, new_eval))
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|