Skip to content
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
1 change: 1 addition & 0 deletions lib/amplitude-experiment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
require 'experiment/variant'
require 'experiment/factory'
require 'experiment/remote/client'
require 'experiment/remote/fetch_options'
require 'experiment/local/client'
require 'experiment/local/config'
require 'experiment/local/assignment/assignment'
Expand Down
38 changes: 25 additions & 13 deletions lib/experiment/remote/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def initialize(api_key, config = nil)
# @param [User] user
# @return [Hash] Variants Hash
def fetch(user)
AmplitudeExperiment.filter_default_variants(fetch_internal(user))
AmplitudeExperiment.filter_default_variants(fetch_internal(user, nil))
rescue StandardError => e
@logger.error("[Experiment] Failed to fetch variants: #{e.message}")
{}
Expand All @@ -41,9 +41,10 @@ def fetch(user)
# This method will automatically retry if configured (default). This function differs from fetch as it will
# return a default variant object if the flag was evaluated but the user was not assigned (i.e. off).
# @param [User] user
# @param [FetchOptions] fetch_options
# @return [Hash] Variants Hash
def fetch_v2(user)
fetch_internal(user)
def fetch_v2(user, fetch_options = nil)
fetch_internal(user, fetch_options)
rescue StandardError => e
@logger.error("[Experiment] Failed to fetch variants: #{e.message}")
{}
Expand All @@ -56,7 +57,7 @@ def fetch_v2(user)
# @yield [User, Hash] callback block takes user object and variants hash
def fetch_async(user, &callback)
Thread.new do
variants = fetch_internal(user)
variants = AmplitudeExperiment.filter_default_variants(fetch_internal(user, nil))
yield(user, variants) unless callback.nil?
variants
rescue StandardError => e
Expand All @@ -72,10 +73,10 @@ def fetch_async(user, &callback)
# This method will automatically retry if configured (default).
# @param [User] user
# @yield [User, Hash] callback block takes user object and variants hash
def fetch_async_v2(user, &callback)
def fetch_async_v2(user, fetch_options = nil, &callback)
Thread.new do
variants = fetch_internal(user)
yield(user, filter_default_variants(variants)) unless callback.nil?
variants = fetch_internal(user, fetch_options)
yield(user, variants) unless callback.nil?
variants
rescue StandardError => e
@logger.error("[Experiment] Failed to fetch variants: #{e.message}")
Expand All @@ -87,14 +88,15 @@ def fetch_async_v2(user, &callback)
private

# @param [User] user
def fetch_internal(user)
# @param [FetchOptions] fetch_options
def fetch_internal(user, fetch_options)
@logger.debug("[Experiment] Fetching variants for user: #{user.as_json}")
do_fetch(user, @config.connect_timeout_millis, @config.fetch_timeout_millis)
do_fetch(user, fetch_options, @config.connect_timeout_millis, @config.fetch_timeout_millis)
rescue StandardError => e
@logger.error("[Experiment] Fetch failed: #{e.message}")
if should_retry_fetch?(e)
begin
retry_fetch(user)
retry_fetch(user, fetch_options)
rescue StandardError => err
@logger.error("[Experiment] Retry Fetch failed: #{err.message}")
end
Expand All @@ -103,7 +105,8 @@ def fetch_internal(user)
end

# @param [User] user
def retry_fetch(user)
# @param [FetchOptions] fetch_options
def retry_fetch(user, fetch_options)
return {} if @config.fetch_retries.zero?

@logger.debug('[Experiment] Retrying fetch')
Expand All @@ -112,7 +115,7 @@ def retry_fetch(user)
@config.fetch_retries.times do
sleep(delay_millis.to_f / 1000.0)
begin
return do_fetch(user, @config.connect_timeout_millis, @config.fetch_retry_timeout_millis)
return do_fetch(user, fetch_options, @config.connect_timeout_millis, @config.fetch_retry_timeout_millis)
rescue StandardError => e
@logger.error("[Experiment] Retry failed: #{e.message}")
err = e
Expand All @@ -123,15 +126,24 @@ def retry_fetch(user)
end

# @param [User] user
# @param [FetchOptions] fetch_options
# @param [Integer] connect_timeout_millis
# @param [Integer] fetch_timeout_millis
def do_fetch(user, connect_timeout_millis, fetch_timeout_millis)
def do_fetch(user, fetch_options, connect_timeout_millis, fetch_timeout_millis)
start_time = Time.now
user_context = add_context(user)
headers = {
'Authorization' => "Api-Key #{@api_key}",
'Content-Type' => 'application/json;charset=utf-8'
}
unless fetch_options.nil?
unless fetch_options.tracks_assignment.nil?
headers['X-Amp-Exp-Track'] = fetch_options.tracks_assignment ? 'track' : 'no-track'
end
unless fetch_options.tracks_exposure.nil?
headers['X-Amp-Exp-Exposure-Track'] = fetch_options.tracks_exposure ? 'track' : 'no-track'
end
end
connect_timeout = connect_timeout_millis.to_f / 1000 if (connect_timeout_millis.to_f / 1000) > 0
read_timeout = fetch_timeout_millis.to_f / 1000 if (fetch_timeout_millis.to_f / 1000) > 0
http = PersistentHttpClient.get(@uri, { open_timeout: connect_timeout, read_timeout: read_timeout }, @api_key)
Expand Down
19 changes: 19 additions & 0 deletions lib/experiment/remote/fetch_options.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module AmplitudeExperiment
# Fetch options
class FetchOptions
# Whether to track assignment events.
# If not provided, the default is null, which will use server default (to track assignment events).
# @return [Boolean, nil] the value of tracks_assignment
attr_accessor :tracks_assignment

# Whether to track exposure events.
# If not provided, the default is null, which will use server default (to not track exposure events).
# @return [Boolean, nil] the value of tracks_exposure
attr_accessor :tracks_exposure

def initialize(tracks_assignment: nil, tracks_exposure: nil)
@tracks_assignment = tracks_assignment
@tracks_exposure = tracks_exposure
end
end
end
51 changes: 51 additions & 0 deletions spec/experiment/remote/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,13 @@ def self.test_fetch_async_shared(response, test_user, variant_name, debug, expec
stub_request(:post, server_url)
.to_return(status: 200, body: response)
client = RemoteEvaluationClient.new(api_key, RemoteEvaluationConfig.new(debug: debug))
callback_called = false
variants = client.fetch_async(test_user) do |user, block_variants|
expect(user).to equal(test_user)
expect(block_variants.fetch(variant_name)).to eq(expected_variant)
callback_called = true
end
sleep 1 until callback_called
expect(variants.key?(variant_name)).to be_truthy
expect(variants.fetch(variant_name)).to eq(expected_variant)
end
Expand Down Expand Up @@ -175,6 +178,54 @@ def self.test_fetch_async_shared(response, test_user, variant_name, debug, expec
expect { variants = client.fetch_v2(test_user) }.to output(/Retrying fetch/).to_stdout_from_any_process
expect(variants).to eq({})
end

it 'fetch v2 with fetch options' do
stub_request(:post, server_url)
.to_return(status: 200, body: response_with_key)
test_user = User.new(user_id: 'test_user')
client = RemoteEvaluationClient.new(api_key)

WebMock.reset!
fetch_options = FetchOptions.new(tracks_assignment: true, tracks_exposure: true)
variants = client.fetch_v2(test_user, fetch_options)
expect(variants.key?(variant_name)).to be_truthy
expect(variants.fetch(variant_name)).to eq(Variant.new(key: 'on', payload: 'payload', value: 'on'))

expect(a_request(:post, server_url).with(headers: { 'X-Amp-Exp-Track' => 'track', 'X-Amp-Exp-Exposure-Track' => 'track' })).to have_been_made.once

WebMock.reset!
fetch_options = FetchOptions.new(tracks_assignment: false, tracks_exposure: false)
client.fetch_v2(test_user, fetch_options)
expect(a_request(:post, server_url).with(headers: { 'X-Amp-Exp-Track' => 'no-track', 'X-Amp-Exp-Exposure-Track' => 'no-track' })).to have_been_made.once

WebMock.reset!
last_request = nil
WebMock.after_request { |request_signature, _response| last_request = request_signature }
fetch_options = FetchOptions.new
client.fetch_v2(test_user, fetch_options)
expect(a_request(:post, server_url)).to have_been_made.once
expect(last_request.headers.key?('X-Amp-Exp-Track')).to be_falsy
expect(last_request.headers.key?('X-Amp-Exp-Exposure-Track')).to be_falsy
end
end

describe '#fetch_async_v2' do
it 'fetch async v2 with fetch options' do
stub_request(:post, server_url)
.to_return(status: 200, body: response_with_key)
test_user = User.new(user_id: 'test_user')
fetch_options = FetchOptions.new(tracks_assignment: true, tracks_exposure: true)
client = RemoteEvaluationClient.new(api_key, RemoteEvaluationConfig.new(debug: true))
callback_called = false
client.fetch_async_v2(test_user, fetch_options) do |user, block_variants|
expect(user).to equal(test_user)
expect(block_variants.key?(variant_name)).to be_truthy
expect(block_variants.fetch(variant_name)).to eq(Variant.new(key: 'on', payload: 'payload'))
expect(a_request(:post, server_url).with(headers: { 'X-Amp-Exp-Track' => 'track', 'X-Amp-Exp-Exposure-Track' => 'track' })).to have_been_made.once
callback_called = true
end
sleep 1 until callback_called
end
end
end
end