launchdarkly-server-sdk 6.3.0 → 8.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|