Skip to content

Commit

Permalink
feat: fetch v2 for remote evaluation client (#64)
Browse files Browse the repository at this point in the history
  • Loading branch information
tyiuhc authored May 17, 2024
1 parent ab3ca2f commit 9091b5d
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 24 deletions.
52 changes: 48 additions & 4 deletions lib/experiment/remote/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,37 @@ def initialize(api_key, config = nil)
else
Logger::INFO
end
endpoint = "#{@config.server_url}/sdk/vardata"
endpoint = "#{@config.server_url}/sdk/v2/vardata?v=0"
@uri = URI(endpoint)
raise ArgumentError, 'Experiment API key is empty' if @api_key.nil? || @api_key.empty?
end

# Fetch all variants for a user synchronous.
# Fetch all variants for a user synchronously.
#
# This method will automatically retry if configured (default).
# @param [User] user
# @return [Hash] Variants Hash
def fetch(user)
filter_default_variants(fetch_internal(user))
rescue StandardError => e
@logger.error("[Experiment] Failed to fetch variants: #{e.message}")
{}
end

# Fetch all variants for a user synchronously.
#
# 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
# @return [Hash] Variants Hash
def fetch_v2(user)
fetch_internal(user)
rescue StandardError => e
@logger.error("[Experiment] Failed to fetch variants: #{e.message}")
{}
end

# Fetch all variants for a user asynchronous.
# Fetch all variants for a user asynchronously.
#
# This method will automatically retry if configured (default).
# @param [User] user
Expand All @@ -53,6 +66,24 @@ def fetch_async(user, &callback)
end
end

# Fetch all variants for a user asynchronously. 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).
#
# 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)
Thread.new do
variants = fetch_internal(user)
yield(user, filter_default_variants(variants)) unless callback.nil?
variants
rescue StandardError => e
@logger.error("[Experiment] Failed to fetch variants: #{e.message}")
yield(user, {}) unless callback.nil?
{}
end
end

private

# @param [User] user
Expand Down Expand Up @@ -132,7 +163,7 @@ def parse_json_variants(json)
# value was previously under the "key" field
variant_value = value.fetch('key')
end
variants.store(key, Variant.new(variant_value, value.fetch('payload', nil)))
variants.store(key, Variant.new(variant_value, value.fetch('payload', nil), value.fetch('key', nil), value.fetch('metadata', nil)))
end
variants
end
Expand All @@ -150,5 +181,18 @@ def should_retry_fetch?(err)

true
end

def filter_default_variants(variants)
variants.each do |key, value|
default = value&.metadata&.fetch('default', nil)
deployed = value&.metadata&.fetch('deployed', nil)
default = false if default.nil?
deployed = true if deployed.nil?
variants.delete(key) if default || !deployed
end
variants
end

private :filter_default_variants
end
end
12 changes: 10 additions & 2 deletions lib/experiment/variant.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
module AmplitudeExperiment
# Variant
class Variant
# The key of the variant determined by the flag configuration.
# @return [String] the value of variant key
attr_accessor :key

# The value of the variant determined by the flag configuration.
# @return [String] the value of variant value
attr_accessor :value
Expand All @@ -9,17 +13,21 @@ class Variant
# @return [Object, nil] the value of variant payload
attr_accessor :payload

attr_accessor :metadata

# @param [String] value The value of the variant determined by the flag configuration.
# @param [Object, nil] payload The attached payload, if any.
def initialize(value, payload = nil)
def initialize(value, payload = nil, key = nil, metadata = nil)
@key = key
@value = value
@payload = payload
@metadata = metadata
end

# Determine if current variant equal other variant
# @param [Variant] other
def ==(other)
value == other.value &&
key == other.key && value == other.value &&
payload == other.payload
end
end
Expand Down
2 changes: 1 addition & 1 deletion spec/experiment/local/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
require_relative '../../../lib/amplitude'

module AmplitudeExperiment
LOCAL_SERVER_URL = 'https://api.lab.amplitude.com/sdk/vardata'.freeze
LOCAL_SERVER_URL = 'https://api.lab.amplitude.com/sdk/v2/vardata?v=0'.freeze
TEST_USER = User.new(user_id: 'test_user')
TEST_USER_2 = User.new(user_id: 'user_id', device_id: 'device_id')

Expand Down
67 changes: 50 additions & 17 deletions spec/experiment/remote/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module AmplitudeExperiment
API_KEY = 'client-DvWljIjiiuqLbyjqdvBaLFfEBrAvGuA3'.freeze
SERVER_URL = 'https://api.lab.amplitude.com/sdk/vardata'.freeze
SERVER_URL = 'https://api.lab.amplitude.com/sdk/v2/vardata?v=0'.freeze

describe RemoteEvaluationClient do
describe '#initialize' do
Expand Down Expand Up @@ -33,25 +33,25 @@ module AmplitudeExperiment
})

describe '#fetch' do
def self.test_fetch(response, test_user, variant_name, debug, expected_state, expected_payload)
def self.test_fetch(response, test_user, variant_name, debug, expected_state, expected_payload, expected_key = nil)
it "fetch sync success with response #{response}, user #{test_user.user_id}, debug #{debug}" do
stub_request(:post, SERVER_URL)
.to_return(status: 200, body: response)
client = RemoteEvaluationClient.new(API_KEY, RemoteEvaluationConfig.new(debug: debug))
expected_variant = Variant.new(expected_state, expected_payload)
expected_variant = Variant.new(expected_state, expected_payload, expected_key)
variants = client.fetch(test_user)
expect(variants.key?(variant_name)).to be_truthy
expect(variants.fetch(variant_name)).to eq(expected_variant)
end
end

test_fetch response_with_key, test_user, variant_name, false, 'on', 'payload'
test_fetch response_with_key, test_user, variant_name, false, 'on', 'payload', 'on'
test_fetch response_with_value, test_user_with_properties, variant_name, false, 'on', 'payload'
test_fetch response_with_int_payload, test_user, variant_name, true, 'off', 123
test_fetch response_with_boolean_payload, test_user_with_properties, variant_name, false, 'on', false
test_fetch response_with_list_payload, test_user, variant_name, false, 'on', %w[payload1 payload2]
test_fetch response_with_hash_payload, test_user_with_properties, variant_name, false, 'off', { 'nested' => 'nested payload' }
test_fetch response_without_payload, test_user, variant_name, false, 'on', nil
test_fetch response_with_int_payload, test_user, variant_name, true, 'off', 123, 'off'
test_fetch response_with_boolean_payload, test_user_with_properties, variant_name, false, 'on', false, 'on'
test_fetch response_with_list_payload, test_user, variant_name, false, 'on', %w[payload1 payload2], 'on'
test_fetch response_with_hash_payload, test_user_with_properties, variant_name, false, 'off', { 'nested' => 'nested payload' }, 'off'
test_fetch response_without_payload, test_user, variant_name, false, 'on', nil, 'on'
test_fetch response_with_value_without_payload, test_user, variant_name, false, 'on', nil

it 'fetch timeout failure' do
Expand All @@ -70,12 +70,12 @@ def self.test_fetch(response, test_user, variant_name, debug, expected_state, ex
allow(Thread).to receive(:new).and_yield
end

def self.test_fetch_async(response, test_user, variant_name, debug, expected_state, expected_payload)
def self.test_fetch_async(response, test_user, variant_name, debug, expected_state, expected_payload, expected_key = nil)
it "fetch async success with response #{response}, user #{test_user.user_id}, debug #{debug}" do
stub_request(:post, SERVER_URL)
.to_return(status: 200, body: response)
client = RemoteEvaluationClient.new(API_KEY, RemoteEvaluationConfig.new(debug: debug))
expected_variant = Variant.new(expected_state, expected_payload)
expected_variant = Variant.new(expected_state, expected_payload, expected_key)
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)
Expand All @@ -85,13 +85,13 @@ def self.test_fetch_async(response, test_user, variant_name, debug, expected_sta
end
end

test_fetch_async response_with_key, test_user, variant_name, false, 'on', 'payload'
test_fetch_async response_with_key, test_user, variant_name, false, 'on', 'payload', 'on'
test_fetch_async response_with_value, test_user_with_properties, variant_name, false, 'on', 'payload'
test_fetch_async response_with_int_payload, test_user, variant_name, true, 'off', 123
test_fetch_async response_with_boolean_payload, test_user_with_properties, variant_name, false, 'on', false
test_fetch_async response_with_list_payload, test_user, variant_name, false, 'on', %w[payload1 payload2]
test_fetch_async response_with_hash_payload, test_user_with_properties, variant_name, false, 'off', { 'nested' => 'nested payload' }
test_fetch_async response_without_payload, test_user, variant_name, false, 'on', nil
test_fetch_async response_with_int_payload, test_user, variant_name, true, 'off', 123, 'off'
test_fetch_async response_with_boolean_payload, test_user_with_properties, variant_name, false, 'on', false, 'on'
test_fetch_async response_with_list_payload, test_user, variant_name, false, 'on', %w[payload1 payload2], 'on'
test_fetch_async response_with_hash_payload, test_user_with_properties, variant_name, false, 'off', { 'nested' => 'nested payload' }, 'off'
test_fetch_async response_without_payload, test_user, variant_name, false, 'on', nil, 'on'
test_fetch_async response_with_value_without_payload, test_user, variant_name, false, 'on', nil

it 'fetch async timeout failure' do
Expand Down Expand Up @@ -128,5 +128,38 @@ def self.test_fetch_async(response, test_user, variant_name, debug, expected_sta
end
end
end

describe '#fetch_v2' do
def self.test_fetch_v2(response, test_user, variant_name, debug, expected_state, expected_payload, expected_key = nil)
it "fetch v2 sync success with response #{response}, user #{test_user.user_id}, debug #{debug}" do
stub_request(:post, SERVER_URL)
.to_return(status: 200, body: response)
client = RemoteEvaluationClient.new(API_KEY, RemoteEvaluationConfig.new(debug: debug))
expected_variant = Variant.new(expected_state, expected_payload, expected_key)
variants = client.fetch_v2(test_user)
expect(variants.key?(variant_name)).to be_truthy
expect(variants.fetch(variant_name)).to eq(expected_variant)
end
end

test_fetch_v2 response_with_key, test_user, variant_name, false, 'on', 'payload', 'on'
test_fetch_v2 response_with_value, test_user_with_properties, variant_name, false, 'on', 'payload'
test_fetch_v2 response_with_int_payload, test_user, variant_name, true, 'off', 123, 'off'
test_fetch_v2 response_with_boolean_payload, test_user_with_properties, variant_name, false, 'on', false, 'on'
test_fetch_v2 response_with_list_payload, test_user, variant_name, false, 'on', %w[payload1 payload2], 'on'
test_fetch_v2 response_with_hash_payload, test_user_with_properties, variant_name, false, 'off', { 'nested' => 'nested payload' }, 'off'
test_fetch_v2 response_without_payload, test_user, variant_name, false, 'on', nil, 'on'
test_fetch_v2 response_with_value_without_payload, test_user, variant_name, false, 'on', nil

it 'fetch v2 timeout failure' do
stub_request(:post, SERVER_URL)
.to_timeout
test_user = User.new(user_id: 'test_user')
client = RemoteEvaluationClient.new(API_KEY, RemoteEvaluationConfig.new(fetch_timeout_millis: 1, fetch_retries: 1, debug: true))
variants = nil
expect { variants = client.fetch_v2(test_user) }.to output(/Retrying fetch/).to_stdout_from_any_process
expect(variants).to eq({})
end
end
end
end

0 comments on commit 9091b5d

Please sign in to comment.