Skip to content

Alda/1.5.0 #84

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 5 commits into from
Dec 13, 2017
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
6 changes: 6 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 1.5.0
December 13, 2017

* Implemented IP anonymization.
* Implemented bucketing IDs.
* Implemented Notification Listeners.
-------------------------------------------------------------------------------
## 1.4.0
October 3, 2017
Expand Down
12 changes: 12 additions & 0 deletions lib/optimizely.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
require_relative 'optimizely/helpers/validator'
require_relative 'optimizely/helpers/variable_type'
require_relative 'optimizely/logger'
require_relative 'optimizely/notification_center'
require_relative 'optimizely/project_config'

module Optimizely
Expand All @@ -38,6 +39,7 @@ class Project
attr_reader :event_builder
attr_reader :event_dispatcher
attr_reader :logger
attr_reader :notification_center

def initialize(datafile, event_dispatcher = nil, logger = nil, error_handler = nil, skip_json_validation = false, user_profile_service = nil)
# Constructor for Projects.
Expand Down Expand Up @@ -83,6 +85,7 @@ def initialize(datafile, event_dispatcher = nil, logger = nil, error_handler = n

@decision_service = DecisionService.new(@config, @user_profile_service)
@event_builder = EventBuilder.new(@config)
@notification_center = NotificationCenter.new(@logger, @error_handler)
end

def activate(experiment_key, user_id, attributes = nil)
Expand Down Expand Up @@ -231,6 +234,10 @@ def track(event_key, user_id, attributes = nil, event_tags = nil)
rescue => e
@logger.log(Logger::ERROR, "Unable to dispatch conversion event. Error: #{e}")
end
@notification_center.send_notifications(
NotificationCenter::NOTIFICATION_TYPES[:TRACK],
event_key, user_id, attributes, event_tags, conversion_event
)
end

def is_feature_enabled(feature_flag_key, user_id, attributes = nil)
Expand Down Expand Up @@ -512,6 +519,11 @@ def send_impression(experiment, variation_key, user_id, attributes = nil)
rescue => e
@logger.log(Logger::ERROR, "Unable to dispatch impression event. Error: #{e}")
end
variation = @config.get_variation_from_id(experiment_key, variation_id)
@notification_center.send_notifications(
NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE],
experiment,user_id, attributes, variation, impression_event
)
end
end
end
34 changes: 18 additions & 16 deletions lib/optimizely/bucketer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ module Optimizely
class Bucketer
# Optimizely bucketing algorithm that evenly distributes visitors.

BUCKETING_ID_TEMPLATE = '%{user_id}%{entity_id}'
BUCKETING_ID_TEMPLATE = '%{bucketing_id}%{entity_id}'
HASH_SEED = 1
MAX_HASH_VALUE = 2**32
MAX_TRAFFIC_VALUE = 10_000
Expand All @@ -35,13 +35,15 @@ def initialize(config)
@config = config
end

def bucket(experiment, user_id)
def bucket(experiment, bucketing_id, user_id)
# Determines ID of variation to be shown for a given experiment key and user ID.
#
# experiment - Experiment for which visitor is to be bucketed.
# bucketing_id - String A customer-assigned value used to generate the bucketing key
# user_id - String ID for user.
#
# Returns variation in which visitor with ID user_id has been placed. Nil if no variation.
return nil if experiment.nil?

# check if experiment is in a group; if so, check if user is bucketed into specified experiment
experiment_id = experiment['id']
Expand All @@ -51,7 +53,7 @@ def bucket(experiment, user_id)
group = @config.group_key_map.fetch(group_id)
if Helpers::Group.random_policy?(group)
traffic_allocations = group.fetch('trafficAllocation')
bucketed_experiment_id = find_bucket(user_id, group_id, traffic_allocations)
bucketed_experiment_id = find_bucket(bucketing_id, user_id, group_id, traffic_allocations)
# return if the user is not bucketed into any experiment
unless bucketed_experiment_id
@config.logger.log(Logger::INFO, "User '#{user_id}' is in no experiment.")
Expand All @@ -76,7 +78,7 @@ def bucket(experiment, user_id)
end

traffic_allocations = experiment['trafficAllocation']
variation_id = find_bucket(user_id, experiment_id, traffic_allocations)
variation_id = find_bucket(bucketing_id, user_id, experiment_id, traffic_allocations)
if variation_id && variation_id != ''
variation = @config.get_variation_from_id(experiment_key, variation_id)
variation_key = variation ? variation['key'] : nil
Expand All @@ -96,18 +98,18 @@ def bucket(experiment, user_id)
nil
end

def find_bucket(user_id, parent_id, traffic_allocations)
def find_bucket(bucketing_id, user_id, parent_id, traffic_allocations)
# Helper function to find the matching entity ID for a given bucketing value in a list of traffic allocations.
#
# bucketing_id - String A customer-assigned value user to generate bucketing key
# user_id - String ID for user
# parent_id - String entity ID to use for bucketing ID
# traffic_allocations - Array of traffic allocations
#
# Returns entity ID corresponding to the provided bucket value or nil if no match is found.

bucketing_id = sprintf(BUCKETING_ID_TEMPLATE, user_id: user_id, entity_id: parent_id)
bucket_value = generate_bucket_value(bucketing_id)
@config.logger.log(Logger::DEBUG, "Assigned bucket #{bucket_value} to user '#{user_id}'.")
bucketing_key = sprintf(BUCKETING_ID_TEMPLATE, bucketing_id: bucketing_id, entity_id: parent_id)
bucket_value = generate_bucket_value(bucketing_key)
@config.logger.log(Logger::DEBUG, "Assigned bucket #{bucket_value} to user '#{user_id}' with bucketing ID: '#{bucketing_id}'.")

traffic_allocations.each do |traffic_allocation|
current_end_of_range = traffic_allocation['endOfRange']
Expand All @@ -122,25 +124,25 @@ def find_bucket(user_id, parent_id, traffic_allocations)

private

def generate_bucket_value(bucketing_id)
def generate_bucket_value(bucketing_key)
# Helper function to generate bucket value in half-closed interval [0, MAX_TRAFFIC_VALUE).
#
# bucketing_id - String ID for bucketing.
# bucketing_key - String - Value used to generate bucket value
#
# Returns bucket value corresponding to the provided bucketing ID.
# Returns bucket value corresponding to the provided bucketing key.

ratio = (generate_unsigned_hash_code_32_bit(bucketing_id)).to_f / MAX_HASH_VALUE
ratio = (generate_unsigned_hash_code_32_bit(bucketing_key)).to_f / MAX_HASH_VALUE
(ratio * MAX_TRAFFIC_VALUE).to_i
end

def generate_unsigned_hash_code_32_bit(bucketing_id)
def generate_unsigned_hash_code_32_bit(bucketing_key)
# Helper function to retreive hash code
#
# bucketing_id - String ID for bucketing.
# bucketing_key - String - Value used for the key of the murmur hash
#
# Returns hash code which is a 32 bit unsigned integer.

MurmurHash3::V32.str_hash(bucketing_id, @bucket_seed) & UNSIGNED_MAX_32_BIT_VALUE
MurmurHash3::V32.str_hash(bucketing_key, @bucket_seed) & UNSIGNED_MAX_32_BIT_VALUE
end
end
end
Loading