Skip to content

feat: add odp to project and user context #316

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 22 commits into from
Oct 13, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0a7b1ee
add odp to project and user context
andrewleap-optimizely Sep 27, 2022
f47cff6
fix broken tests
andrewleap-optimizely Sep 27, 2022
ce1b1ae
add project/user_context odp tests
andrewleap-optimizely Sep 28, 2022
2bcd343
Merge branch 'master' into aleap/add_odp_to_client_and_user_context
andrewleap-optimizely Sep 28, 2022
22fee39
enable odp for legacy tests
andrewleap-optimizely Sep 29, 2022
503172a
syncronize qualified_for
andrewleap-optimizely Sep 29, 2022
9e4482b
skip segments_cache if odp disabled
andrewleap-optimizely Sep 29, 2022
e3d4621
remove odp_manager from user_context
andrewleap-optimizely Sep 29, 2022
2ff91ff
make fetch_qualified_segments non blocking
andrewleap-optimizely Oct 3, 2022
ee0934b
add sdk_settings
andrewleap-optimizely Oct 3, 2022
784eb8a
rename zaius files
andrewleap-optimizely Oct 5, 2022
19ed8da
rename zaius classes/attributes
andrewleap-optimizely Oct 6, 2022
e58f444
extract odp setup to method
andrewleap-optimizely Oct 6, 2022
de97dc3
add non blocking flag to fetch_qualified_segments
andrewleap-optimizely Oct 6, 2022
80619f9
change odp_state when config updated via setters
andrewleap-optimizely Oct 6, 2022
19dd02d
add support for sdk_settings in optimizely factory
andrewleap-optimizely Oct 6, 2022
94d75fd
remove api_key/api_host setters from odp_config
andrewleap-optimizely Oct 7, 2022
1062469
switch fetch_segments flag param to callback
andrewleap-optimizely Oct 7, 2022
d02b0e8
return status bool from sync fetch_segments
andrewleap-optimizely Oct 10, 2022
a0cd8a9
fix copyright
andrewleap-optimizely Oct 11, 2022
521ca7b
move method validation to init
andrewleap-optimizely Oct 11, 2022
e56d8b2
delay starting event_manager until odp integration
andrewleap-optimizely Oct 13, 2022
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
63 changes: 61 additions & 2 deletions lib/optimizely.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
require_relative 'optimizely/notification_center'
require_relative 'optimizely/optimizely_config'
require_relative 'optimizely/optimizely_user_context'
require_relative 'optimizely/odp/lru_cache'
require_relative 'optimizely/odp/odp_manager'

module Optimizely
class Project
Expand All @@ -46,7 +48,7 @@ class Project
attr_reader :notification_center
# @api no-doc
attr_reader :config_manager, :decision_service, :error_handler, :event_dispatcher,
:event_processor, :logger, :stopped
:event_processor, :logger, :odp_manager, :stopped

# Constructor for Projects.
#
Expand Down Expand Up @@ -74,7 +76,9 @@ def initialize( # rubocop:disable Metrics/ParameterLists
config_manager = nil,
notification_center = nil,
event_processor = nil,
default_decide_options = []
default_decide_options = [],
odp_segments_cache = nil,
sdk_settings = {}
)
@logger = logger || NoOpLogger.new
@error_handler = error_handler || NoOpErrorHandler.new
Expand All @@ -98,6 +102,41 @@ def initialize( # rubocop:disable Metrics/ParameterLists

@notification_center = notification_center.is_a?(Optimizely::NotificationCenter) ? notification_center : NotificationCenter.new(@logger, @error_handler)

unless sdk_settings.is_a? Hash
@logger.log(Logger::DEBUG, 'Provided sdk_settings is not a hash.')
sdk_settings = {}
end

odp_disabled = sdk_settings[:disable_odp] || false
unless odp_disabled
@notification_center.add_notification_listener(
NotificationCenter::NOTIFICATION_TYPES[:OPTIMIZELY_CONFIG_UPDATE],
-> { update_odp_config_on_datafile_update }
)
end

segments_cache_size = sdk_settings[:segments_cache_size]
segments_cache_timeout_in_secs = sdk_settings[:segments_cache_timeout_in_secs]

if odp_segments_cache && !Helpers::Validator.segments_cache_valid?(odp_segments_cache)
@logger.log(Logger::ERROR, 'Invalid ODP segments cache, reverting to default.')
odp_segments_cache = nil
end

if (segments_cache_size || segments_cache_timeout_in_secs) && !odp_segments_cache
odp_segments_cache = LRUCache.new(
segments_cache_size || Helpers::Constants::ODP_SEGMENTS_CACHE_CONFIG[:DEFAULT_CAPACITY],
segments_cache_timeout_in_secs || Helpers::Constants::ODP_SEGMENTS_CACHE_CONFIG[:DEFAULT_TIMEOUT_SECS]
)
end

# odp manager must be initialized before config_manager to ensure update of odp_config
@odp_manager = OdpManager.new(
disable: odp_disabled,
segments_cache: odp_segments_cache,
logger: @logger
)

@config_manager = if config_manager.respond_to?(:config)
config_manager
elsif sdk_key
Expand All @@ -112,6 +151,7 @@ def initialize( # rubocop:disable Metrics/ParameterLists
else
StaticProjectConfigManager.new(datafile, @logger, @error_handler, skip_json_validation)
end
update_odp_config_on_datafile_update if datafile && !odp_disabled

@decision_service = DecisionService.new(@logger, @user_profile_service)

Expand Down Expand Up @@ -816,6 +856,7 @@ def close
@stopped = true
@config_manager.stop! if @config_manager.respond_to?(:stop!)
@event_processor.stop! if @event_processor.respond_to?(:stop!)
@odp_manager.stop! if @odp_manager.respond_to?(:stop!)
end

def get_optimizely_config
Expand Down Expand Up @@ -869,6 +910,17 @@ def get_optimizely_config
end
end

# Send an event to the ODP server.
#
# @param action - the event action name.
# @param type - the event type (default = "fullstack").
# @param identifiers - a hash for identifiers.
# @param data - a hash for associated data. The default event data will be added to this data before sending to the ODP server.

def send_odp_event(action:, type: Helpers::Constants::ODP_MANAGER_CONFIG[:EVENT_TYPE], identifiers: {}, data: {})
@odp_manager.send_event(type: type, action: action, identifiers: identifiers, data: data)
end

private

def get_variation_with_config(experiment_key, user_id, attributes, config)
Expand Down Expand Up @@ -1126,5 +1178,12 @@ def send_impression(config, experiment, variation_key, flag_key, rule_key, enabl
def project_config
@config_manager.config
end

def update_odp_config_on_datafile_update
config = @config_manager&.config
return unless config

@odp_manager.update_odp_config(config.public_key_for_odp, config.host_for_odp, config.all_segments)
end
end
end
16 changes: 16 additions & 0 deletions lib/optimizely/helpers/validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,22 @@ def odp_data_types_valid?(data)
valid_types = [String, Float, Integer, TrueClass, FalseClass, NilClass]
data.values.all? { |e| valid_types.member? e.class }
end

def segments_cache_valid?(segments_cache)
# Determines if a given segments_cache is valid.
#
# segments_cache - custom cache to be validated.
#
# Returns boolean depending on whether cache has required methods.
(
segments_cache.respond_to?(:reset) &&
segments_cache.method(:reset)&.parameters&.empty? &&
segments_cache.respond_to?(:lookup) &&
segments_cache.method(:lookup)&.parameters&.length&.positive? &&
segments_cache.respond_to?(:save) &&
segments_cache.method(:save)&.parameters&.length&.positive?
)
end
end
end
end
1 change: 1 addition & 0 deletions lib/optimizely/odp/odp_event_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#
require_relative 'zaius_rest_api_manager'
require_relative '../helpers/constants'
require_relative 'odp_event'

module Optimizely
class OdpEventManager
Expand Down
2 changes: 1 addition & 1 deletion lib/optimizely/odp/odp_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ def update_odp_config(api_key, api_host, segments_to_check)
@event_manager.update_config
end

def close!
def stop!
return unless @enabled

@event_manager.stop!
Expand Down
22 changes: 19 additions & 3 deletions lib/optimizely/optimizely_user_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,15 @@ def initialize(optimizely_client, user_id, user_attributes)
@user_id = user_id
@user_attributes = user_attributes.nil? ? {} : user_attributes.clone
@forced_decisions = {}
@qualified_segments = []
@qualified_segments = nil

@optimizely_client.odp_manager.identify_user(user_id: user_id)
end

def clone
user_context = OptimizelyUserContext.new(@optimizely_client, @user_id, user_attributes)
@forced_decision_mutex.synchronize { user_context.instance_variable_set('@forced_decisions', @forced_decisions.dup) unless @forced_decisions.empty? }
@qualified_segment_mutex.synchronize { user_context.instance_variable_set('@qualified_segments', @qualified_segments.dup) unless @qualified_segments.empty? }
@qualified_segment_mutex.synchronize { user_context.instance_variable_set('@qualified_segments', @qualified_segments.dup) unless @qualified_segments.nil? }
user_context
end

Expand Down Expand Up @@ -194,11 +196,25 @@ def qualified_segments=(segments)
# Checks if user is qualified for the provided segment.
#
# @param segment - A segment name
# @return true if qualified.

def qualified_for?(segment)
return false if @qualified_segments.empty?
return false if @qualified_segments.nil? || @qualified_segments.empty?

@qualified_segment_mutex.synchronize { @qualified_segments.include?(segment) }
end

# Fetch all qualified segments for the user context.
#
# The segments fetched will be saved in `@qualified_segments` and can be accessed any time.
#
# @param options - A set of options for fetching qualified segments (optional).
# @return On success, returns a non-nil segments array (can be empty). On failure, nil is returned.

def fetch_qualified_segments(options = [])
segments = @optimizely_client.odp_manager.fetch_qualified_segments(user_id: @user_id, options: options)
self.qualified_segments = segments
segments
end
end
end
2 changes: 1 addition & 1 deletion lib/optimizely/user_condition_evaluator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ def semver_less_than_or_equal_evaluator(condition)
end

def qualified_evaluator(condition)
# Evaluate the given match condition for the given user qaulified segments.
# Evaluate the given match condition for the given user qualified segments.
# Returns boolean true if condition value is in the user's qualified segments,
# false if the condition value is not in the user's qualified segments,
# nil if the condition value isn't a string.
Expand Down
2 changes: 1 addition & 1 deletion spec/audience_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
let(:config) { Optimizely::DatafileProjectConfig.new(config_body_JSON, spy_logger, error_handler) }
let(:typed_audience_config) { Optimizely::DatafileProjectConfig.new(config_typed_audience_JSON, spy_logger, error_handler) }
let(:integration_config) { Optimizely::DatafileProjectConfig.new(config_integration_JSON, spy_logger, error_handler) }
let(:project_instance) { Optimizely::Project.new(config_body_JSON, nil, spy_logger, error_handler) }
let(:project_instance) { Optimizely::Project.new(config_body_JSON, nil, spy_logger, error_handler, false, nil, nil, nil, nil, nil, [], nil, {disable_odp: true}) }
let(:user_context) { project_instance.create_user_context('some-user', {}) }

it 'should return true for user_meets_audience_conditions? when experiment is using no audience' do
Expand Down
11 changes: 7 additions & 4 deletions spec/decision_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
let(:spy_user_profile_service) { spy('user_profile_service') }
let(:config) { Optimizely::DatafileProjectConfig.new(config_body_JSON, spy_logger, error_handler) }
let(:decision_service) { Optimizely::DecisionService.new(spy_logger, spy_user_profile_service) }
let(:project_instance) { Optimizely::Project.new(config_body_JSON, nil, spy_logger, error_handler) }
let(:project_instance) { Optimizely::Project.new(config_body_JSON, nil, spy_logger, error_handler, false, nil, nil, nil, nil, nil, [], nil, {disable_odp: true}) }
let(:user_context) { project_instance.create_user_context('some-user', {}) }

describe '#get_variation' do
Expand Down Expand Up @@ -889,7 +889,8 @@
bucketing_id, reason = decision_service.send(:get_bucketing_id, 'test_user', user_attributes)
expect(bucketing_id).to eq('test_user')
expect(reason).to eq(nil)
expect(spy_logger).not_to have_received(:log)
expect(spy_logger).not_to have_received(:log).with(Logger::WARN, anything)
expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything)
end

it 'should not log any message and return given bucketing ID when bucketing ID is a String' do
Expand All @@ -900,7 +901,8 @@
bucketing_id, reason = decision_service.send(:get_bucketing_id, 'test_user', user_attributes)
expect(bucketing_id).to eq('i_am_bucketing_id')
expect(reason).to eq(nil)
expect(spy_logger).not_to have_received(:log)
expect(spy_logger).not_to have_received(:log).with(Logger::WARN, anything)
expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything)
end

it 'should not log any message and return empty String when bucketing ID is empty String' do
Expand All @@ -911,7 +913,8 @@
bucketing_id, reason = decision_service.send(:get_bucketing_id, 'test_user', user_attributes)
expect(bucketing_id).to eq('')
expect(reason).to eq(nil)
expect(spy_logger).not_to have_received(:log)
expect(spy_logger).not_to have_received(:log).with(Logger::WARN, anything)
expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything)
end
end

Expand Down
30 changes: 15 additions & 15 deletions spec/odp/odp_manager_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
expect(segments_cache.instance_variable_get('@capacity')).to eq 10_000
expect(segments_cache.instance_variable_get('@timeout')).to eq 600

manager.close!
manager.stop!
expect(event_manager.running?).to be false
end

Expand All @@ -73,7 +73,7 @@
expect(manager.instance_variable_get('@segment_manager')).to be segment_manager
expect(manager.instance_variable_get('@segment_manager').instance_variable_get('@segments_cache')).to be segments_cache

manager.close!
manager.stop!
end

it 'should allow custom segments_cache' do
Expand All @@ -83,7 +83,7 @@

expect(manager.instance_variable_get('@segment_manager').instance_variable_get('@segments_cache')).to be segments_cache

manager.close!
manager.stop!
end

it 'should allow custom event_manager' do
Expand All @@ -93,7 +93,7 @@

expect(manager.instance_variable_get('@event_manager')).to be event_manager

manager.close!
manager.stop!
end

it 'should not instantiate event/segment managers when disabled' do
Expand All @@ -120,7 +120,7 @@
segments = manager.fetch_qualified_segments(user_id: user_value, options: nil)

expect(segments).to eq [segments_to_check[0]]
manager.close!
manager.stop!
end

it 'should log error if disabled' do
Expand All @@ -137,7 +137,7 @@

response = manager.fetch_qualified_segments(user_id: 'user1', options: nil)
expect(response).to be_nil
manager.close!
manager.stop!
end

it 'should ignore cache' do
Expand All @@ -160,7 +160,7 @@
segments = manager.fetch_qualified_segments(user_id: user_value, options: [Optimizely::OptimizelySegmentOption::IGNORE_CACHE])

expect(segments).to eq [segments_to_check[0]]
manager.close!
manager.stop!
end

it 'should reset cache' do
Expand All @@ -184,7 +184,7 @@

expect(segments).to eq [segments_to_check[0]]
expect(segments_cache.lookup('wow')).to be_nil
manager.close!
manager.stop!
end
end

Expand All @@ -205,7 +205,7 @@

manager.send_event(**event)

manager.close!
manager.stop!
end

it 'should log error if data is invalid' do
Expand All @@ -217,7 +217,7 @@

manager.send_event(**event)

manager.close!
manager.stop!
end
end

Expand All @@ -239,7 +239,7 @@

manager.identify_user(user_id: user_value)

manager.close!
manager.stop!
end

it 'should log debug if disabled' do
Expand All @@ -249,7 +249,7 @@
manager = Optimizely::OdpManager.new(disable: true, logger: spy_logger)
manager.identify_user(user_id: user_value)

manager.close!
manager.stop!
end

it 'should log debug if not integrated' do
Expand All @@ -259,7 +259,7 @@
manager.update_odp_config(nil, nil, [])
manager.identify_user(user_id: user_value)

manager.close!
manager.stop!
end

it 'should log debug if datafile not ready' do
Expand All @@ -269,7 +269,7 @@
manager = Optimizely::OdpManager.new(disable: false, logger: spy_logger)
manager.identify_user(user_id: user_value)

manager.close!
manager.stop!
end
end

Expand Down Expand Up @@ -306,7 +306,7 @@
expect(event_manager.instance_variable_get('@api_host')).to eq api_host
expect(event_manager.instance_variable_get('@api_key')).to eq api_key

manager.close!
manager.stop!
end
end
end
Loading