Skip to content

Commit

Permalink
feat: Do not include anonymous contexts in identify and index events
Browse files Browse the repository at this point in the history
  • Loading branch information
keelerm84 committed Jun 6, 2024
1 parent adb1d23 commit e1bc04b
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 12 deletions.
20 changes: 20 additions & 0 deletions lib/ldclient-rb/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,26 @@ def valid?
@error.nil?
end

#
# For a multi-kind context:
#
# A multi-kind context is made up of two or more single-kind contexts. This method will first discard any
# single-kind contexts which are anonymous. It will then create a new multi-kind context from the remaining
# single-kind contexts. This may result in an invalid context (e.g. all single-kind contexts are anonymous).
#
# For a single-kind context:
#
# If the context is not anonymous, this method will return the current context as is and unmodified.
#
# If the context is anonymous, this method will return an invalid context.
#
def without_anonymous_contexts
contexts = multi_kind? ? @contexts : [self]
contexts = contexts.reject { |c| c.anonymous }

LDContext.create_multi(contexts)
end

#
# Returns a hash mapping each context's kind to its key.
#
Expand Down
22 changes: 17 additions & 5 deletions lib/ldclient-rb/events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ def record_eval_event(
end

def record_identify_event(context)
post_to_inbox(LaunchDarkly::Impl::IdentifyEvent.new(timestamp, context))
filtered = context.without_anonymous_contexts
post_to_inbox(LaunchDarkly::Impl::IdentifyEvent.new(timestamp, filtered)) if filtered.valid?
end

def record_custom_event(context, key, data = nil, metric_value = nil)
Expand Down Expand Up @@ -319,16 +320,27 @@ def dispatch_event(event, outbox)
will_add_full_event = true
end

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

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

private def get_indexable_context(event, &block)
return if event.context.nil?

filtered = event.context.without_anonymous_contexts
return unless filtered.valid?

return if notice_context(filtered)
return if event.is_a?(LaunchDarkly::Impl::IdentifyEvent)
return if event.is_a?(LaunchDarkly::Impl::MigrationOpEvent)

yield filtered unless block.nil?
end

#
# Add to the set of contexts we've noticed, and return true if the context
# was already known to us.
Expand Down
124 changes: 117 additions & 7 deletions spec/events_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,123 @@ module LaunchDarkly
let(:default_config_opts) { { diagnostic_opt_out: true, logger: $null_log } }
let(:default_config) { Config.new(default_config_opts) }
let(:context) { LDContext.create({ kind: "user", key: "userkey", name: "Red" }) }
let(:anon_context) { LDContext.create({ kind: "org", key: "orgkey", name: "Organization", anonymous: true }) }

it "queues identify event" do
with_processor_and_sender(default_config, starting_timestamp) do |ep, sender|
ep.record_identify_event(context)
describe "identify events" do
it "can be queued" do
with_processor_and_sender(default_config, starting_timestamp) do |ep, sender|
ep.record_identify_event(context)

output = flush_and_get_events(ep, sender)
expect(output).to contain_exactly(eq(identify_event(default_config, context)))
output = flush_and_get_events(ep, sender)
expect(output).to contain_exactly(eq(identify_event(default_config, context)))
end
end

it "does not queue if anonymous" do
with_processor_and_sender(default_config, starting_timestamp) do |ep, sender|
ep.record_identify_event(anon_context)

output = flush_and_get_events(ep, sender)
expect(output).to be_nil
end
end

it "strips anonymous contexts from multi kind contexts" do
with_processor_and_sender(default_config, starting_timestamp) do |ep, sender|
user = LDContext.create({ kind: "user", key: "userkey", name: "Example User", anonymous: true })
org = LDContext.create({ kind: "org", key: "orgkey", name: "Big Organization" })
device = LDContext.create({ kind: "device", key: "devicekey", name: "IoT Device", anonymous: true })

ep.record_identify_event(LDContext.create_multi([user, org, device]))

output = flush_and_get_events(ep, sender)
expect(output).to contain_exactly(eq(identify_event(default_config, org)))
end
end

it "does not queue if all are anonymous" do
with_processor_and_sender(default_config, starting_timestamp) do |ep, sender|
user = LDContext.create({ kind: "user", key: "userkey", name: "Example User", anonymous: true })
org = LDContext.create({ kind: "org", key: "orgkey", name: "Big Organization", anonymous: true })
device = LDContext.create({ kind: "device", key: "devicekey", name: "IoT Device", anonymous: true })

ep.record_identify_event(LDContext.create_multi([user, org, device]))

output = flush_and_get_events(ep, sender)
expect(output).to be_nil
end
end
end

describe "index events" do
it "ignore single-kind anonymous context" do
with_processor_and_sender(default_config, starting_timestamp) do |ep, sender|
flag = { key: "flagkey", version: 11 }
ep.record_eval_event(anon_context, 'flagkey', 11, 1, 'value', nil, nil, true)

output = flush_and_get_events(ep, sender)
expect(output).to contain_exactly(
eq(feature_event(default_config, flag, anon_context, 1, 'value')),
include(:kind => "summary")
)

summary = output.detect { |e| e[:kind] == "summary" }
expect(summary[:features][:flagkey][:contextKinds]).to contain_exactly("org")
end
end

it "ignore anonymous contexts from multi-kind" do
with_processor_and_sender(default_config, starting_timestamp) do |ep, sender|
flag = { key: "flagkey", version: 11 }
multi = LDContext.create_multi([context, anon_context])
ep.record_eval_event(multi, 'flagkey', 11, 1, 'value', nil, nil, true)

output = flush_and_get_events(ep, sender)
expect(output).to contain_exactly(
eq(index_event(default_config, context)),
eq(feature_event(default_config, flag, multi, 1, 'value')),
include(:kind => "summary")
)

summary = output.detect { |e| e[:kind] == "summary" }
expect(summary[:features][:flagkey][:contextKinds]).to contain_exactly("user", "org")
end
end

it "handles mult-kind context being completely anonymous" do
with_processor_and_sender(default_config, starting_timestamp) do |ep, sender|
flag = { key: "flagkey", version: 11 }
anon_user = LDContext.create({ kind: "user", key: "userkey", name: "User name", anonymous: true })
multi = LDContext.create_multi([anon_user, anon_context])
ep.record_eval_event(multi, 'flagkey', 11, 1, 'value', nil, nil, true)

output = flush_and_get_events(ep, sender)
expect(output).to contain_exactly(
eq(feature_event(default_config, flag, multi, 1, 'value')),
include(:kind => "summary")
)

summary = output.detect { |e| e[:kind] == "summary" }
expect(summary[:features][:flagkey][:contextKinds]).to contain_exactly("user", "org")
end
end

it "anonymous context does not prevent subsequent index events" do
with_processor_and_sender(default_config, starting_timestamp) do |ep, sender|
flag = { key: "flagkey", version: 11 }
ep.record_eval_event(anon_context, 'flagkey', 11, 1, 'value', nil, nil, false)
non_anon_context = LDContext.create({ kind: "org", key: "orgkey", name: "Organization", anonymous: false })
ep.record_eval_event(non_anon_context, 'flagkey', 11, 1, 'value', nil, nil, false)

output = flush_and_get_events(ep, sender)
expect(output).to contain_exactly(
eq(index_event(default_config, non_anon_context, starting_timestamp + 1)),
include(:kind => "summary")
)

summary = output.detect { |e| e[:kind] == "summary" }
expect(summary[:features][:flagkey][:contextKinds]).to contain_exactly("org")
end
end
end

Expand Down Expand Up @@ -274,7 +384,7 @@ module LaunchDarkly

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

output = flush_and_get_events(ep, sender)
Expand Down Expand Up @@ -721,7 +831,7 @@ def custom_event(context, key, data, metric_value)
def flush_and_get_events(ep, sender)
ep.flush
ep.wait_until_inactive
sender.analytics_payloads.pop
sender.analytics_payloads.pop unless sender.analytics_payloads.empty?
end
end
end

0 comments on commit e1bc04b

Please sign in to comment.