Skip to content

feat: send flag decisions #272

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ Gemfile.lock
/tmp/
*.DS_Store
.ruby-version
vendor/bundle
vendor/bundle
*.code-workspace
59 changes: 49 additions & 10 deletions lib/optimizely.rb
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,10 @@ def activate(experiment_key, user_id, attributes = nil)

# Create and dispatch impression event
experiment = config.get_experiment_from_key(experiment_key)
send_impression(config, experiment, variation_key, user_id, attributes)
send_impression(
config, experiment, variation_key, '', experiment_key,
Optimizely::DecisionService::DECISION_SOURCES['EXPERIMENT'], user_id, attributes
)

variation_key
end
Expand Down Expand Up @@ -316,14 +319,23 @@ def is_feature_enabled(feature_flag_key, user_id, attributes = nil)
experiment_key: decision.experiment['key'],
variation_key: variation['key']
}
# Send event if Decision came from an experiment.
send_impression(config, decision.experiment, variation['key'], user_id, attributes)
else
@logger.log(Logger::DEBUG,
"The user '#{user_id}' is not being experimented on in feature '#{feature_flag_key}'.")
# Send event if Decision came from a feature test.
send_impression(
config, decision.experiment, variation['key'], feature_flag_key, decision.experiment['key'], source_string, user_id, attributes
)
elsif decision.source == Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT'] && config.send_flag_decisions
send_impression(
config, decision.experiment, variation['key'], feature_flag_key, decision.experiment['key'], source_string, user_id, attributes
)
end
end

if decision.nil? && config.send_flag_decisions
send_impression(
config, nil, '', feature_flag_key, '', source_string, user_id, attributes
)
end

@notification_center.send_notifications(
NotificationCenter::NOTIFICATION_TYPES[:DECISION],
Helpers::Constants::DECISION_NOTIFICATION_TYPES['FEATURE'],
Expand Down Expand Up @@ -867,15 +879,42 @@ def validate_instantiation_options
raise InvalidInputError, 'event_dispatcher'
end

def send_impression(config, experiment, variation_key, user_id, attributes = nil)
def send_impression(config, experiment, variation_key, flag_key, rule_key, rule_type, user_id, attributes = nil)
if experiment.nil?
experiment = {
'id' => '',
'key' => '',
'layerId' => '',
'status' => '',
'variations' => [],
'trafficAllocation' => [],
'audienceIds' => [],
'audienceConditions' => [],
'forcedVariations' => {}
}
end

experiment_key = experiment['key']
variation_id = config.get_variation_id_from_key(experiment_key, variation_key)
user_event = UserEventFactory.create_impression_event(config, experiment, variation_id, user_id, attributes)

variation_id = ''
variation_id = config.get_variation_id_from_key(experiment_key, variation_key) if experiment_key != ''

metadata = {
flag_key: flag_key,
rule_key: rule_key,
rule_type: rule_type,
variation_key: variation_key
}

user_event = UserEventFactory.create_impression_event(config, experiment, variation_id, metadata, user_id, attributes)
@event_processor.process(user_event)
return unless @notification_center.notification_count(NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE]).positive?

@logger.log(Logger::INFO, "Activating user '#{user_id}' in experiment '#{experiment_key}'.")
variation = config.get_variation_from_id(experiment_key, variation_id)

experiment = nil if experiment_key == ''
variation = nil
variation = config.get_variation_from_id(experiment_key, variation_id) unless experiment.nil?
log_event = EventFactory.create_log_event(user_event, @logger)
@notification_center.send_notifications(
NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE],
Expand Down
2 changes: 2 additions & 0 deletions lib/optimizely/config/datafile_project_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class DatafileProjectConfig < ProjectConfig
attr_reader :revision
attr_reader :rollouts
attr_reader :version
attr_reader :send_flag_decisions

attr_reader :attribute_key_map
attr_reader :audience_id_map
Expand Down Expand Up @@ -83,6 +84,7 @@ def initialize(datafile, logger, error_handler)
@bot_filtering = config['botFiltering']
@revision = config['revision']
@rollouts = config.fetch('rollouts', [])
@send_flag_decisions = config.fetch('sendFlagDecisions', false)

# Json type is represented in datafile as a subtype of string for the sake of backwards compatibility.
# Converting it to a first-class json type while creating Project Config
Expand Down
1 change: 1 addition & 0 deletions lib/optimizely/decision_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class DecisionService
Decision = Struct.new(:experiment, :variation, :source)

DECISION_SOURCES = {
'EXPERIMENT' => 'experiment',
'FEATURE_TEST' => 'feature-test',
'ROLLOUT' => 'rollout'
}.freeze
Expand Down
10 changes: 6 additions & 4 deletions lib/optimizely/event/entity/decision.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

#
# Copyright 2019, Optimizely and contributors
# Copyright 2019-2020, Optimizely and contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -17,19 +17,21 @@
#
module Optimizely
class Decision
attr_reader :campaign_id, :experiment_id, :variation_id
attr_reader :campaign_id, :experiment_id, :variation_id, :metadata

def initialize(campaign_id:, experiment_id:, variation_id:)
def initialize(campaign_id:, experiment_id:, variation_id:, metadata:)
@campaign_id = campaign_id
@experiment_id = experiment_id
@variation_id = variation_id
@metadata = metadata
end

def as_json
{
campaign_id: @campaign_id,
experiment_id: @experiment_id,
variation_id: @variation_id
variation_id: @variation_id,
metadata: @metadata
}
end
end
Expand Down
6 changes: 4 additions & 2 deletions lib/optimizely/event/entity/impression_event.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

#
# Copyright 2019, Optimizely and contributors
# Copyright 2019-2020, Optimizely and contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -19,7 +19,7 @@
require 'optimizely/helpers/date_time_utils'
module Optimizely
class ImpressionEvent < UserEvent
attr_reader :user_id, :experiment_layer_id, :experiment_id, :variation_id,
attr_reader :user_id, :experiment_layer_id, :experiment_id, :variation_id, :metadata,
:visitor_attributes, :bot_filtering

def initialize(
Expand All @@ -28,6 +28,7 @@ def initialize(
experiment_layer_id:,
experiment_id:,
variation_id:,
metadata:,
visitor_attributes:,
bot_filtering:
)
Expand All @@ -38,6 +39,7 @@ def initialize(
@experiment_layer_id = experiment_layer_id
@experiment_id = experiment_id
@variation_id = variation_id
@metadata = metadata
@visitor_attributes = visitor_attributes
@bot_filtering = bot_filtering
end
Expand Down
7 changes: 4 additions & 3 deletions lib/optimizely/event/event_factory.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

#
# Copyright 2019, Optimizely and contributors
# Copyright 2019-2020, Optimizely and contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -101,10 +101,11 @@ def build_attribute_list(user_attributes, project_config)
private

def create_impression_event_visitor(impression_event)
decision = Optimizely::Decision.new(
decision = Decision.new(
campaign_id: impression_event.experiment_layer_id,
experiment_id: impression_event.experiment_id,
variation_id: impression_event.variation_id
variation_id: impression_event.variation_id,
metadata: impression_event.metadata
)

snapshot_event = Optimizely::SnapshotEvent.new(
Expand Down
7 changes: 4 additions & 3 deletions lib/optimizely/event/user_event_factory.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

#
# Copyright 2019, Optimizely and contributors
# Copyright 2019-2020, Optimizely and contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -22,7 +22,7 @@
module Optimizely
class UserEventFactory
# UserEventFactory builds ImpressionEvent and ConversionEvent objects from a given user_event.
def self.create_impression_event(project_config, experiment, variation_id, user_id, user_attributes)
def self.create_impression_event(project_config, experiment, variation_id, metadata, user_id, user_attributes)
# Create impression Event to be sent to the logging endpoint.
#
# project_config - Instance of ProjectConfig
Expand All @@ -42,13 +42,14 @@ def self.create_impression_event(project_config, experiment, variation_id, user_
).as_json

visitor_attributes = Optimizely::EventFactory.build_attribute_list(user_attributes, project_config)
experiment_layer_id = project_config.experiment_key_map[experiment['key']]['layerId']
experiment_layer_id = experiment['layerId']
Optimizely::ImpressionEvent.new(
event_context: event_context,
user_id: user_id,
experiment_layer_id: experiment_layer_id,
experiment_id: experiment['id'],
variation_id: variation_id,
metadata: metadata,
visitor_attributes: visitor_attributes,
bot_filtering: project_config.bot_filtering
)
Expand Down
2 changes: 2 additions & 0 deletions lib/optimizely/project_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ def bot_filtering; end

def revision; end

def send_flag_decisions; end

def rollouts; end

def experiment_running?(experiment); end
Expand Down
11 changes: 11 additions & 0 deletions spec/config/datafile_project_config_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
require 'json'
require 'spec_helper'
require 'optimizely/config/datafile_project_config'
require 'optimizely/exceptions'
Expand Down Expand Up @@ -50,6 +51,7 @@
expect(project_config.groups).to eq(config_body['groups'])
expect(project_config.project_id).to eq(config_body['projectId'])
expect(project_config.revision).to eq(config_body['revision'])
expect(project_config.send_flag_decisions).to eq(config_body['sendFlagDecisions'])

expected_attribute_key_map = {
'browser_type' => config_body['attributes'][0],
Expand Down Expand Up @@ -769,6 +771,15 @@

expect(project_config.audience_id_map).to eq(expected_audience_id_map)
end

it 'should initialize send_flag_decisions to false when not in datafile' do
config_body_without_flag_decision = config_body.dup
config_body_without_flag_decision.delete('sendFlagDecisions')
config_body_json = JSON.dump(config_body_without_flag_decision)
project_config = Optimizely::DatafileProjectConfig.new(config_body_json, logger, error_handler)

expect(project_config.send_flag_decisions).to eq(false)
end
end

describe '@logger' do
Expand Down
12 changes: 10 additions & 2 deletions spec/event/batch_event_processor_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

#
# Copyright 2019, Optimizely and contributors
# Copyright 2019-2020, Optimizely and contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -310,7 +310,15 @@
)

experiment = project_config.get_experiment_from_key('test_experiment')
impression_event = Optimizely::UserEventFactory.create_impression_event(project_config, experiment, '111128', 'test_user', nil)
metadata = {
flag_key: '',
rule_key: 'test_experiment',
rule_type: 'experiment',
variation_key: '111128'
}
impression_event = Optimizely::UserEventFactory.create_impression_event(
project_config, experiment, '111128', metadata, 'test_user', nil
)
log_event = Optimizely::EventFactory.create_log_event(impression_event, spy_logger)

allow(Optimizely::EventFactory).to receive(:create_log_event).with(any_args).and_return(log_event)
Expand Down
18 changes: 15 additions & 3 deletions spec/event/event_entities_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

#
# Copyright 2019, Optimizely and contributors
# Copyright 2019-2020, Optimizely and contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -49,7 +49,13 @@
decisions: [{
campaign_id: '7719770039',
experiment_id: '111127',
variation_id: '111128'
variation_id: '111128',
metadata: {
flag_key: '',
rule_key: 'test_experiment',
rule_type: 'experiment',
variation_key: '111128'
}
}],
events: [{
entity_id: '7719770039',
Expand Down Expand Up @@ -128,7 +134,13 @@
key: 'campaign_activated'
)

decision = Optimizely::Decision.new(campaign_id: '7719770039', experiment_id: '111127', variation_id: '111128')
metadata = {
flag_key: '',
rule_key: 'test_experiment',
rule_type: 'experiment',
variation_key: '111128'
}
decision = Optimizely::Decision.new(campaign_id: '7719770039', experiment_id: '111127', variation_id: '111128', metadata: metadata)
snapshot = Optimizely::Snapshot.new(events: [snapshot_event.as_json], decisions: [decision.as_json])
visitor = Optimizely::Visitor.new(
snapshots: [snapshot.as_json],
Expand Down
Loading