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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -4
  3. data/lib/ldclient-rb/config.rb +112 -62
  4. data/lib/ldclient-rb/context.rb +444 -0
  5. data/lib/ldclient-rb/evaluation_detail.rb +26 -22
  6. data/lib/ldclient-rb/events.rb +256 -146
  7. data/lib/ldclient-rb/flags_state.rb +26 -15
  8. data/lib/ldclient-rb/impl/big_segments.rb +18 -18
  9. data/lib/ldclient-rb/impl/broadcaster.rb +78 -0
  10. data/lib/ldclient-rb/impl/context.rb +96 -0
  11. data/lib/ldclient-rb/impl/context_filter.rb +145 -0
  12. data/lib/ldclient-rb/impl/data_source.rb +188 -0
  13. data/lib/ldclient-rb/impl/data_store.rb +59 -0
  14. data/lib/ldclient-rb/impl/dependency_tracker.rb +102 -0
  15. data/lib/ldclient-rb/impl/diagnostic_events.rb +9 -10
  16. data/lib/ldclient-rb/impl/evaluator.rb +386 -142
  17. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +40 -41
  18. data/lib/ldclient-rb/impl/evaluator_helpers.rb +50 -0
  19. data/lib/ldclient-rb/impl/evaluator_operators.rb +26 -55
  20. data/lib/ldclient-rb/impl/event_sender.rb +7 -6
  21. data/lib/ldclient-rb/impl/event_summarizer.rb +68 -0
  22. data/lib/ldclient-rb/impl/event_types.rb +136 -0
  23. data/lib/ldclient-rb/impl/flag_tracker.rb +58 -0
  24. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +19 -7
  25. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +38 -30
  26. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +24 -11
  27. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +109 -12
  28. data/lib/ldclient-rb/impl/migrations/migrator.rb +287 -0
  29. data/lib/ldclient-rb/impl/migrations/tracker.rb +136 -0
  30. data/lib/ldclient-rb/impl/model/clause.rb +45 -0
  31. data/lib/ldclient-rb/impl/model/feature_flag.rb +255 -0
  32. data/lib/ldclient-rb/impl/model/preprocessed_data.rb +64 -0
  33. data/lib/ldclient-rb/impl/model/segment.rb +132 -0
  34. data/lib/ldclient-rb/impl/model/serialization.rb +54 -44
  35. data/lib/ldclient-rb/impl/repeating_task.rb +3 -4
  36. data/lib/ldclient-rb/impl/sampler.rb +25 -0
  37. data/lib/ldclient-rb/impl/store_client_wrapper.rb +102 -8
  38. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +2 -2
  39. data/lib/ldclient-rb/impl/unbounded_pool.rb +1 -1
  40. data/lib/ldclient-rb/impl/util.rb +59 -1
  41. data/lib/ldclient-rb/in_memory_store.rb +9 -2
  42. data/lib/ldclient-rb/integrations/consul.rb +2 -2
  43. data/lib/ldclient-rb/integrations/dynamodb.rb +2 -2
  44. data/lib/ldclient-rb/integrations/file_data.rb +4 -4
  45. data/lib/ldclient-rb/integrations/redis.rb +5 -5
  46. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +287 -62
  47. data/lib/ldclient-rb/integrations/test_data.rb +18 -14
  48. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +20 -9
  49. data/lib/ldclient-rb/interfaces.rb +600 -14
  50. data/lib/ldclient-rb/ldclient.rb +314 -134
  51. data/lib/ldclient-rb/memoized_value.rb +1 -1
  52. data/lib/ldclient-rb/migrations.rb +230 -0
  53. data/lib/ldclient-rb/non_blocking_thread_pool.rb +1 -1
  54. data/lib/ldclient-rb/polling.rb +52 -6
  55. data/lib/ldclient-rb/reference.rb +274 -0
  56. data/lib/ldclient-rb/requestor.rb +9 -11
  57. data/lib/ldclient-rb/stream.rb +96 -34
  58. data/lib/ldclient-rb/util.rb +97 -14
  59. data/lib/ldclient-rb/version.rb +1 -1
  60. data/lib/ldclient-rb.rb +3 -4
  61. metadata +65 -23
  62. data/lib/ldclient-rb/event_summarizer.rb +0 -55
  63. data/lib/ldclient-rb/file_data_source.rb +0 -23
  64. data/lib/ldclient-rb/impl/event_factory.rb +0 -126
  65. data/lib/ldclient-rb/newrelic.rb +0 -17
  66. data/lib/ldclient-rb/redis_store.rb +0 -88
  67. 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 rule [Object] the rule
10
- # @param user [Object] the user properties
11
- # @return [Number] the variation index, or nil if there is an error
12
- def self.variation_index_for_user(flag, rule, user)
13
-
14
- variation = rule[:variation]
15
- return variation, false if !variation.nil? # fixed variation
16
- rollout = rule[: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[:variations]
17
+ variations = rollout.variations
19
18
  if !variations.nil? && variations.length > 0 # percentage rollout
20
- bucket_by = rollout[:bucketBy].nil? ? "key" : rollout[:bucketBy]
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[:seed]
23
- bucket = bucket_user(user, flag[:key], bucket_by, flag[:salt], seed) # may not be present
24
- sum = 0;
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
- if rollout[:kind] == "experiment" && !variate[:untracked]
27
- in_experiment = true
28
- end
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 user's bucket value was greater than or equal to the end of the last bucket. This could happen due
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* users), we
39
- # will simply put the user in the last bucket.
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
- in_experiment = rollout[:kind] == "experiment" && !last_variation[:untracked]
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 user's bucket value as a floating-point value in `[0, 1)`.
45
+ # Returns a context's bucket value as a floating-point value in `[0, 1)`.
50
46
  #
51
- # @param user [Object] the user properties
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 user attribute to be used for bucketing
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 [Number] the bucket value, from 0 inclusive to 1 exclusive
56
- def self.bucket_user(user, key, bucket_by, salt, seed)
57
- return nil unless user[:key]
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
- id_hash = bucketable_string_value(EvaluatorOperators.user_value(user, bucket_by))
60
- if id_hash.nil?
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
- if user[:secondary]
65
- id_hash += "." + user[:secondary].to_s
66
- end
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 = (Digest::SHA1.hexdigest(hash_key))[0..14]
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 user_value the value of the user attribute that is referenced in the current clause (left-hand
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 `user_value` is being compared to (right-hand side of the
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, user_value, clause_value)
18
+ def self.apply(op, context_value, clause_value)
19
19
  case op
20
20
  when :in
21
- user_value == clause_value
21
+ context_value == clause_value
22
22
  when :startsWith
23
- string_op(user_value, clause_value, lambda { |a, b| a.start_with? b })
23
+ string_op(context_value, clause_value, lambda { |a, b| a.start_with? b })
24
24
  when :endsWith
25
- string_op(user_value, clause_value, lambda { |a, b| a.end_with? b })
25
+ string_op(context_value, clause_value, lambda { |a, b| a.end_with? b })
26
26
  when :contains
27
- string_op(user_value, clause_value, lambda { |a, b| a.include? b })
27
+ string_op(context_value, clause_value, lambda { |a, b| a.include? b })
28
28
  when :matches
29
- string_op(user_value, clause_value, lambda { |a, b|
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(user_value, clause_value, lambda { |a, b| a < b })
38
+ numeric_op(context_value, clause_value, lambda { |a, b| a < b })
39
39
  when :lessThanOrEqual
40
- numeric_op(user_value, clause_value, lambda { |a, b| a <= b })
40
+ numeric_op(context_value, clause_value, lambda { |a, b| a <= b })
41
41
  when :greaterThan
42
- numeric_op(user_value, clause_value, lambda { |a, b| a > b })
42
+ numeric_op(context_value, clause_value, lambda { |a, b| a > b })
43
43
  when :greaterThanOrEqual
44
- numeric_op(user_value, clause_value, lambda { |a, b| a >= b })
44
+ numeric_op(context_value, clause_value, lambda { |a, b| a >= b })
45
45
  when :before
46
- date_op(user_value, clause_value, lambda { |a, b| a < b })
46
+ date_op(context_value, clause_value, lambda { |a, b| a < b })
47
47
  when :after
48
- date_op(user_value, clause_value, lambda { |a, b| a > b })
48
+ date_op(context_value, clause_value, lambda { |a, b| a > b })
49
49
  when :semVerEqual
50
- semver_op(user_value, clause_value, lambda { |a, b| a == b })
50
+ semver_op(context_value, clause_value, lambda { |a, b| a == b })
51
51
  when :semVerLessThan
52
- semver_op(user_value, clause_value, lambda { |a, b| a < b })
52
+ semver_op(context_value, clause_value, lambda { |a, b| a < b })
53
53
  when :semVerGreaterThan
54
- semver_op(user_value, clause_value, lambda { |a, b| a > b })
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 clause_match_user.
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(user_value, clause_value, fn)
99
- (user_value.is_a? String) && (clause_value.is_a? String) && fn.call(user_value, clause_value)
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(user_value, clause_value, fn)
103
- (user_value.is_a? Numeric) && (clause_value.is_a? Numeric) && fn.call(user_value, clause_value)
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(user_value, clause_value, fn)
107
- ud = to_date(user_value)
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(user_value, clause_value, fn)
117
- uv = to_semver(user_value)
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 = 3
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
- (0..1).each do |attempt|
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
- if !is_diagnostic
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
- if !response.headers["date"].nil?
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
- return EventSenderResult.new(false, false, nil)
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