From 554f9652f64fe52e2c809514490dd211c0699a27 Mon Sep 17 00:00:00 2001 From: Louis Chan <91093020+louis-launchdarkly@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:01:07 -0700 Subject: [PATCH] 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 --- contract-tests/client_entity.rb | 1 + contract-tests/service.rb | 1 + lib/ldclient-rb/config.rb | 11 +++ lib/ldclient-rb/context.rb | 20 +++++ lib/ldclient-rb/events.rb | 23 +++-- spec/config_spec.rb | 8 ++ spec/events_spec.rb | 151 ++++++++++++++++++++++++++++++-- 7 files changed, 203 insertions(+), 12 deletions(-) diff --git a/contract-tests/client_entity.rb b/contract-tests/client_entity.rb index faab8c9d..9c840912 100644 --- a/contract-tests/client_entity.rb +++ b/contract-tests/client_entity.rb @@ -35,6 +35,7 @@ def initialize(log, config) opts[:all_attributes_private] = !!events[:allAttributesPrivate] opts[:private_attributes] = events[:globalPrivateAttributes] opts[:flush_interval] = (events[:flushIntervalMs] / 1_000) unless events[:flushIntervalMs].nil? + opts[:omit_anonymous_contexts] = !!events[:omitAnonymousContexts] else opts[:send_events] = false end diff --git a/contract-tests/service.rb b/contract-tests/service.rb index 5f252970..954715ee 100644 --- a/contract-tests/service.rb +++ b/contract-tests/service.rb @@ -40,6 +40,7 @@ 'inline-context', 'anonymous-redaction', 'evaluation-hooks', + 'omit-anonymous-contexts', ], }.to_json end diff --git a/lib/ldclient-rb/config.rb b/lib/ldclient-rb/config.rb index 09347e20..9de83df0 100644 --- a/lib/ldclient-rb/config.rb +++ b/lib/ldclient-rb/config.rb @@ -43,6 +43,7 @@ class Config # @option opts [BigSegmentsConfig] :big_segments See {#big_segments}. # @option opts [Hash] :application See {#application} # @option opts [String] :payload_filter_key See {#payload_filter_key} + # @option opts [Boolean] :omit_anonymous_contexts See {#omit_anonymous_contexts} # @option hooks [Array "summary") + ) + + summary = output.detect { |e| e[:kind] == "summary" } + expect(summary[:features][:flagkey][:contextKinds]).to contain_exactly("org") + end + end + + it "ignore single-kind anonymous context if omit_anonymous_contexts" do + with_processor_and_sender(omit_anonymous_contexts_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(omit_anonymous_contexts_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 if omit_anonymous_contexts" do + with_processor_and_sender(omit_anonymous_contexts_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(omit_anonymous_contexts_config, context)), + eq(feature_event(omit_anonymous_contexts_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 if omit_anonymous_contexts" do + with_processor_and_sender(omit_anonymous_contexts_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(omit_anonymous_contexts_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 if omit_anonymous_contexts" do + with_processor_and_sender(omit_anonymous_contexts_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(omit_anonymous_contexts_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 @@ -274,7 +411,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) @@ -721,7 +858,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