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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/amplitude-experiment.gemspec +5 -5
  3. data/lib/amplitude/processor.rb +1 -1
  4. data/lib/amplitude/timeline.rb +1 -1
  5. data/lib/amplitude-experiment.rb +6 -1
  6. data/lib/experiment/deployment/deployment_runner.rb +3 -3
  7. data/lib/experiment/evaluation/evaluation.rb +311 -0
  8. data/lib/experiment/evaluation/flag.rb +123 -0
  9. data/lib/experiment/evaluation/murmur3.rb +104 -0
  10. data/lib/experiment/evaluation/select.rb +16 -0
  11. data/lib/experiment/evaluation/semantic_version.rb +52 -0
  12. data/lib/experiment/evaluation/topological_sort.rb +56 -0
  13. data/lib/experiment/flag/flag_config_fetcher.rb +1 -1
  14. data/lib/experiment/flag/flag_config_storage.rb +1 -1
  15. data/lib/experiment/local/client.rb +8 -14
  16. data/lib/experiment/local/config.rb +21 -3
  17. data/lib/experiment/persistent_http_client.rb +1 -1
  18. data/lib/experiment/remote/client.rb +9 -12
  19. data/lib/experiment/remote/config.rb +31 -4
  20. data/lib/experiment/util/flag_config.rb +10 -10
  21. data/lib/experiment/version.rb +1 -1
  22. metadata +20 -24
  23. data/lib/experiment/local/evaluation/evaluation.rb +0 -76
  24. data/lib/experiment/local/evaluation/lib/linuxArm64/libevaluation_interop.so +0 -0
  25. data/lib/experiment/local/evaluation/lib/linuxArm64/libevaluation_interop_api.h +0 -110
  26. data/lib/experiment/local/evaluation/lib/linuxX64/libevaluation_interop.so +0 -0
  27. data/lib/experiment/local/evaluation/lib/linuxX64/libevaluation_interop_api.h +0 -110
  28. data/lib/experiment/local/evaluation/lib/macosArm64/libevaluation_interop.dylib +0 -0
  29. data/lib/experiment/local/evaluation/lib/macosArm64/libevaluation_interop_api.h +0 -110
  30. data/lib/experiment/local/evaluation/lib/macosX64/libevaluation_interop.dylib +0 -0
  31. data/lib/experiment/local/evaluation/lib/macosX64/libevaluation_interop_api.h +0 -110
  32. 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.
@@ -40,7 +40,7 @@ module AmplitudeExperiment
40
40
 
41
41
  def put_flag_config(flag_config)
42
42
  @flag_configs_lock.synchronize do
43
- @flag_configs[flag_config['key']] = flag_config
43
+ @flag_configs[flag_config.key] = flag_config
44
44
  end
45
45
  end
46
46
 
@@ -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 = AmplitudeExperiment.topological_sort(flags, flag_keys.to_set)
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: #{context_json} - Rules: #{flags}") if @config.debug
78
- result = evaluation(flags_json, context_json)
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['key']} dependent on cohorts #{cohort_ids_str} without #{missing_cohorts_str} in storage"
110
+ "Evaluating flag #{flag.key} dependent on cohorts #{cohort_ids_str} without #{missing_cohorts_str} in storage"
117
111
  else
118
- "Evaluating flag #{flag['key']} dependent on cohorts #{cohort_ids_str} without cohort syncing configured"
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, server_zone: ServerZone::US, bootstrap: {},
45
- flag_config_polling_interval_millis: 30_000, debug: false, assignment_config: nil,
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
- @debug = debug || false
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 = Logger.new($stdout)
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] timeout_millis
127
- def do_fetch(user, timeout_millis)
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
- read_timeout = timeout_millis.to_f / 1000 if (timeout_millis.to_f / 1000) > 0
135
- http = PersistentHttpClient.get(@uri, { read_timeout: read_timeout }, @api_key)
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, server_url: DEFAULT_SERVER_URL, fetch_timeout_millis: 10_000, fetch_retries: 0,
53
- fetch_retry_backoff_min_millis: 500, fetch_retry_backoff_max_millis: 10_000,
54
- fetch_retry_backoff_scalar: 1.5, fetch_retry_timeout_millis: 10_000)
55
- @debug = debug
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['op']) &&
4
- condition['selector'] &&
5
- condition['selector'][-1] == 'cohort_ids'
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['conditions'] || []
10
+ conditions = segment.conditions || []
11
11
  conditions.each do |condition|
12
12
  condition = condition[0]
13
- next unless cohort_filter?(condition) && (condition['selector'][1].length > 2)
13
+ next unless cohort_filter?(condition) && (condition.selector[1].length > 2)
14
14
 
15
- context_subtype = condition['selector'][1]
15
+ context_subtype = condition.selector[1]
16
16
  group_type =
17
17
  if context_subtype == 'user'
18
18
  USER_GROUP_TYPE
19
- elsif condition['selector'].include?('groups')
20
- condition['selector'][2]
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['values'])
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['segments'] || []
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
@@ -1,3 +1,3 @@
1
1
  module AmplitudeExperiment
2
- VERSION = '1.5.0'.freeze
2
+ VERSION = '1.7.0'.freeze
3
3
  end
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.5.0
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: 2024-08-27 00:00:00.000000000 Z
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: :development
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.4'
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.4'
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: ffi
154
+ name: jar-dependencies
155
155
  requirement: !ruby/object:Gem::Requirement
156
156
  requirements:
157
- - - "~>"
157
+ - - '='
158
158
  - !ruby/object:Gem::Version
159
- version: '1.15'
160
- type: :runtime
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: '1.15'
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