diff --git a/Gemfile.lock b/Gemfile.lock index 632f9dcf..ff4cdf63 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - launchdarkly-server-sdk (5.8.2) + launchdarkly-server-sdk (6.0.0) concurrent-ruby (~> 1.1) http (~> 4.4.1) json (~> 2.3.1) diff --git a/README.md b/README.md index ef8c0e33..2f7b01c6 100644 --- a/README.md +++ b/README.md @@ -55,4 +55,3 @@ About LaunchDarkly * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides * [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation * [blog.launchdarkly.com](https://blog.launchdarkly.com/ "LaunchDarkly Blog Documentation") for the latest product updates - * [Feature Flagging Guide](https://github.com/launchdarkly/featureflags/ "Feature Flagging Guide") for best practices and strategies diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 88296f02..cb66e704 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -45,7 +45,7 @@ jobs: workingDirectory: $(System.DefaultWorkingDirectory) script: | ruby -v - gem install bundler + gem install bundler:2.2.7 bundle install mkdir rspec bundle exec rspec --format progress --format RspecJunitFormatter -o ./rspec/rspec.xml spec diff --git a/lib/ldclient-rb/events.rb b/lib/ldclient-rb/events.rb index 2e26e1fa..c59db7d0 100644 --- a/lib/ldclient-rb/events.rb +++ b/lib/ldclient-rb/events.rb @@ -439,6 +439,7 @@ def make_output_event(event) out[:variation] = event[:variation] if event.has_key?(:variation) out[:version] = event[:version] if event.has_key?(:version) out[:prereqOf] = event[:prereqOf] if event.has_key?(:prereqOf) + out[:contextKind] = event[:contextKind] if event.has_key?(:contextKind) if @inline_users || is_debug out[:user] = process_user(event) else @@ -466,6 +467,7 @@ def make_output_event(event) out[:userKey] = event[:user].nil? ? nil : event[:user][:key] end out[:metricValue] = event[:metricValue] if event.has_key?(:metricValue) + out[:contextKind] = event[:contextKind] if event.has_key?(:contextKind) out when "index" { diff --git a/lib/ldclient-rb/impl/event_factory.rb b/lib/ldclient-rb/impl/event_factory.rb index 2e7d2697..256eea98 100644 --- a/lib/ldclient-rb/impl/event_factory.rb +++ b/lib/ldclient-rb/impl/event_factory.rb @@ -28,6 +28,7 @@ def new_eval_event(flag, user, detail, default_value, prereq_of_flag = nil) e[:debugEventsUntilDate] = flag[:debugEventsUntilDate] if flag[:debugEventsUntilDate] e[:prereqOf] = prereq_of_flag[:key] if !prereq_of_flag.nil? e[:reason] = detail.reason if add_experiment_data || @with_reasons + e[:contextKind] = context_to_context_kind(user) if !user.nil? && user[:anonymous] e end @@ -43,6 +44,7 @@ def new_default_event(flag, user, default_value, reason) e[:trackEvents] = true if flag[:trackEvents] e[:debugEventsUntilDate] = flag[:debugEventsUntilDate] if flag[:debugEventsUntilDate] e[:reason] = reason if @with_reasons + e[:contextKind] = context_to_context_kind(user) if !user.nil? && user[:anonymous] e end @@ -55,6 +57,7 @@ def new_unknown_flag_event(key, user, default_value, reason) default: default_value } e[:reason] = reason if @with_reasons + e[:contextKind] = context_to_context_kind(user) if !user.nil? && user[:anonymous] e end @@ -66,6 +69,16 @@ def new_identify_event(user) } end + def new_alias_event(current_context, previous_context) + { + kind: 'alias', + key: current_context[:key], + contextKind: context_to_context_kind(current_context), + previousKey: previous_context[:key], + previousContextKind: context_to_context_kind(previous_context) + } + end + def new_custom_event(event_name, user, data, metric_value) e = { kind: 'custom', @@ -74,11 +87,20 @@ def new_custom_event(event_name, user, data, metric_value) } e[:data] = data if !data.nil? e[:metricValue] = metric_value if !metric_value.nil? + e[:contextKind] = context_to_context_kind(user) if !user.nil? && user[:anonymous] e end private + def context_to_context_kind(user) + if !user.nil? && user[:anonymous] + return "anonymousUser" + else + return "user" + end + end + def is_experiment(flag, reason) return false if !reason case reason[:kind] diff --git a/lib/ldclient-rb/ldclient.rb b/lib/ldclient-rb/ldclient.rb index cfa63351..5d803ef3 100644 --- a/lib/ldclient-rb/ldclient.rb +++ b/lib/ldclient-rb/ldclient.rb @@ -282,6 +282,23 @@ def track(event_name, user, data = nil, metric_value = nil) @event_processor.add_event(@event_factory_default.new_custom_event(event_name, user, data, metric_value)) end + # + # Associates a new and old user object for analytics purposes via an alias event. + # + # @param current_context [Hash] The current version of a user. + # @param previous_context [Hash] The previous version of a user. + # @return [void] + # + def alias(current_context, previous_context) + if !current_context || current_context[:key].nil? || !previous_context || previous_context[:key].nil? + @config.logger.warn("Alias called with nil user or nil user key!") + return + end + sanitize_user(current_context) + sanitize_user(previous_context) + @event_processor.add_event(@event_factory_default.new_alias_event(current_context, previous_context)) + end + # # Returns all feature flag values for the given user. # diff --git a/spec/events_spec.rb b/spec/events_spec.rb index d7854567..e9a6d6ff 100644 --- a/spec/events_spec.rb +++ b/spec/events_spec.rb @@ -408,6 +408,16 @@ def with_processor_and_sender(config) end end + it "queues alias event" do + with_processor_and_sender(default_config) do |ep, sender| + e = { kind: "alias", key: "a", contextKind: "user", previousKey: "b", previousContextKind: "user" } + ep.add_event(e) + + output = flush_and_get_events(ep, sender) + expect(output).to contain_exactly(e) + end + end + it "treats nil value for custom the same as an empty hash" do with_processor_and_sender(default_config) do |ep, sender| user_with_nil_custom = { key: "userkey", custom: nil } diff --git a/spec/ldclient_spec.rb b/spec/ldclient_spec.rb index 76e5b0f7..f7d215e2 100644 --- a/spec/ldclient_spec.rb +++ b/spec/ldclient_spec.rb @@ -25,6 +25,12 @@ } } end + let(:user_anonymous) do + { + key: "anonymous@test.com", + anonymous: true + } + end let(:numeric_key_user) do { key: 33, @@ -155,6 +161,24 @@ def event_processor client.variation("key", nil, "default") end + it "queues a feature event for an existing feature when user is anonymous" do + config.feature_store.init({ LaunchDarkly::FEATURES => {} }) + config.feature_store.upsert(LaunchDarkly::FEATURES, feature_with_value) + expect(event_processor).to receive(:add_event).with(hash_including( + kind: "feature", + key: "key", + version: 100, + contextKind: "anonymousUser", + user: user_anonymous, + variation: 0, + value: "value", + default: "default", + trackEvents: true, + debugEventsUntilDate: 1000 + )) + client.variation("key", user_anonymous, "default") + end + it "queues a feature event for an existing feature when user key is nil" do config.feature_store.init({ LaunchDarkly::FEATURES => {} }) config.feature_store.upsert(LaunchDarkly::FEATURES, feature_with_value) @@ -455,6 +479,12 @@ def event_processor client.track("custom_event_name", user, nil, 1.5) end + it "includes contextKind with anonymous user" do + expect(event_processor).to receive(:add_event).with(hash_including( + kind: "custom", key: "custom_event_name", user: user_anonymous, metricValue: 2.2, contextKind: "anonymousUser")) + client.track("custom_event_name", user_anonymous, nil, 2.2) + end + it "sanitizes the user in the event" do expect(event_processor).to receive(:add_event).with(hash_including(user: sanitized_numeric_key_user)) client.track("custom_event_name", numeric_key_user, nil) @@ -473,6 +503,26 @@ def event_processor end end + describe '#alias' do + it "queues up an alias event" do + expect(event_processor).to receive(:add_event).with(hash_including( + kind: "alias", key: user[:key], contextKind: "user", previousKey: user_anonymous[:key], previousContextKind: "anonymousUser")) + client.alias(user, user_anonymous) + end + + it "does not send an event, and logs a warning, if user is nil" do + expect(event_processor).not_to receive(:add_event) + expect(logger).to receive(:warn) + client.alias(nil, nil) + end + + it "does not send an event, and logs a warning, if user key is nil" do + expect(event_processor).not_to receive(:add_event) + expect(logger).to receive(:warn) + client.alias(user_without_key, user_without_key) + end + end + describe '#identify' do it "queues up an identify event" do expect(event_processor).to receive(:add_event).with(hash_including(kind: "identify", key: user[:key], user: user))