amplitude-experiment 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/amplitude-experiment.gemspec +5 -5
- data/lib/amplitude/processor.rb +1 -1
- data/lib/amplitude/timeline.rb +1 -1
- data/lib/amplitude-experiment.rb +6 -1
- data/lib/experiment/deployment/deployment_runner.rb +3 -3
- data/lib/experiment/evaluation/evaluation.rb +311 -0
- data/lib/experiment/evaluation/flag.rb +123 -0
- data/lib/experiment/evaluation/murmur3.rb +104 -0
- data/lib/experiment/evaluation/select.rb +16 -0
- data/lib/experiment/evaluation/semantic_version.rb +52 -0
- data/lib/experiment/evaluation/topological_sort.rb +56 -0
- data/lib/experiment/flag/flag_config_fetcher.rb +1 -1
- data/lib/experiment/flag/flag_config_storage.rb +1 -1
- data/lib/experiment/local/client.rb +8 -14
- data/lib/experiment/local/config.rb +21 -3
- data/lib/experiment/persistent_http_client.rb +1 -1
- data/lib/experiment/remote/client.rb +9 -12
- data/lib/experiment/remote/config.rb +31 -4
- data/lib/experiment/util/flag_config.rb +10 -10
- data/lib/experiment/version.rb +1 -1
- metadata +20 -24
- data/lib/experiment/local/evaluation/evaluation.rb +0 -76
- data/lib/experiment/local/evaluation/lib/linuxArm64/libevaluation_interop.so +0 -0
- data/lib/experiment/local/evaluation/lib/linuxArm64/libevaluation_interop_api.h +0 -110
- data/lib/experiment/local/evaluation/lib/linuxX64/libevaluation_interop.so +0 -0
- data/lib/experiment/local/evaluation/lib/linuxX64/libevaluation_interop_api.h +0 -110
- data/lib/experiment/local/evaluation/lib/macosArm64/libevaluation_interop.dylib +0 -0
- data/lib/experiment/local/evaluation/lib/macosArm64/libevaluation_interop_api.h +0 -110
- data/lib/experiment/local/evaluation/lib/macosX64/libevaluation_interop.dylib +0 -0
- data/lib/experiment/local/evaluation/lib/macosX64/libevaluation_interop_api.h +0 -110
- data/lib/experiment/util/topological_sort.rb +0 -39
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class SemanticVersion
|
|
4
|
+
include Comparable
|
|
5
|
+
|
|
6
|
+
attr_reader :major, :minor, :patch, :pre_release
|
|
7
|
+
|
|
8
|
+
MAJOR_MINOR_REGEX = '(\d+)\.(\d+)'
|
|
9
|
+
PATCH_REGEX = '(\d+)'
|
|
10
|
+
PRERELEASE_REGEX = '(-(([-\w]+\.?)*))?'
|
|
11
|
+
VERSION_PATTERN = /^#{MAJOR_MINOR_REGEX}(\.#{PATCH_REGEX}#{PRERELEASE_REGEX})?$/.freeze
|
|
12
|
+
|
|
13
|
+
def initialize(major, minor, patch, pre_release = nil)
|
|
14
|
+
@major = major
|
|
15
|
+
@minor = minor
|
|
16
|
+
@patch = patch
|
|
17
|
+
@pre_release = pre_release
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.parse(version)
|
|
21
|
+
return nil if version.nil?
|
|
22
|
+
|
|
23
|
+
match = VERSION_PATTERN.match(version)
|
|
24
|
+
return nil unless match
|
|
25
|
+
|
|
26
|
+
major = match[1].to_i
|
|
27
|
+
minor = match[2].to_i
|
|
28
|
+
patch = match[4]&.to_i || 0
|
|
29
|
+
pre_release = match[5]
|
|
30
|
+
|
|
31
|
+
new(major, minor, patch, pre_release)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def <=>(other)
|
|
35
|
+
return nil unless other.is_a?(SemanticVersion)
|
|
36
|
+
|
|
37
|
+
result = major <=> other.major
|
|
38
|
+
return result unless result.zero?
|
|
39
|
+
|
|
40
|
+
result = minor <=> other.minor
|
|
41
|
+
return result unless result.zero?
|
|
42
|
+
|
|
43
|
+
result = patch <=> other.patch
|
|
44
|
+
return result unless result.zero?
|
|
45
|
+
|
|
46
|
+
return 1 if !pre_release && other.pre_release
|
|
47
|
+
return -1 if pre_release && !other.pre_release
|
|
48
|
+
return 0 if !pre_release && !other.pre_release
|
|
49
|
+
|
|
50
|
+
pre_release <=> other.pre_release
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CycleError < StandardError
|
|
4
|
+
attr_accessor :path
|
|
5
|
+
|
|
6
|
+
def initialize(path)
|
|
7
|
+
super("Detected a cycle between flags #{path}")
|
|
8
|
+
self.path = path
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Performs topological sorting of feature flags based on their dependencies
|
|
13
|
+
class TopologicalSort
|
|
14
|
+
# Sort flags topologically based on their dependencies
|
|
15
|
+
def self.sort(flags, flag_keys = nil)
|
|
16
|
+
available = flags.clone
|
|
17
|
+
result = []
|
|
18
|
+
starting_keys = flag_keys.nil? || flag_keys.empty? ? flags.keys : flag_keys
|
|
19
|
+
|
|
20
|
+
starting_keys.each do |flag_key|
|
|
21
|
+
traversal = parent_traversal(flag_key, available)
|
|
22
|
+
result.concat(traversal) if traversal
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
result
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Perform depth-first traversal of flag dependencies
|
|
29
|
+
def self.parent_traversal(flag_key, available, path = [])
|
|
30
|
+
flag = available[flag_key]
|
|
31
|
+
return nil unless flag
|
|
32
|
+
|
|
33
|
+
# No dependencies - return flag and remove from available
|
|
34
|
+
if !flag.dependencies || flag.dependencies.empty?
|
|
35
|
+
available.delete(flag.key)
|
|
36
|
+
return [flag]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Check for cycles
|
|
40
|
+
path.push(flag.key)
|
|
41
|
+
result = []
|
|
42
|
+
|
|
43
|
+
flag.dependencies.each do |parent_key|
|
|
44
|
+
raise CycleError, path if path.any? { |p| p == parent_key }
|
|
45
|
+
|
|
46
|
+
traversal = parent_traversal(parent_key, available, path)
|
|
47
|
+
result.concat(traversal) if traversal
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
result.push(flag)
|
|
51
|
+
path.pop
|
|
52
|
+
available.delete(flag.key)
|
|
53
|
+
|
|
54
|
+
result
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -44,7 +44,7 @@ module AmplitudeExperiment
|
|
|
44
44
|
raise "flagConfigs - received error response: #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPOK)
|
|
45
45
|
|
|
46
46
|
@logger.debug("[Experiment] Fetch flag configs: #{response.body}")
|
|
47
|
-
JSON.parse(response.body)
|
|
47
|
+
JSON.parse(response.body).map { |f| Evaluation::Flag.from_hash(f) }
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
# Fetch local evaluation mode flag configs from the Experiment API server.
|
|
@@ -12,19 +12,15 @@ module AmplitudeExperiment
|
|
|
12
12
|
# @param [LocalEvaluationConfig] config The config object
|
|
13
13
|
|
|
14
14
|
def initialize(api_key, config = nil)
|
|
15
|
-
require 'experiment/local/evaluation/evaluation'
|
|
16
15
|
@api_key = api_key
|
|
17
16
|
@config = config || LocalEvaluationConfig.new
|
|
17
|
+
@logger = @config.logger
|
|
18
18
|
@flags = nil
|
|
19
19
|
@flags_mutex = Mutex.new
|
|
20
|
-
@logger = Logger.new($stdout)
|
|
21
|
-
@logger.level = if @config.debug
|
|
22
|
-
Logger::DEBUG
|
|
23
|
-
else
|
|
24
|
-
Logger::INFO
|
|
25
|
-
end
|
|
26
20
|
raise ArgumentError, 'Experiment API key is empty' if @api_key.nil? || @api_key.empty?
|
|
27
21
|
|
|
22
|
+
@engine = Evaluation::Engine.new
|
|
23
|
+
|
|
28
24
|
@assignment_service = nil
|
|
29
25
|
@assignment_service = AssignmentService.new(AmplitudeAnalytics::Amplitude.new(config.assignment_config.api_key, configuration: config.assignment_config), AssignmentFilter.new(config.assignment_config.cache_capacity)) if config&.assignment_config
|
|
30
26
|
|
|
@@ -67,15 +63,13 @@ module AmplitudeExperiment
|
|
|
67
63
|
flags = @flag_config_storage.flag_configs
|
|
68
64
|
return {} if flags.nil?
|
|
69
65
|
|
|
70
|
-
sorted_flags =
|
|
66
|
+
sorted_flags = TopologicalSort.sort(flags, flag_keys)
|
|
71
67
|
required_cohorts_in_storage(sorted_flags)
|
|
72
|
-
flags_json = sorted_flags.to_json
|
|
73
68
|
user = enrich_user_with_cohorts(user, flags) if @config.cohort_sync_config
|
|
74
69
|
context = AmplitudeExperiment.user_to_evaluation_context(user)
|
|
75
|
-
context_json = context.to_json
|
|
76
70
|
|
|
77
|
-
@logger.debug("[Experiment] Evaluate: User: #{
|
|
78
|
-
result =
|
|
71
|
+
@logger.debug("[Experiment] Evaluate: User: #{context} - Rules: #{flags}") if @config.debug
|
|
72
|
+
result = @engine.evaluate(context, sorted_flags)
|
|
79
73
|
@logger.debug("[Experiment] evaluate - result: #{result}") if @config.debug
|
|
80
74
|
variants = AmplitudeExperiment.evaluation_variants_json_to_variants(result)
|
|
81
75
|
@assignment_service&.track(Assignment.new(user, variants))
|
|
@@ -113,9 +107,9 @@ module AmplitudeExperiment
|
|
|
113
107
|
missing_cohorts_str = "[#{missing_cohorts.map(&:to_s).join(', ')}]"
|
|
114
108
|
|
|
115
109
|
message = if @config.cohort_sync_config
|
|
116
|
-
"Evaluating flag #{flag
|
|
110
|
+
"Evaluating flag #{flag.key} dependent on cohorts #{cohort_ids_str} without #{missing_cohorts_str} in storage"
|
|
117
111
|
else
|
|
118
|
-
"Evaluating flag #{flag
|
|
112
|
+
"Evaluating flag #{flag.key} dependent on cohorts #{cohort_ids_str} without cohort syncing configured"
|
|
119
113
|
end
|
|
120
114
|
|
|
121
115
|
@logger.warn(message)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require 'logger'
|
|
2
|
+
|
|
1
3
|
module AmplitudeExperiment
|
|
2
4
|
module ServerZone
|
|
3
5
|
US = 'US'.freeze
|
|
@@ -9,11 +11,17 @@ module AmplitudeExperiment
|
|
|
9
11
|
# Default server url
|
|
10
12
|
DEFAULT_SERVER_URL = 'https://api.lab.amplitude.com'.freeze
|
|
11
13
|
EU_SERVER_URL = 'https://flag.lab.eu.amplitude.com'.freeze
|
|
14
|
+
DEFAULT_LOGDEV = $stdout
|
|
15
|
+
DEFAULT_LOG_LEVEL = Logger::ERROR
|
|
12
16
|
|
|
13
17
|
# Set to true to log some extra information to the console.
|
|
14
18
|
# @return [Boolean] the value of debug
|
|
15
19
|
attr_accessor :debug
|
|
16
20
|
|
|
21
|
+
# Set the client logger to a user defined [Logger]
|
|
22
|
+
# @return [Logger] the logger instance of the client
|
|
23
|
+
attr_accessor :logger
|
|
24
|
+
|
|
17
25
|
# The server endpoint from which to request variants.
|
|
18
26
|
# @return [String] the value of server url
|
|
19
27
|
attr_accessor :server_url
|
|
@@ -35,16 +43,26 @@ module AmplitudeExperiment
|
|
|
35
43
|
attr_accessor :cohort_sync_config
|
|
36
44
|
|
|
37
45
|
# @param [Boolean] debug Set to true to log some extra information to the console.
|
|
46
|
+
# @param [Logger] logger instance to be used for all client logging behavior
|
|
38
47
|
# @param [String] server_url The server endpoint from which to request variants.
|
|
39
48
|
# @param [String] server_zone Location of the Amplitude data center to get flags and cohorts from, US or EU
|
|
40
49
|
# @param [Hash] bootstrap The value of bootstrap.
|
|
41
50
|
# @param [long] flag_config_polling_interval_millis The value of flag config polling interval in million seconds.
|
|
42
51
|
# @param [AssignmentConfig] assignment_config Configuration for automatically tracking assignment events after an evaluation.
|
|
43
52
|
# @param [CohortSyncConfig] cohort_sync_config Configuration for downloading cohorts required for flag evaluation
|
|
44
|
-
def initialize(server_url: DEFAULT_SERVER_URL,
|
|
45
|
-
|
|
53
|
+
def initialize(server_url: DEFAULT_SERVER_URL,
|
|
54
|
+
server_zone: ServerZone::US,
|
|
55
|
+
bootstrap: {},
|
|
56
|
+
flag_config_polling_interval_millis: 30_000,
|
|
57
|
+
debug: false,
|
|
58
|
+
logger: nil,
|
|
59
|
+
assignment_config: nil,
|
|
46
60
|
cohort_sync_config: nil)
|
|
47
|
-
@
|
|
61
|
+
@logger = logger
|
|
62
|
+
if logger.nil?
|
|
63
|
+
@logger = Logger.new(DEFAULT_LOGDEV)
|
|
64
|
+
@logger.level = debug ? Logger::DEBUG : DEFAULT_LOG_LEVEL
|
|
65
|
+
end
|
|
48
66
|
@server_url = server_url
|
|
49
67
|
@server_zone = server_zone
|
|
50
68
|
@cohort_sync_config = cohort_sync_config
|
|
@@ -5,7 +5,7 @@ module AmplitudeExperiment
|
|
|
5
5
|
# WARNING: these connections are not safe for concurrent requests. Callers
|
|
6
6
|
# must synchronize requests per connection.
|
|
7
7
|
class PersistentHttpClient
|
|
8
|
-
DEFAULT_OPTIONS = { read_timeout: 80 }.freeze
|
|
8
|
+
DEFAULT_OPTIONS = { open_timeout: 60, read_timeout: 80 }.freeze
|
|
9
9
|
|
|
10
10
|
class << self
|
|
11
11
|
# url: URI / String
|
|
@@ -13,12 +13,7 @@ module AmplitudeExperiment
|
|
|
13
13
|
def initialize(api_key, config = nil)
|
|
14
14
|
@api_key = api_key
|
|
15
15
|
@config = config || RemoteEvaluationConfig.new
|
|
16
|
-
@logger =
|
|
17
|
-
@logger.level = if @config.debug
|
|
18
|
-
Logger::DEBUG
|
|
19
|
-
else
|
|
20
|
-
Logger::INFO
|
|
21
|
-
end
|
|
16
|
+
@logger = @config.logger
|
|
22
17
|
endpoint = "#{@config.server_url}/sdk/v2/vardata?v=0"
|
|
23
18
|
@uri = URI(endpoint)
|
|
24
19
|
raise ArgumentError, 'Experiment API key is empty' if @api_key.nil? || @api_key.empty?
|
|
@@ -89,7 +84,7 @@ module AmplitudeExperiment
|
|
|
89
84
|
# @param [User] user
|
|
90
85
|
def fetch_internal(user)
|
|
91
86
|
@logger.debug("[Experiment] Fetching variants for user: #{user.as_json}")
|
|
92
|
-
do_fetch(user, @config.fetch_timeout_millis)
|
|
87
|
+
do_fetch(user, @config.connect_timeout_millis, @config.fetch_timeout_millis)
|
|
93
88
|
rescue StandardError => e
|
|
94
89
|
@logger.error("[Experiment] Fetch failed: #{e.message}")
|
|
95
90
|
if should_retry_fetch?(e)
|
|
@@ -112,7 +107,7 @@ module AmplitudeExperiment
|
|
|
112
107
|
@config.fetch_retries.times do
|
|
113
108
|
sleep(delay_millis.to_f / 1000.0)
|
|
114
109
|
begin
|
|
115
|
-
return do_fetch(user, @config.fetch_retry_timeout_millis)
|
|
110
|
+
return do_fetch(user, @config.connect_timeout_millis, @config.fetch_retry_timeout_millis)
|
|
116
111
|
rescue StandardError => e
|
|
117
112
|
@logger.error("[Experiment] Retry failed: #{e.message}")
|
|
118
113
|
err = e
|
|
@@ -123,16 +118,18 @@ module AmplitudeExperiment
|
|
|
123
118
|
end
|
|
124
119
|
|
|
125
120
|
# @param [User] user
|
|
126
|
-
# @param [Integer]
|
|
127
|
-
|
|
121
|
+
# @param [Integer] connect_timeout_millis
|
|
122
|
+
# @param [Integer] fetch_timeout_millis
|
|
123
|
+
def do_fetch(user, connect_timeout_millis, fetch_timeout_millis)
|
|
128
124
|
start_time = Time.now
|
|
129
125
|
user_context = add_context(user)
|
|
130
126
|
headers = {
|
|
131
127
|
'Authorization' => "Api-Key #{@api_key}",
|
|
132
128
|
'Content-Type' => 'application/json;charset=utf-8'
|
|
133
129
|
}
|
|
134
|
-
|
|
135
|
-
|
|
130
|
+
connect_timeout = connect_timeout_millis.to_f / 1000 if (connect_timeout_millis.to_f / 1000) > 0
|
|
131
|
+
read_timeout = fetch_timeout_millis.to_f / 1000 if (fetch_timeout_millis.to_f / 1000) > 0
|
|
132
|
+
http = PersistentHttpClient.get(@uri, { open_timeout: connect_timeout, read_timeout: read_timeout }, @api_key)
|
|
136
133
|
request = Net::HTTP::Post.new(@uri, headers)
|
|
137
134
|
request.body = user_context.to_json
|
|
138
135
|
@logger.warn("[Experiment] encoded user object length #{request.body.length} cannot be cached by CDN; must be < 8KB") if request.body.length > 8000
|
|
@@ -1,17 +1,29 @@
|
|
|
1
|
+
require 'logger'
|
|
2
|
+
|
|
1
3
|
module AmplitudeExperiment
|
|
2
4
|
# Configuration
|
|
3
5
|
class RemoteEvaluationConfig
|
|
4
6
|
# Default server url
|
|
5
7
|
DEFAULT_SERVER_URL = 'https://api.lab.amplitude.com'.freeze
|
|
8
|
+
DEFAULT_LOGDEV = $stdout
|
|
9
|
+
DEFAULT_LOG_LEVEL = Logger::ERROR
|
|
6
10
|
|
|
7
11
|
# Set to true to log some extra information to the console.
|
|
8
12
|
# @return [Boolean] the value of debug
|
|
9
13
|
attr_accessor :debug
|
|
10
14
|
|
|
15
|
+
# Set the client logger to a user defined [Logger]
|
|
16
|
+
# @return [Logger] the logger instance of the client
|
|
17
|
+
attr_accessor :logger
|
|
18
|
+
|
|
11
19
|
# The server endpoint from which to request variants.
|
|
12
20
|
# @return [Boolean] the value of server url
|
|
13
21
|
attr_accessor :server_url
|
|
14
22
|
|
|
23
|
+
# The request connection open timeout, in milliseconds, used when fetching variants triggered by calling start() or setUser().
|
|
24
|
+
# @return [Integer] the value of connect_timeout_millis
|
|
25
|
+
attr_accessor :connect_timeout_millis
|
|
26
|
+
|
|
15
27
|
# The request timeout, in milliseconds, used when fetching variants triggered by calling start() or setUser().
|
|
16
28
|
# @return [Integer] the value of fetch_timeout_millis
|
|
17
29
|
attr_accessor :fetch_timeout_millis
|
|
@@ -39,7 +51,10 @@ module AmplitudeExperiment
|
|
|
39
51
|
attr_accessor :fetch_retry_timeout_millis
|
|
40
52
|
|
|
41
53
|
# @param [Boolean] debug Set to true to log some extra information to the console.
|
|
54
|
+
# @param [Logger] logger instance to be used for all client logging behavior
|
|
42
55
|
# @param [String] server_url The server endpoint from which to request variants.
|
|
56
|
+
# @param [Integer] connect_timeout_millis The request connection open timeout, in milliseconds, used when
|
|
57
|
+
# fetching variants triggered by calling start() or setUser().
|
|
43
58
|
# @param [Integer] fetch_timeout_millis The request timeout, in milliseconds, used when fetching variants
|
|
44
59
|
# triggered by calling start() or setUser().
|
|
45
60
|
# @param [Integer] fetch_retries The number of retries to attempt before failing.
|
|
@@ -49,11 +64,23 @@ module AmplitudeExperiment
|
|
|
49
64
|
# greater than the max, the max is used for all subsequent retries.
|
|
50
65
|
# @param [Float] fetch_retry_backoff_scalar Scales the minimum backoff exponentially.
|
|
51
66
|
# @param [Integer] fetch_retry_timeout_millis The request timeout for retrying fetch requests.
|
|
52
|
-
def initialize(debug: false,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
67
|
+
def initialize(debug: false,
|
|
68
|
+
logger: nil,
|
|
69
|
+
server_url: DEFAULT_SERVER_URL,
|
|
70
|
+
connect_timeout_millis: 60_000,
|
|
71
|
+
fetch_timeout_millis: 10_000,
|
|
72
|
+
fetch_retries: 0,
|
|
73
|
+
fetch_retry_backoff_min_millis: 500,
|
|
74
|
+
fetch_retry_backoff_max_millis: 10_000,
|
|
75
|
+
fetch_retry_backoff_scalar: 1.5,
|
|
76
|
+
fetch_retry_timeout_millis: 10_000)
|
|
77
|
+
@logger = logger
|
|
78
|
+
if logger.nil?
|
|
79
|
+
@logger = Logger.new(DEFAULT_LOGDEV)
|
|
80
|
+
@logger.level = debug ? Logger::DEBUG : DEFAULT_LOG_LEVEL
|
|
81
|
+
end
|
|
56
82
|
@server_url = server_url
|
|
83
|
+
@connect_timeout_millis = connect_timeout_millis
|
|
57
84
|
@fetch_timeout_millis = fetch_timeout_millis
|
|
58
85
|
@fetch_retries = fetch_retries
|
|
59
86
|
@fetch_retry_backoff_min_millis = fetch_retry_backoff_min_millis
|
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
module AmplitudeExperiment
|
|
2
2
|
def self.cohort_filter?(condition)
|
|
3
|
-
['set contains any', 'set does not contain any'].include?(condition
|
|
4
|
-
condition
|
|
5
|
-
condition
|
|
3
|
+
['set contains any', 'set does not contain any'].include?(condition.op) &&
|
|
4
|
+
condition.selector &&
|
|
5
|
+
condition.selector[-1] == 'cohort_ids'
|
|
6
6
|
end
|
|
7
7
|
|
|
8
8
|
def self.get_grouped_cohort_condition_ids(segment)
|
|
9
9
|
cohort_ids = {}
|
|
10
|
-
conditions = segment
|
|
10
|
+
conditions = segment.conditions || []
|
|
11
11
|
conditions.each do |condition|
|
|
12
12
|
condition = condition[0]
|
|
13
|
-
next unless cohort_filter?(condition) && (condition
|
|
13
|
+
next unless cohort_filter?(condition) && (condition.selector[1].length > 2)
|
|
14
14
|
|
|
15
|
-
context_subtype = condition
|
|
15
|
+
context_subtype = condition.selector[1]
|
|
16
16
|
group_type =
|
|
17
17
|
if context_subtype == 'user'
|
|
18
18
|
USER_GROUP_TYPE
|
|
19
|
-
elsif condition
|
|
20
|
-
condition
|
|
19
|
+
elsif condition.selector.include?('groups')
|
|
20
|
+
condition.selector[2]
|
|
21
21
|
else
|
|
22
22
|
next
|
|
23
23
|
end
|
|
24
24
|
cohort_ids[group_type] ||= Set.new
|
|
25
|
-
cohort_ids[group_type].merge(condition
|
|
25
|
+
cohort_ids[group_type].merge(condition.values)
|
|
26
26
|
end
|
|
27
27
|
cohort_ids
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def self.get_grouped_cohort_ids_from_flag(flag)
|
|
31
31
|
cohort_ids = {}
|
|
32
|
-
segments = flag
|
|
32
|
+
segments = flag.segments || []
|
|
33
33
|
segments.each do |segment|
|
|
34
34
|
get_grouped_cohort_condition_ids(segment).each do |key, values|
|
|
35
35
|
cohort_ids[key] ||= Set.new
|
data/lib/experiment/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: amplitude-experiment
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Amplitude
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2025-11-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: concurrent-ruby
|
|
@@ -17,7 +17,7 @@ dependencies:
|
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
19
|
version: 1.2.2
|
|
20
|
-
type: :
|
|
20
|
+
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
@@ -58,14 +58,14 @@ dependencies:
|
|
|
58
58
|
requirements:
|
|
59
59
|
- - '='
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '6.
|
|
61
|
+
version: '6.10'
|
|
62
62
|
type: :development
|
|
63
63
|
prerelease: false
|
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
65
|
requirements:
|
|
66
66
|
- - '='
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '6.
|
|
68
|
+
version: '6.10'
|
|
69
69
|
- !ruby/object:Gem::Dependency
|
|
70
70
|
name: rspec
|
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -151,19 +151,19 @@ dependencies:
|
|
|
151
151
|
- !ruby/object:Gem::Version
|
|
152
152
|
version: 2.8.1
|
|
153
153
|
- !ruby/object:Gem::Dependency
|
|
154
|
-
name:
|
|
154
|
+
name: jar-dependencies
|
|
155
155
|
requirement: !ruby/object:Gem::Requirement
|
|
156
156
|
requirements:
|
|
157
|
-
- -
|
|
157
|
+
- - '='
|
|
158
158
|
- !ruby/object:Gem::Version
|
|
159
|
-
version:
|
|
160
|
-
type: :
|
|
159
|
+
version: 0.4.1
|
|
160
|
+
type: :development
|
|
161
161
|
prerelease: false
|
|
162
162
|
version_requirements: !ruby/object:Gem::Requirement
|
|
163
163
|
requirements:
|
|
164
|
-
- -
|
|
164
|
+
- - '='
|
|
165
165
|
- !ruby/object:Gem::Version
|
|
166
|
-
version:
|
|
166
|
+
version: 0.4.1
|
|
167
167
|
description: Amplitude Experiment Ruby Server SDK
|
|
168
168
|
email:
|
|
169
169
|
- sdk@amplitude.com
|
|
@@ -197,6 +197,12 @@ files:
|
|
|
197
197
|
- lib/experiment/cookie.rb
|
|
198
198
|
- lib/experiment/deployment/deployment_runner.rb
|
|
199
199
|
- lib/experiment/error.rb
|
|
200
|
+
- lib/experiment/evaluation/evaluation.rb
|
|
201
|
+
- lib/experiment/evaluation/flag.rb
|
|
202
|
+
- lib/experiment/evaluation/murmur3.rb
|
|
203
|
+
- lib/experiment/evaluation/select.rb
|
|
204
|
+
- lib/experiment/evaluation/semantic_version.rb
|
|
205
|
+
- lib/experiment/evaluation/topological_sort.rb
|
|
200
206
|
- lib/experiment/factory.rb
|
|
201
207
|
- lib/experiment/flag/flag_config_fetcher.rb
|
|
202
208
|
- lib/experiment/flag/flag_config_storage.rb
|
|
@@ -206,15 +212,6 @@ files:
|
|
|
206
212
|
- lib/experiment/local/assignment/assignment_service.rb
|
|
207
213
|
- lib/experiment/local/client.rb
|
|
208
214
|
- lib/experiment/local/config.rb
|
|
209
|
-
- lib/experiment/local/evaluation/evaluation.rb
|
|
210
|
-
- lib/experiment/local/evaluation/lib/linuxArm64/libevaluation_interop.so
|
|
211
|
-
- lib/experiment/local/evaluation/lib/linuxArm64/libevaluation_interop_api.h
|
|
212
|
-
- lib/experiment/local/evaluation/lib/linuxX64/libevaluation_interop.so
|
|
213
|
-
- lib/experiment/local/evaluation/lib/linuxX64/libevaluation_interop_api.h
|
|
214
|
-
- lib/experiment/local/evaluation/lib/macosArm64/libevaluation_interop.dylib
|
|
215
|
-
- lib/experiment/local/evaluation/lib/macosArm64/libevaluation_interop_api.h
|
|
216
|
-
- lib/experiment/local/evaluation/lib/macosX64/libevaluation_interop.dylib
|
|
217
|
-
- lib/experiment/local/evaluation/lib/macosX64/libevaluation_interop_api.h
|
|
218
215
|
- lib/experiment/persistent_http_client.rb
|
|
219
216
|
- lib/experiment/remote/client.rb
|
|
220
217
|
- lib/experiment/remote/config.rb
|
|
@@ -223,7 +220,6 @@ files:
|
|
|
223
220
|
- lib/experiment/util/hash.rb
|
|
224
221
|
- lib/experiment/util/lru_cache.rb
|
|
225
222
|
- lib/experiment/util/poller.rb
|
|
226
|
-
- lib/experiment/util/topological_sort.rb
|
|
227
223
|
- lib/experiment/util/user.rb
|
|
228
224
|
- lib/experiment/util/variant.rb
|
|
229
225
|
- lib/experiment/variant.rb
|
|
@@ -233,7 +229,7 @@ licenses:
|
|
|
233
229
|
- MIT
|
|
234
230
|
metadata:
|
|
235
231
|
rubygems_mfa_required: 'false'
|
|
236
|
-
post_install_message:
|
|
232
|
+
post_install_message:
|
|
237
233
|
rdoc_options: []
|
|
238
234
|
require_paths:
|
|
239
235
|
- lib
|
|
@@ -249,7 +245,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
249
245
|
version: '0'
|
|
250
246
|
requirements: []
|
|
251
247
|
rubygems_version: 3.1.6
|
|
252
|
-
signing_key:
|
|
248
|
+
signing_key:
|
|
253
249
|
specification_version: 4
|
|
254
250
|
summary: Amplitude Experiment Ruby Server SDK
|
|
255
251
|
test_files: []
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
# rubocop:disable all
|
|
2
|
-
require 'ffi'
|
|
3
|
-
require 'json'
|
|
4
|
-
|
|
5
|
-
# The evaluation wrapper
|
|
6
|
-
module EvaluationInterop
|
|
7
|
-
extend FFI::Library
|
|
8
|
-
host_os = RbConfig::CONFIG['host_os']
|
|
9
|
-
cpu = RbConfig::CONFIG['host_cpu']
|
|
10
|
-
evaluation_dir = File.dirname(__FILE__)
|
|
11
|
-
ffi_lib ["#{evaluation_dir}/lib/macosX64/libevaluation_interop.dylib"] if host_os =~ /darwin|mac os/ && cpu =~ /x86_64/
|
|
12
|
-
ffi_lib ["#{evaluation_dir}/lib/macosArm64/libevaluation_interop.dylib"] if host_os =~ /darwin|mac os/ && cpu =~ /arm64/
|
|
13
|
-
ffi_lib ["#{evaluation_dir}/lib/linuxX64/libevaluation_interop.so"] if host_os =~ /linux/ && cpu =~ /x86_64/
|
|
14
|
-
ffi_lib ["#{evaluation_dir}/lib/linuxArm64/libevaluation_interop.so"] if host_os =~ /linux/ && cpu =~ /arm64|aarch64/
|
|
15
|
-
|
|
16
|
-
class Root < FFI::Struct
|
|
17
|
-
layout :evaluate, callback([:string, :string], :pointer)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
class Kotlin < FFI::Struct
|
|
21
|
-
layout :root, Root
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
class Libevaluation_interop_ExportedSymbols < FFI::Struct
|
|
25
|
-
layout :DisposeStablePointer, callback([:pointer], :void),
|
|
26
|
-
:DisposeString, callback([:pointer], :void),
|
|
27
|
-
:IsInstance, callback([:pointer, :string], :pointer),
|
|
28
|
-
:createNullableByte, callback([:string], :pointer),
|
|
29
|
-
:getNonNullValueOfByte, callback([:pointer], :pointer),
|
|
30
|
-
:createNullableShort, callback([:pointer], :pointer),
|
|
31
|
-
:getNonNullValueOfShort, callback([:pointer], :pointer),
|
|
32
|
-
:createNullableInt, callback([:pointer], :pointer),
|
|
33
|
-
:getNonNullValueOfInt, callback([:pointer], :pointer),
|
|
34
|
-
:createNullableLong, callback([:pointer], :pointer),
|
|
35
|
-
:getNonNullValueOfLong, callback([:pointer], :pointer),
|
|
36
|
-
:createNullableFloat, callback([:pointer], :pointer),
|
|
37
|
-
:getNonNullValueOfFloat, callback([:pointer], :pointer),
|
|
38
|
-
:createNullableDouble, callback([:pointer], :pointer),
|
|
39
|
-
:getNonNullValueOfDouble, callback([:pointer], :pointer),
|
|
40
|
-
:createNullableChar, callback([:pointer], :pointer),
|
|
41
|
-
:getNonNullValueOfChar, callback([:pointer], :pointer),
|
|
42
|
-
:createNullableBoolean, callback([:pointer], :pointer),
|
|
43
|
-
:getNonNullValueOfBoolean, callback([:pointer], :pointer),
|
|
44
|
-
:createNullableUnit, callback([], :pointer),
|
|
45
|
-
:createNullableUByte, callback([:pointer], :pointer),
|
|
46
|
-
:getNonNullValueOfUByte, callback([:pointer], :pointer),
|
|
47
|
-
:createNullableUShort, callback([:pointer], :pointer),
|
|
48
|
-
:getNonNullValueOfUShort, callback([:pointer], :pointer),
|
|
49
|
-
:createNullableUInt, callback([:pointer], :pointer),
|
|
50
|
-
:getNonNullValueOfUInt, callback([:pointer], :pointer),
|
|
51
|
-
:createNullableULong, callback([:pointer], :pointer),
|
|
52
|
-
:getNonNullValueOfULong, callback([:pointer], :pointer),
|
|
53
|
-
|
|
54
|
-
:kotlin, Kotlin
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
attach_function :libevaluation_interop_symbols, [], Libevaluation_interop_ExportedSymbols.by_ref
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def evaluation(rule_json, context_json)
|
|
61
|
-
lib = EvaluationInterop.libevaluation_interop_symbols()
|
|
62
|
-
evaluate = lib[:kotlin][:root][:evaluate]
|
|
63
|
-
dispose = lib[:DisposeString]
|
|
64
|
-
result_raw = evaluate.call(rule_json, context_json)
|
|
65
|
-
result_json = result_raw.read_string
|
|
66
|
-
result = JSON.parse(result_json)
|
|
67
|
-
dispose.call(result_raw)
|
|
68
|
-
|
|
69
|
-
if result["error"] != nil
|
|
70
|
-
raise "#{result["error"]}"
|
|
71
|
-
elsif result["result"] == nil
|
|
72
|
-
raise "Evaluation result is nil."
|
|
73
|
-
end
|
|
74
|
-
result["result"]
|
|
75
|
-
end
|
|
76
|
-
# rubocop:disable all
|
|
Binary file
|