Skip to content

Commit 7e2ccf2

Browse files
authored
expose cmab prediction endpoint in url template (#378)
* expose cmab prediction endpoint in url template * fix: use keyword arguments for DefaultCmabClient to resolve Rubocop linting error - Changed initialize method to use keyword arguments instead of positional arguments - Updated all call sites to use keyword arguments - Fixes Metrics/ParameterLists offense (too many optional parameters) - All tests passing (11 examples, 0 failures)
1 parent f6b7ad1 commit 7e2ccf2

File tree

5 files changed

+80
-10
lines changed

5 files changed

+80
-10
lines changed

lib/optimizely.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,9 @@ def initialize(
137137
# Initialize CMAB components if cmab service is nil
138138
if cmab_service.nil?
139139
@cmab_client = DefaultCmabClient.new(
140-
nil,
141-
CmabRetryConfig.new,
142-
@logger
140+
http_client: nil,
141+
retry_config: CmabRetryConfig.new,
142+
logger: @logger
143143
)
144144
@cmab_cache = LRUCache.new(Optimizely::DefaultCmabCacheOptions::DEFAULT_CMAB_CACHE_SIZE, Optimizely::DefaultCmabCacheOptions::DEFAULT_CMAB_CACHE_TIMEOUT)
145145
@cmab_service = DefaultCmabService.new(

lib/optimizely/cmab/cmab_client.rb

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,22 @@ class DefaultCmabClient
4343
# Client for interacting with the CMAB service.
4444
# Provides methods to fetch decisions with optional retry logic.
4545

46-
def initialize(http_client = nil, retry_config = nil, logger = nil)
46+
def initialize(http_client: nil, retry_config: nil, logger: nil, prediction_endpoint: nil)
4747
# Initialize the CMAB client.
4848
# Args:
4949
# http_client: HTTP client for making requests.
5050
# retry_config: Configuration for retry settings.
5151
# logger: Logger for logging errors and info.
52+
# prediction_endpoint: Custom prediction endpoint URL template.
53+
# Use #{rule_id} as placeholder for rule_id.
5254
@http_client = http_client || DefaultHttpClient.new
5355
@retry_config = retry_config || CmabRetryConfig.new
5456
@logger = logger || NoOpLogger.new
57+
@prediction_endpoint = if prediction_endpoint.to_s.strip.empty?
58+
'https://prediction.cmab.optimizely.com/predict/%s'
59+
else
60+
prediction_endpoint
61+
end
5562
end
5663

5764
def fetch_decision(rule_id, user_id, attributes, cmab_uuid, timeout: MAX_WAIT_TIME)
@@ -64,7 +71,7 @@ def fetch_decision(rule_id, user_id, attributes, cmab_uuid, timeout: MAX_WAIT_TI
6471
# timeout: Maximum wait time for the request to respond in seconds. (default is 10 seconds).
6572
# Returns:
6673
# The variation ID.
67-
url = "https://prediction.cmab.optimizely.com/predict/#{rule_id}"
74+
url = format(@prediction_endpoint, rule_id)
6875
cmab_attributes = attributes.map { |key, value| {'id' => key.to_s, 'value' => value, 'type' => 'custom_attribute'} }
6976

7077
request_body = {

lib/optimizely/helpers/sdk_settings.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ module Optimizely
2222
module Helpers
2323
class OptimizelySdkSettings
2424
attr_accessor :odp_disabled, :segments_cache_size, :segments_cache_timeout_in_secs, :odp_segments_cache, :odp_segment_manager,
25-
:odp_event_manager, :fetch_segments_timeout, :odp_event_timeout, :odp_flush_interval
25+
:odp_event_manager, :fetch_segments_timeout, :odp_event_timeout, :odp_flush_interval, :cmab_prediction_endpoint
2626

2727
# Contains configuration used for Optimizely Project initialization.
2828
#
@@ -35,6 +35,7 @@ class OptimizelySdkSettings
3535
# @param odp_segment_request_timeout - Time to wait in seconds for fetch_qualified_segments (optional. default = 10).
3636
# @param odp_event_request_timeout - Time to wait in seconds for send_odp_events (optional. default = 10).
3737
# @param odp_event_flush_interval - Time to wait in seconds for odp events to accumulate before sending (optional. default = 1).
38+
# @param cmab_prediction_endpoint - Custom CMAB prediction endpoint URL template (optional). Use %s as placeholder for rule_id. Defaults to production endpoint if not provided.
3839
def initialize(
3940
disable_odp: false,
4041
segments_cache_size: Constants::ODP_SEGMENTS_CACHE_CONFIG[:DEFAULT_CAPACITY],
@@ -44,7 +45,8 @@ def initialize(
4445
odp_event_manager: nil,
4546
odp_segment_request_timeout: nil,
4647
odp_event_request_timeout: nil,
47-
odp_event_flush_interval: nil
48+
odp_event_flush_interval: nil,
49+
cmab_prediction_endpoint: nil
4850
)
4951
@odp_disabled = disable_odp
5052
@segments_cache_size = segments_cache_size
@@ -55,6 +57,7 @@ def initialize(
5557
@fetch_segments_timeout = odp_segment_request_timeout
5658
@odp_event_timeout = odp_event_request_timeout
5759
@odp_flush_interval = odp_event_flush_interval
60+
@cmab_prediction_endpoint = cmab_prediction_endpoint
5861
end
5962
end
6063
end

lib/optimizely/optimizely_factory.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,12 @@ def self.cmab_custom_cache(custom_cache)
119119
@cmab_custom_cache = custom_cache
120120
end
121121

122+
# Convenience method for setting custom CMAB prediction endpoint.
123+
# @param prediction_endpoint String - Custom URL template for CMAB prediction API. Use %s as placeholder for rule_id.
124+
def self.cmab_prediction_endpoint(prediction_endpoint)
125+
@cmab_prediction_endpoint = prediction_endpoint
126+
end
127+
122128
# Returns a new optimizely instance.
123129
#
124130
# @params sdk_key - Required String uniquely identifying the fallback datafile corresponding to project.
@@ -202,7 +208,11 @@ def self.custom_instance( # rubocop:disable Metrics/ParameterLists
202208
)
203209

204210
# Initialize CMAB components
205-
cmab_client = DefaultCmabClient.new(logger: logger)
211+
cmab_prediction_endpoint = nil
212+
cmab_prediction_endpoint = settings.cmab_prediction_endpoint if settings&.cmab_prediction_endpoint
213+
cmab_prediction_endpoint ||= @cmab_prediction_endpoint
214+
215+
cmab_client = DefaultCmabClient.new(logger: logger, prediction_endpoint: cmab_prediction_endpoint)
206216
cmab_cache = @cmab_custom_cache || LRUCache.new(
207217
@cmab_cache_size || Optimizely::DefaultCmabCacheOptions::DEFAULT_CMAB_CACHE_SIZE,
208218
@cmab_cache_ttl || Optimizely::DefaultCmabCacheOptions::DEFAULT_CMAB_CACHE_TIMEOUT

spec/cmab/cmab_client_spec.rb

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
end
5656

5757
context 'when client is configured without retries' do
58-
let(:client) { described_class.new(nil, Optimizely::CmabRetryConfig.new(max_retries: 0), spy_logger) }
58+
let(:client) { described_class.new(http_client: nil, retry_config: Optimizely::CmabRetryConfig.new(max_retries: 0), logger: spy_logger) }
5959

6060
it 'should return the variation id on success' do
6161
WebMock.stub_request(:post, expected_url)
@@ -132,7 +132,7 @@
132132
end
133133

134134
context 'when client is configured with retries' do
135-
let(:client_with_retry) { described_class.new(nil, retry_config, spy_logger) }
135+
let(:client_with_retry) { described_class.new(http_client: nil, retry_config: retry_config, logger: spy_logger) }
136136

137137
it 'should return the variation id on first try' do
138138
WebMock.stub_request(:post, expected_url)
@@ -195,4 +195,54 @@
195195
expect(spy_logger).to have_received(:log).with(Logger::ERROR, a_string_including('Max retries exceeded for CMAB request'))
196196
end
197197
end
198+
199+
context 'when custom prediction endpoint is configured' do
200+
let(:custom_endpoint) { 'https://custom.endpoint.com/predict/%s' }
201+
let(:custom_url) { 'https://custom.endpoint.com/predict/test_rule' }
202+
let(:client_with_custom_endpoint) { described_class.new(http_client: nil, retry_config: Optimizely::CmabRetryConfig.new(max_retries: 0), logger: spy_logger, prediction_endpoint: custom_endpoint) }
203+
204+
it 'should use the custom prediction endpoint' do
205+
WebMock.stub_request(:post, custom_url)
206+
.with(body: expected_body_for_webmock, headers: expected_headers)
207+
.to_return(status: 200, body: {'predictions' => [{'variation_id' => 'custom123'}]}.to_json, headers: {'Content-Type' => 'application/json'})
208+
209+
result = client_with_custom_endpoint.fetch_decision(rule_id, user_id, attributes, cmab_uuid)
210+
211+
expect(result).to eq('custom123')
212+
expect(WebMock).to have_requested(:post, custom_url)
213+
.with(body: expected_body_for_webmock, headers: expected_headers).once
214+
end
215+
end
216+
217+
context 'when no prediction endpoint is provided' do
218+
let(:client_with_default) { described_class.new(http_client: nil, retry_config: Optimizely::CmabRetryConfig.new(max_retries: 0), logger: spy_logger, prediction_endpoint: nil) }
219+
220+
it 'should use the default prediction endpoint' do
221+
WebMock.stub_request(:post, expected_url)
222+
.with(body: expected_body_for_webmock, headers: expected_headers)
223+
.to_return(status: 200, body: {'predictions' => [{'variation_id' => 'default123'}]}.to_json, headers: {'Content-Type' => 'application/json'})
224+
225+
result = client_with_default.fetch_decision(rule_id, user_id, attributes, cmab_uuid)
226+
227+
expect(result).to eq('default123')
228+
expect(WebMock).to have_requested(:post, expected_url)
229+
.with(body: expected_body_for_webmock, headers: expected_headers).once
230+
end
231+
end
232+
233+
context 'when empty string prediction endpoint is provided' do
234+
let(:client_with_empty_endpoint) { described_class.new(http_client: nil, retry_config: Optimizely::CmabRetryConfig.new(max_retries: 0), logger: spy_logger, prediction_endpoint: '') }
235+
236+
it 'should fall back to the default prediction endpoint' do
237+
WebMock.stub_request(:post, expected_url)
238+
.with(body: expected_body_for_webmock, headers: expected_headers)
239+
.to_return(status: 200, body: {'predictions' => [{'variation_id' => 'fallback123'}]}.to_json, headers: {'Content-Type' => 'application/json'})
240+
241+
result = client_with_empty_endpoint.fetch_decision(rule_id, user_id, attributes, cmab_uuid)
242+
243+
expect(result).to eq('fallback123')
244+
expect(WebMock).to have_requested(:post, expected_url)
245+
.with(body: expected_body_for_webmock, headers: expected_headers).once
246+
end
247+
end
198248
end

0 commit comments

Comments
 (0)