Skip to content

Commit 554f965

Browse files
feat: adding option to omit anonymous contexts in identify and index events (#287)
From investigation, LaunchDarkly thinks we don't need these events to support any existing LaunchDarkly features. However, customers may depend on this behavior for data export, so instead of fully remove the identify and index events for anonymous context, we added a new option to the SDK to control this behavior. --------- Co-authored-by: Matthew Keeler <[email protected]>
1 parent 9f6c902 commit 554f965

File tree

7 files changed

+203
-12
lines changed

7 files changed

+203
-12
lines changed

contract-tests/client_entity.rb

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def initialize(log, config)
3535
opts[:all_attributes_private] = !!events[:allAttributesPrivate]
3636
opts[:private_attributes] = events[:globalPrivateAttributes]
3737
opts[:flush_interval] = (events[:flushIntervalMs] / 1_000) unless events[:flushIntervalMs].nil?
38+
opts[:omit_anonymous_contexts] = !!events[:omitAnonymousContexts]
3839
else
3940
opts[:send_events] = false
4041
end

contract-tests/service.rb

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
'inline-context',
4141
'anonymous-redaction',
4242
'evaluation-hooks',
43+
'omit-anonymous-contexts',
4344
],
4445
}.to_json
4546
end

lib/ldclient-rb/config.rb

+11
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class Config
4343
# @option opts [BigSegmentsConfig] :big_segments See {#big_segments}.
4444
# @option opts [Hash] :application See {#application}
4545
# @option opts [String] :payload_filter_key See {#payload_filter_key}
46+
# @option opts [Boolean] :omit_anonymous_contexts See {#omit_anonymous_contexts}
4647
# @option hooks [Array<Interfaces::Hooks::Hook]
4748
#
4849
def initialize(opts = {})
@@ -77,6 +78,7 @@ def initialize(opts = {})
7778
@application = LaunchDarkly::Impl::Util.validate_application_info(opts[:application] || {}, @logger)
7879
@payload_filter_key = opts[:payload_filter_key]
7980
@hooks = (opts[:hooks] || []).keep_if { |hook| hook.is_a? Interfaces::Hooks::Hook }
81+
@omit_anonymous_contexts = opts.has_key?(:omit_anonymous_contexts) && opts[:omit_anonymous_contexts]
8082
@data_source_update_sink = nil
8183
end
8284

@@ -385,6 +387,15 @@ def diagnostic_opt_out?
385387
#
386388
attr_reader :hooks
387389

390+
#
391+
# Sets whether anonymous contexts should be omitted from index and identify events.
392+
#
393+
# The default value is false. Anonymous contexts will be included in index and identify events.
394+
# @return [Boolean]
395+
#
396+
attr_reader :omit_anonymous_contexts
397+
398+
388399
#
389400
# The default LaunchDarkly client configuration. This configuration sets
390401
# reasonable defaults for most users.

lib/ldclient-rb/context.rb

+20
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,26 @@ def valid?
101101
@error.nil?
102102
end
103103

104+
#
105+
# For a multi-kind context:
106+
#
107+
# A multi-kind context is made up of two or more single-kind contexts. This method will first discard any
108+
# single-kind contexts which are anonymous. It will then create a new multi-kind context from the remaining
109+
# single-kind contexts. This may result in an invalid context (e.g. all single-kind contexts are anonymous).
110+
#
111+
# For a single-kind context:
112+
#
113+
# If the context is not anonymous, this method will return the current context as is and unmodified.
114+
#
115+
# If the context is anonymous, this method will return an invalid context.
116+
#
117+
def without_anonymous_contexts
118+
contexts = multi_kind? ? @contexts : [self]
119+
contexts = contexts.reject { |c| c.anonymous }
120+
121+
LDContext.create_multi(contexts)
122+
end
123+
104124
#
105125
# Returns a hash mapping each context's kind to its key.
106126
#

lib/ldclient-rb/events.rb

+18-5
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ def initialize(sdk_key, config, client = nil, diagnostic_accumulator = nil, test
144144
Impl::EventSender.new(sdk_key, config, client || Util.new_http_client(config.events_uri, config))
145145

146146
@timestamp_fn = (test_properties || {})[:timestamp_fn] || proc { Impl::Util.current_time_millis }
147+
@omit_anonymous_contexts = config.omit_anonymous_contexts
147148

148149
EventDispatcher.new(@inbox, sdk_key, config, diagnostic_accumulator, event_sender)
149150
end
@@ -167,7 +168,8 @@ def record_eval_event(
167168
end
168169

169170
def record_identify_event(context)
170-
post_to_inbox(LaunchDarkly::Impl::IdentifyEvent.new(timestamp, context))
171+
target_context = !@omit_anonymous_contexts ? context : context.without_anonymous_contexts
172+
post_to_inbox(LaunchDarkly::Impl::IdentifyEvent.new(timestamp, target_context)) if target_context.valid?
171173
end
172174

173175
def record_custom_event(context, key, data = nil, metric_value = nil)
@@ -319,16 +321,27 @@ def dispatch_event(event, outbox)
319321
will_add_full_event = true
320322
end
321323

322-
# For each context we haven't seen before, we add an index event - unless this is already
323-
# an identify event for that context.
324-
if !event.context.nil? && !notice_context(event.context) && !event.is_a?(LaunchDarkly::Impl::IdentifyEvent) && !event.is_a?(LaunchDarkly::Impl::MigrationOpEvent)
325-
outbox.add_event(LaunchDarkly::Impl::IndexEvent.new(event.timestamp, event.context))
324+
get_indexable_context(event) do |ctx|
325+
outbox.add_event(LaunchDarkly::Impl::IndexEvent.new(event.timestamp, ctx))
326326
end
327327

328328
outbox.add_event(event) if will_add_full_event && @sampler.sample(event.sampling_ratio.nil? ? 1 : event.sampling_ratio)
329329
outbox.add_event(debug_event) if !debug_event.nil? && @sampler.sample(event.sampling_ratio.nil? ? 1 : event.sampling_ratio)
330330
end
331331

332+
private def get_indexable_context(event, &block)
333+
return if event.context.nil?
334+
335+
context = !@config.omit_anonymous_contexts ? event.context : event.context.without_anonymous_contexts
336+
return unless context.valid?
337+
338+
return if notice_context(context)
339+
return if event.is_a?(LaunchDarkly::Impl::IdentifyEvent)
340+
return if event.is_a?(LaunchDarkly::Impl::MigrationOpEvent)
341+
342+
yield context unless block.nil?
343+
end
344+
332345
#
333346
# Add to the set of contexts we've noticed, and return true if the context
334347
# was already known to us.

spec/config_spec.rb

+8
Original file line numberDiff line numberDiff line change
@@ -99,5 +99,13 @@ module LaunchDarkly
9999
end
100100
end
101101
end
102+
describe ".omit_anonymous_contexts" do
103+
it "defaults to false" do
104+
expect(subject.new.omit_anonymous_contexts).to eq false
105+
end
106+
it "can be set to true" do
107+
expect(subject.new(omit_anonymous_contexts: true).omit_anonymous_contexts).to eq true
108+
end
109+
end
102110
end
103111
end

spec/events_spec.rb

+144-7
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,151 @@ module LaunchDarkly
1212
let(:starting_timestamp) { 1000 }
1313
let(:default_config_opts) { { diagnostic_opt_out: true, logger: $null_log } }
1414
let(:default_config) { Config.new(default_config_opts) }
15+
let(:omit_anonymous_contexts_config) { Config.new(default_config_opts.merge(omit_anonymous_contexts: true))}
1516
let(:context) { LDContext.create({ kind: "user", key: "userkey", name: "Red" }) }
17+
let(:anon_context) { LDContext.create({ kind: "org", key: "orgkey", name: "Organization", anonymous: true }) }
1618

17-
it "queues identify event" do
18-
with_processor_and_sender(default_config, starting_timestamp) do |ep, sender|
19-
ep.record_identify_event(context)
19+
describe "identify events" do
20+
it "can be queued" do
21+
with_processor_and_sender(default_config, starting_timestamp) do |ep, sender|
22+
ep.record_identify_event(context)
2023

21-
output = flush_and_get_events(ep, sender)
22-
expect(output).to contain_exactly(eq(identify_event(default_config, context)))
24+
output = flush_and_get_events(ep, sender)
25+
expect(output).to contain_exactly(eq(identify_event(default_config, context)))
26+
end
27+
end
28+
29+
it "does queue if anonymous" do
30+
with_processor_and_sender(default_config, starting_timestamp) do |ep, sender|
31+
ep.record_identify_event(anon_context)
32+
33+
output = flush_and_get_events(ep, sender)
34+
expect(output).to contain_exactly(eq(identify_event(default_config, anon_context)))
35+
end
36+
end
37+
38+
it "does not queue if anonymous and omit_anonymous_contexts" do
39+
with_processor_and_sender(omit_anonymous_contexts_config, starting_timestamp) do |ep, sender|
40+
ep.record_identify_event(anon_context)
41+
42+
output = flush_and_get_events(ep, sender)
43+
expect(output).to be_nil
44+
end
45+
end
46+
47+
it "strips anonymous contexts from multi kind contexts if omit_anonymous_contexts" do
48+
with_processor_and_sender(omit_anonymous_contexts_config, starting_timestamp) do |ep, sender|
49+
user = LDContext.create({ kind: "user", key: "userkey", name: "Example User", anonymous: true })
50+
org = LDContext.create({ kind: "org", key: "orgkey", name: "Big Organization" })
51+
device = LDContext.create({ kind: "device", key: "devicekey", name: "IoT Device", anonymous: true })
52+
53+
ep.record_identify_event(LDContext.create_multi([user, org, device]))
54+
55+
output = flush_and_get_events(ep, sender)
56+
expect(output).to contain_exactly(eq(identify_event(omit_anonymous_contexts_config, org)))
57+
end
58+
end
59+
60+
it "does not queue if all are anonymous and omit_anonymous_contexts" do
61+
with_processor_and_sender(omit_anonymous_contexts_config, starting_timestamp) do |ep, sender|
62+
user = LDContext.create({ kind: "user", key: "userkey", name: "Example User", anonymous: true })
63+
org = LDContext.create({ kind: "org", key: "orgkey", name: "Big Organization", anonymous: true })
64+
device = LDContext.create({ kind: "device", key: "devicekey", name: "IoT Device", anonymous: true })
65+
66+
ep.record_identify_event(LDContext.create_multi([user, org, device]))
67+
68+
output = flush_and_get_events(ep, sender)
69+
expect(output).to be_nil
70+
end
71+
end
72+
end
73+
74+
describe "index events" do
75+
it "does not ignore single-kind anonymous context" do
76+
with_processor_and_sender(default_config, starting_timestamp) do |ep, sender|
77+
flag = { key: "flagkey", version: 11 }
78+
ep.record_eval_event(anon_context, 'flagkey', 11, 1, 'value', nil, nil, true)
79+
80+
output = flush_and_get_events(ep, sender)
81+
expect(output).to contain_exactly(
82+
eq(index_event(default_config, anon_context)),
83+
eq(feature_event(default_config, flag, anon_context, 1, 'value')),
84+
include(:kind => "summary")
85+
)
86+
87+
summary = output.detect { |e| e[:kind] == "summary" }
88+
expect(summary[:features][:flagkey][:contextKinds]).to contain_exactly("org")
89+
end
90+
end
91+
92+
it "ignore single-kind anonymous context if omit_anonymous_contexts" do
93+
with_processor_and_sender(omit_anonymous_contexts_config, starting_timestamp) do |ep, sender|
94+
flag = { key: "flagkey", version: 11 }
95+
ep.record_eval_event(anon_context, 'flagkey', 11, 1, 'value', nil, nil, true)
96+
97+
output = flush_and_get_events(ep, sender)
98+
expect(output).to contain_exactly(
99+
eq(feature_event(omit_anonymous_contexts_config, flag, anon_context, 1, 'value')),
100+
include(:kind => "summary")
101+
)
102+
103+
summary = output.detect { |e| e[:kind] == "summary" }
104+
expect(summary[:features][:flagkey][:contextKinds]).to contain_exactly("org")
105+
end
106+
end
107+
108+
it "ignore anonymous contexts from multi-kind if omit_anonymous_contexts" do
109+
with_processor_and_sender(omit_anonymous_contexts_config, starting_timestamp) do |ep, sender|
110+
flag = { key: "flagkey", version: 11 }
111+
multi = LDContext.create_multi([context, anon_context])
112+
ep.record_eval_event(multi, 'flagkey', 11, 1, 'value', nil, nil, true)
113+
114+
output = flush_and_get_events(ep, sender)
115+
expect(output).to contain_exactly(
116+
eq(index_event(omit_anonymous_contexts_config, context)),
117+
eq(feature_event(omit_anonymous_contexts_config, flag, multi, 1, 'value')),
118+
include(:kind => "summary")
119+
)
120+
121+
summary = output.detect { |e| e[:kind] == "summary" }
122+
expect(summary[:features][:flagkey][:contextKinds]).to contain_exactly("user", "org")
123+
end
124+
end
125+
126+
it "handles mult-kind context being completely anonymous if omit_anonymous_contexts" do
127+
with_processor_and_sender(omit_anonymous_contexts_config, starting_timestamp) do |ep, sender|
128+
flag = { key: "flagkey", version: 11 }
129+
anon_user = LDContext.create({ kind: "user", key: "userkey", name: "User name", anonymous: true })
130+
multi = LDContext.create_multi([anon_user, anon_context])
131+
ep.record_eval_event(multi, 'flagkey', 11, 1, 'value', nil, nil, true)
132+
133+
output = flush_and_get_events(ep, sender)
134+
expect(output).to contain_exactly(
135+
eq(feature_event(omit_anonymous_contexts_config, flag, multi, 1, 'value')),
136+
include(:kind => "summary")
137+
)
138+
139+
summary = output.detect { |e| e[:kind] == "summary" }
140+
expect(summary[:features][:flagkey][:contextKinds]).to contain_exactly("user", "org")
141+
end
142+
end
143+
144+
it "anonymous context does not prevent subsequent index events if omit_anonymous_contexts" do
145+
with_processor_and_sender(omit_anonymous_contexts_config, starting_timestamp) do |ep, sender|
146+
flag = { key: "flagkey", version: 11 }
147+
ep.record_eval_event(anon_context, 'flagkey', 11, 1, 'value', nil, nil, false)
148+
non_anon_context = LDContext.create({ kind: "org", key: "orgkey", name: "Organization", anonymous: false })
149+
ep.record_eval_event(non_anon_context, 'flagkey', 11, 1, 'value', nil, nil, false)
150+
151+
output = flush_and_get_events(ep, sender)
152+
expect(output).to contain_exactly(
153+
eq(index_event(omit_anonymous_contexts_config, non_anon_context, starting_timestamp + 1)),
154+
include(:kind => "summary")
155+
)
156+
157+
summary = output.detect { |e| e[:kind] == "summary" }
158+
expect(summary[:features][:flagkey][:contextKinds]).to contain_exactly("org")
159+
end
23160
end
24161
end
25162

@@ -274,7 +411,7 @@ module LaunchDarkly
274411

275412
it "treats nil value for custom the same as an empty hash" do
276413
with_processor_and_sender(default_config, starting_timestamp) do |ep, sender|
277-
user_with_nil_custom = LDContext.create({ key: "userkey", custom: nil })
414+
user_with_nil_custom = LDContext.create({ key: "userkey", kind: "user", custom: nil })
278415
ep.record_identify_event(user_with_nil_custom)
279416

280417
output = flush_and_get_events(ep, sender)
@@ -721,7 +858,7 @@ def custom_event(context, key, data, metric_value)
721858
def flush_and_get_events(ep, sender)
722859
ep.flush
723860
ep.wait_until_inactive
724-
sender.analytics_payloads.pop
861+
sender.analytics_payloads.pop unless sender.analytics_payloads.empty?
725862
end
726863
end
727864
end

0 commit comments

Comments
 (0)