Skip to content

Commit 53bf4d3

Browse files
prepare 5.6.0 release (#142)
1 parent 8dabcea commit 53bf4d3

File tree

8 files changed

+265
-91
lines changed

8 files changed

+265
-91
lines changed

Gemfile.lock

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
launchdarkly-server-sdk (5.5.7)
4+
launchdarkly-server-sdk (5.5.12)
55
concurrent-ruby (~> 1.0)
66
json (>= 1.8, < 3)
77
ld-eventsource (= 1.0.1)
@@ -35,7 +35,6 @@ GEM
3535
ffi (1.9.25)
3636
ffi (1.9.25-java)
3737
hitimes (1.3.1)
38-
hitimes (1.3.1-java)
3938
http_tools (0.4.5)
4039
jmespath (1.4.0)
4140
json (1.8.6)

lib/ldclient-rb/evaluation.rb

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -199,24 +199,24 @@ def error_result(errorKind, value = nil)
199199

200200
# Evaluates a feature flag and returns an EvalResult. The result.value will be nil if the flag returns
201201
# the default value. Error conditions produce a result with an error reason, not an exception.
202-
def evaluate(flag, user, store, logger)
202+
def evaluate(flag, user, store, logger, event_factory)
203203
if user.nil? || user[:key].nil?
204204
return EvalResult.new(error_result('USER_NOT_SPECIFIED'), [])
205205
end
206206

207207
sanitized_user = Util.stringify_attrs(user, USER_ATTRS_TO_STRINGIFY_FOR_EVALUATION)
208208

209209
events = []
210-
detail = eval_internal(flag, sanitized_user, store, events, logger)
210+
detail = eval_internal(flag, sanitized_user, store, events, logger, event_factory)
211211
return EvalResult.new(detail, events)
212212
end
213213

214-
def eval_internal(flag, user, store, events, logger)
214+
def eval_internal(flag, user, store, events, logger, event_factory)
215215
if !flag[:on]
216216
return get_off_value(flag, { kind: 'OFF' }, logger)
217217
end
218218

219-
prereq_failure_reason = check_prerequisites(flag, user, store, events, logger)
219+
prereq_failure_reason = check_prerequisites(flag, user, store, events, logger, event_factory)
220220
if !prereq_failure_reason.nil?
221221
return get_off_value(flag, prereq_failure_reason, logger)
222222
end
@@ -249,7 +249,7 @@ def eval_internal(flag, user, store, events, logger)
249249
return EvaluationDetail.new(nil, nil, { kind: 'FALLTHROUGH' })
250250
end
251251

252-
def check_prerequisites(flag, user, store, events, logger)
252+
def check_prerequisites(flag, user, store, events, logger, event_factory)
253253
(flag[:prerequisites] || []).each do |prerequisite|
254254
prereq_ok = true
255255
prereq_key = prerequisite[:key]
@@ -260,23 +260,13 @@ def check_prerequisites(flag, user, store, events, logger)
260260
prereq_ok = false
261261
else
262262
begin
263-
prereq_res = eval_internal(prereq_flag, user, store, events, logger)
263+
prereq_res = eval_internal(prereq_flag, user, store, events, logger, event_factory)
264264
# Note that if the prerequisite flag is off, we don't consider it a match no matter what its
265265
# off variation was. But we still need to evaluate it in order to generate an event.
266266
if !prereq_flag[:on] || prereq_res.variation_index != prerequisite[:variation]
267267
prereq_ok = false
268268
end
269-
event = {
270-
kind: "feature",
271-
key: prereq_key,
272-
user: user,
273-
variation: prereq_res.variation_index,
274-
value: prereq_res.value,
275-
version: prereq_flag[:version],
276-
prereqOf: flag[:key],
277-
trackEvents: prereq_flag[:trackEvents],
278-
debugEventsUntilDate: prereq_flag[:debugEventsUntilDate]
279-
}
269+
event = event_factory.new_eval_event(prereq_flag, user, prereq_res, nil, flag)
280270
events.push(event)
281271
rescue => exn
282272
Util.log_exception(logger, "Error evaluating prerequisite flag \"#{prereq_key}\" for flag \"#{flag[:key]}\"", exn)

lib/ldclient-rb/events.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,7 @@ def make_output_event(event)
456456
else
457457
out[:userKey] = event[:user].nil? ? nil : event[:user][:key]
458458
end
459+
out[:metricValue] = event[:metricValue] if event.has_key?(:metricValue)
459460
out
460461
when "index"
461462
{
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
2+
module LaunchDarkly
3+
module Impl
4+
# Event constructors are centralized here to avoid mistakes and repetitive logic.
5+
# The LDClient owns two instances of EventFactory: one that always embeds evaluation reasons
6+
# in the events (for when variation_detail is called) and one that doesn't.
7+
#
8+
# Note that these methods do not set the "creationDate" property, because in the Ruby client,
9+
# that is done by EventProcessor.add_event().
10+
class EventFactory
11+
def initialize(with_reasons)
12+
@with_reasons = with_reasons
13+
end
14+
15+
def new_eval_event(flag, user, detail, default_value, prereq_of_flag = nil)
16+
add_experiment_data = is_experiment(flag, detail.reason)
17+
e = {
18+
kind: 'feature',
19+
key: flag[:key],
20+
user: user,
21+
variation: detail.variation_index,
22+
value: detail.value,
23+
default: default_value,
24+
version: flag[:version]
25+
}
26+
# the following properties are handled separately so we don't waste bandwidth on unused keys
27+
e[:trackEvents] = true if add_experiment_data || flag[:trackEvents]
28+
e[:debugEventsUntilDate] = flag[:debugEventsUntilDate] if flag[:debugEventsUntilDate]
29+
e[:prereqOf] = prereq_of_flag[:key] if !prereq_of_flag.nil?
30+
e[:reason] = detail.reason if add_experiment_data || @with_reasons
31+
e
32+
end
33+
34+
def new_default_event(flag, user, default_value, reason)
35+
e = {
36+
kind: 'feature',
37+
key: flag[:key],
38+
user: user,
39+
value: default_value,
40+
default: default_value,
41+
version: flag[:version]
42+
}
43+
e[:trackEvents] = true if flag[:trackEvents]
44+
e[:debugEventsUntilDate] = flag[:debugEventsUntilDate] if flag[:debugEventsUntilDate]
45+
e[:reason] = reason if @with_reasons
46+
e
47+
end
48+
49+
def new_unknown_flag_event(key, user, default_value, reason)
50+
e = {
51+
kind: 'feature',
52+
key: key,
53+
user: user,
54+
value: default_value,
55+
default: default_value
56+
}
57+
e[:reason] = reason if @with_reasons
58+
e
59+
end
60+
61+
def new_identify_event(user)
62+
{
63+
kind: 'identify',
64+
key: user[:key],
65+
user: user
66+
}
67+
end
68+
69+
def new_custom_event(event_name, user, data, metric_value)
70+
e = {
71+
kind: 'custom',
72+
key: event_name,
73+
user: user
74+
}
75+
e[:data] = data if !data.nil?
76+
e[:metricValue] = metric_value if !metric_value.nil?
77+
e
78+
end
79+
80+
private
81+
82+
def is_experiment(flag, reason)
83+
return false if !reason
84+
case reason[:kind]
85+
when 'RULE_MATCH'
86+
index = reason[:ruleIndex]
87+
if !index.nil?
88+
rules = flag[:rules] || []
89+
return index >= 0 && index < rules.length && rules[index][:trackEvents]
90+
end
91+
when 'FALLTHROUGH'
92+
return !!flag[:trackEventsFallthrough]
93+
end
94+
false
95+
end
96+
end
97+
end
98+
end

lib/ldclient-rb/ldclient.rb

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
require "ldclient-rb/impl/event_factory"
12
require "ldclient-rb/impl/store_client_wrapper"
23
require "concurrent/atomics"
34
require "digest/sha1"
@@ -13,6 +14,7 @@ module LaunchDarkly
1314
#
1415
class LDClient
1516
include Evaluation
17+
include Impl
1618
#
1719
# Creates a new client instance that connects to LaunchDarkly. A custom
1820
# configuration parameter can also supplied to specify advanced options,
@@ -32,6 +34,9 @@ class LDClient
3234
def initialize(sdk_key, config = Config.default, wait_for_sec = 5)
3335
@sdk_key = sdk_key
3436

37+
@event_factory_default = EventFactory.new(false)
38+
@event_factory_with_reasons = EventFactory.new(true)
39+
3540
# We need to wrap the feature store object with a FeatureStoreClientWrapper in order to add
3641
# some necessary logic around updates. Unfortunately, we have code elsewhere that accesses
3742
# the feature store through the Config object, so we need to make a new Config that uses
@@ -165,7 +170,7 @@ def initialized?
165170
# @return the variation to show the user, or the default value if there's an an error
166171
#
167172
def variation(key, user, default)
168-
evaluate_internal(key, user, default, false).value
173+
evaluate_internal(key, user, default, @event_factory_default).value
169174
end
170175

171176
#
@@ -192,7 +197,7 @@ def variation(key, user, default)
192197
# @return [EvaluationDetail] an object describing the result
193198
#
194199
def variation_detail(key, user, default)
195-
evaluate_internal(key, user, default, true)
200+
evaluate_internal(key, user, default, @event_factory_with_reasons)
196201
end
197202

198203
#
@@ -215,7 +220,8 @@ def identify(user)
215220
@config.logger.warn("Identify called with nil user or nil user key!")
216221
return
217222
end
218-
@event_processor.add_event(kind: "identify", key: user[:key], user: user)
223+
sanitize_user(user)
224+
@event_processor.add_event(@event_factory_default.new_identify_event(user))
219225
end
220226

221227
#
@@ -225,18 +231,28 @@ def identify(user)
225231
# Note that event delivery is asynchronous, so the event may not actually be sent
226232
# until later; see {#flush}.
227233
#
234+
# As of this version’s release date, the LaunchDarkly service does not support the `metricValue`
235+
# parameter. As a result, specifying `metricValue` will not yet produce any different behavior
236+
# from omitting it. Refer to the [SDK reference guide](https://docs.launchdarkly.com/docs/ruby-sdk-reference#section-track)
237+
# for the latest status.
238+
#
228239
# @param event_name [String] The name of the event
229240
# @param user [Hash] The user to register; this can have all the same user properties
230241
# described in {#variation}
231-
# @param data [Hash] A hash containing any additional data associated with the event
242+
# @param data [Hash] An optional hash containing any additional data associated with the event
243+
# @param metric_value [Number] A numeric value used by the LaunchDarkly experimentation
244+
# feature in numeric custom metrics. Can be omitted if this event is used by only
245+
# non-numeric metrics. This field will also be returned as part of the custom event
246+
# for Data Export.
232247
# @return [void]
233248
#
234-
def track(event_name, user, data)
249+
def track(event_name, user, data = nil, metric_value = nil)
235250
if !user || user[:key].nil?
236251
@config.logger.warn("Track called with nil user or nil user key!")
237252
return
238253
end
239-
@event_processor.add_event(kind: "custom", key: event_name, user: user, data: data)
254+
sanitize_user(user)
255+
@event_processor.add_event(@event_factory_default.new_custom_event(event_name, user, data, metric_value))
240256
end
241257

242258
#
@@ -294,7 +310,7 @@ def all_flags_state(user, options={})
294310
next
295311
end
296312
begin
297-
result = evaluate(f, user, @store, @config.logger)
313+
result = evaluate(f, user, @store, @config.logger, @event_factory_default)
298314
state.add_flag(f, result.detail.value, result.detail.variation_index, with_reasons ? result.detail.reason : nil,
299315
details_only_if_tracked)
300316
rescue => exn
@@ -334,7 +350,7 @@ def create_default_data_source(sdk_key, config)
334350
end
335351

336352
# @return [EvaluationDetail]
337-
def evaluate_internal(key, user, default, include_reasons_in_events)
353+
def evaluate_internal(key, user, default, event_factory)
338354
if @config.offline?
339355
return error_result('CLIENT_NOT_READY', default)
340356
end
@@ -344,8 +360,9 @@ def evaluate_internal(key, user, default, include_reasons_in_events)
344360
@config.logger.warn { "[LDClient] Client has not finished initializing; using last known values from feature store" }
345361
else
346362
@config.logger.error { "[LDClient] Client has not finished initializing; feature store unavailable, returning default value" }
347-
@event_processor.add_event(kind: "feature", key: key, value: default, default: default, user: user)
348-
return error_result('CLIENT_NOT_READY', default)
363+
detail = error_result('CLIENT_NOT_READY', default)
364+
@event_processor.add_event(event_factory.new_unknown_flag_event(key, user, default, detail.reason))
365+
return detail
349366
end
350367
end
351368

@@ -354,20 +371,19 @@ def evaluate_internal(key, user, default, include_reasons_in_events)
354371
if feature.nil?
355372
@config.logger.info { "[LDClient] Unknown feature flag \"#{key}\". Returning default value" }
356373
detail = error_result('FLAG_NOT_FOUND', default)
357-
@event_processor.add_event(kind: "feature", key: key, value: default, default: default, user: user,
358-
reason: include_reasons_in_events ? detail.reason : nil)
374+
@event_processor.add_event(event_factory.new_unknown_flag_event(key, user, default, detail.reason))
359375
return detail
360376
end
361377

362378
unless user
363379
@config.logger.error { "[LDClient] Must specify user" }
364380
detail = error_result('USER_NOT_SPECIFIED', default)
365-
@event_processor.add_event(make_feature_event(feature, nil, detail, default, include_reasons_in_events))
381+
@event_processor.add_event(event_factory.new_default_event(feature, user, default, detail.reason))
366382
return detail
367383
end
368384

369385
begin
370-
res = evaluate(feature, user, @store, @config.logger) # note, evaluate will do its own sanitization
386+
res = evaluate(feature, user, @store, @config.logger, event_factory)
371387
if !res.events.nil?
372388
res.events.each do |event|
373389
@event_processor.add_event(event)
@@ -377,29 +393,20 @@ def evaluate_internal(key, user, default, include_reasons_in_events)
377393
if detail.default_value?
378394
detail = EvaluationDetail.new(default, nil, detail.reason)
379395
end
380-
@event_processor.add_event(make_feature_event(feature, user, detail, default, include_reasons_in_events))
396+
@event_processor.add_event(event_factory.new_eval_event(feature, user, detail, default))
381397
return detail
382398
rescue => exn
383399
Util.log_exception(@config.logger, "Error evaluating feature flag \"#{key}\"", exn)
384400
detail = error_result('EXCEPTION', default)
385-
@event_processor.add_event(make_feature_event(feature, user, detail, default, include_reasons_in_events))
401+
@event_processor.add_event(event_factory.new_default_event(feature, user, default, detail.reason))
386402
return detail
387403
end
388404
end
389405

390-
def make_feature_event(flag, user, detail, default, with_reasons)
391-
{
392-
kind: "feature",
393-
key: flag[:key],
394-
user: user,
395-
variation: detail.variation_index,
396-
value: detail.value,
397-
default: default,
398-
version: flag[:version],
399-
trackEvents: flag[:trackEvents],
400-
debugEventsUntilDate: flag[:debugEventsUntilDate],
401-
reason: with_reasons ? detail.reason : nil
402-
}
406+
def sanitize_user(user)
407+
if user[:key]
408+
user[:key] = user[:key].to_s
409+
end
403410
end
404411
end
405412

0 commit comments

Comments
 (0)