Skip to content

Commit 3f5cb37

Browse files
authored
Merge pull request #42 from launchdarkly/eb/ch12904/summary-events
implement summary events
2 parents c62d796 + 19ee62f commit 3f5cb37

15 files changed

+986
-171
lines changed

lib/ldclient-rb.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
require "ldclient-rb/newrelic"
99
require "ldclient-rb/stream"
1010
require "ldclient-rb/polling"
11-
require "ldclient-rb/event_serializer"
11+
require "ldclient-rb/user_filter"
12+
require "ldclient-rb/simple_lru_cache"
13+
require "ldclient-rb/event_summarizer"
1214
require "ldclient-rb/events"
1315
require "ldclient-rb/redis_store"
1416
require "ldclient-rb/requestor"

lib/ldclient-rb/config.rb

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,15 @@ class Config
5454
# @option opts [Boolean] :send_events (true) Whether or not to send events back to LaunchDarkly.
5555
# This differs from `offline` in that it affects only the sending of client-side events, not
5656
# streaming or polling for events from the server.
57-
#
57+
# @option opts [Integer] :user_keys_capacity (1000) The number of user keys that the event processor
58+
# can remember at any one time, so that duplicate user details will not be sent in analytics events.
59+
# @option opts [Float] :user_keys_flush_interval (300) The interval in seconds at which the event
60+
# processor will reset its set of known user keys.
61+
# @option opts [Boolean] :inline_users_in_events (false) Whether to include full user details in every
62+
# analytics event. By default, events will only include the user key, except for one "index" event
63+
# that provides the full details for the user.
64+
# @option opts [Object] :update_processor An object that will receive feature flag data from LaunchDarkly.
65+
# Defaults to either the streaming or the polling processor, can be customized for tests.
5866
# @return [type] [description]
5967
# rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
6068
def initialize(opts = {})
@@ -76,6 +84,10 @@ def initialize(opts = {})
7684
@all_attributes_private = opts[:all_attributes_private] || false
7785
@private_attribute_names = opts[:private_attribute_names] || []
7886
@send_events = opts.has_key?(:send_events) ? opts[:send_events] : Config.default_send_events
87+
@user_keys_capacity = opts[:user_keys_capacity] || Config.default_user_keys_capacity
88+
@user_keys_flush_interval = opts[:user_keys_flush_interval] || Config.default_user_keys_flush_interval
89+
@inline_users_in_events = opts[:inline_users_in_events] || false
90+
@update_processor = opts[:update_processor]
7991
end
8092

8193
#
@@ -186,6 +198,26 @@ def offline?
186198
#
187199
attr_reader :send_events
188200

201+
#
202+
# The number of user keys that the event processor can remember at any one time, so that
203+
# duplicate user details will not be sent in analytics events.
204+
#
205+
attr_reader :user_keys_capacity
206+
207+
#
208+
# The interval in seconds at which the event processor will reset its set of known user keys.
209+
#
210+
attr_reader :user_keys_flush_interval
211+
212+
#
213+
# Whether to include full user details in every
214+
# analytics event. By default, events will only include the user key, except for one "index" event
215+
# that provides the full details for the user.
216+
#
217+
attr_reader :inline_users_in_events
218+
219+
attr_reader :update_processor
220+
189221
#
190222
# The default LaunchDarkly client configuration. This configuration sets
191223
# reasonable defaults for most users.
@@ -264,5 +296,13 @@ def self.default_poll_interval
264296
def self.default_send_events
265297
true
266298
end
299+
300+
def self.default_user_keys_capacity
301+
1000
302+
end
303+
304+
def self.default_user_keys_flush_interval
305+
300
306+
end
267307
end
268308
end

lib/ldclient-rb/evaluation.rb

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -127,16 +127,19 @@ def evaluate(flag, user, store)
127127

128128
if flag[:on]
129129
res = eval_internal(flag, user, store, events)
130-
131-
return { value: res, events: events } if !res.nil?
130+
if !res.nil?
131+
res[:events] = events
132+
return res
133+
end
132134
end
133135

134-
if !flag[:offVariation].nil? && flag[:offVariation] < flag[:variations].length
135-
value = flag[:variations][flag[:offVariation]]
136-
return { value: value, events: events }
136+
offVariation = flag[:offVariation]
137+
if !offVariation.nil? && offVariation < flag[:variations].length
138+
value = flag[:variations][offVariation]
139+
return { variation: offVariation, value: value, events: events }
137140
end
138141

139-
{ value: nil, events: events }
142+
{ variation: nil, value: nil, events: events }
140143
end
141144

142145
def eval_internal(flag, user, store, events)
@@ -150,9 +153,13 @@ def eval_internal(flag, user, store, events)
150153
else
151154
begin
152155
prereq_res = eval_internal(prereq_flag, user, store, events)
153-
variation = get_variation(prereq_flag, prerequisite[:variation])
154-
events.push(kind: "feature", key: prereq_flag[:key], value: prereq_res, version: prereq_flag[:version], prereqOf: flag[:key])
155-
if prereq_res.nil? || prereq_res != variation
156+
event = {
157+
kind: "feature", key: prereq_flag[:key], variation: prereq_res[:variation],
158+
value: prereq_res[:value], version: prereq_flag[:version], prereqOf: flag[:key],
159+
trackEvents: prereq_flag[:trackEvents], debugEventsUntilDate: prereq_flag[:debugEventsUntilDate]
160+
}
161+
events.push(event)
162+
if prereq_res.nil? || prereq_res[:variation] != prerequisite[:variation]
156163
failed_prereq = true
157164
end
158165
rescue => exn
@@ -175,7 +182,9 @@ def eval_rules(flag, user, store)
175182
# Check user target matches
176183
(flag[:targets] || []).each do |target|
177184
(target[:values] || []).each do |value|
178-
return get_variation(flag, target[:variation]) if value == user[:key]
185+
if value == user[:key]
186+
return { variation: target[:variation], value: get_variation(flag, target[:variation]) }
187+
end
179188
end
180189
end
181190

@@ -245,15 +254,17 @@ def clause_match_user_no_segments(clause, user)
245254

246255
def variation_for_user(rule, user, flag)
247256
if !rule[:variation].nil? # fixed variation
248-
return get_variation(flag, rule[:variation])
257+
return { variation: rule[:variation], value: get_variation(flag, rule[:variation]) }
249258
elsif !rule[:rollout].nil? # percentage rollout
250259
rollout = rule[:rollout]
251260
bucket_by = rollout[:bucketBy].nil? ? "key" : rollout[:bucketBy]
252261
bucket = bucket_user(user, flag[:key], bucket_by, flag[:salt])
253262
sum = 0;
254263
rollout[:variations].each do |variate|
255264
sum += variate[:weight].to_f / 100000.0
256-
return get_variation(flag, variate[:variation]) if bucket < sum
265+
if bucket < sum
266+
return { variation: variate[:variation], value: get_variation(flag, variate[:variation]) }
267+
end
257268
end
258269
nil
259270
else # the rule isn't well-formed
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
2+
module LaunchDarkly
3+
EventSummarySnapshot = Struct.new(:start_date, :end_date, :counters)
4+
5+
# Manages the state of summarizable information for the EventProcessor, including the
6+
# event counters and user deduplication. Note that the methods of this class are
7+
# deliberately not thread-safe; the EventProcessor is responsible for enforcing
8+
# synchronization across both the summarizer and the event queue.
9+
class EventSummarizer
10+
def initialize(config)
11+
@config = config
12+
@users = SimpleLRUCacheSet.new(@config.user_keys_capacity)
13+
reset_state
14+
end
15+
16+
# Adds to the set of users we've noticed, and return true if the user was already known to us.
17+
def notice_user(user)
18+
if user.nil? || !user.has_key?(:key)
19+
true
20+
else
21+
@users.add(user[:key])
22+
end
23+
end
24+
25+
# Resets the set of users we've seen.
26+
def reset_users
27+
@users.reset
28+
end
29+
30+
# Adds this event to our counters, if it is a type of event we need to count.
31+
def summarize_event(event)
32+
if event[:kind] == "feature"
33+
counter_key = {
34+
key: event[:key],
35+
version: event[:version],
36+
variation: event[:variation]
37+
}
38+
c = @counters[counter_key]
39+
if c.nil?
40+
@counters[counter_key] = {
41+
value: event[:value],
42+
default: event[:default],
43+
count: 1
44+
}
45+
else
46+
c[:count] = c[:count] + 1
47+
end
48+
time = event[:creationDate]
49+
if !time.nil?
50+
@start_date = time if @start_date == 0 || time < @start_date
51+
@end_date = time if time > @end_date
52+
end
53+
end
54+
end
55+
56+
# Returns a snapshot of the current summarized event data, and resets this state.
57+
def snapshot
58+
ret = {
59+
start_date: @start_date,
60+
end_date: @end_date,
61+
counters: @counters
62+
}
63+
reset_state
64+
ret
65+
end
66+
67+
# Transforms the summary data into the format used for event sending.
68+
def output(snapshot)
69+
flags = {}
70+
snapshot[:counters].each { |ckey, cval|
71+
flag = flags[ckey[:key]]
72+
if flag.nil?
73+
flag = {
74+
default: cval[:default],
75+
counters: []
76+
}
77+
flags[ckey[:key]] = flag
78+
end
79+
c = {
80+
value: cval[:value],
81+
count: cval[:count]
82+
}
83+
if ckey[:version].nil?
84+
c[:unknown] = true
85+
else
86+
c[:version] = ckey[:version]
87+
end
88+
flag[:counters].push(c)
89+
}
90+
{
91+
startDate: snapshot[:start_date],
92+
endDate: snapshot[:end_date],
93+
features: flags
94+
}
95+
end
96+
97+
private
98+
99+
def reset_state
100+
@start_date = 0
101+
@end_date = 0
102+
@counters = {}
103+
end
104+
end
105+
end

0 commit comments

Comments
 (0)