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.
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