eppo-server-sdk 0.3.0 → 3.1.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.
@@ -1,238 +1,192 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'singleton'
4
- require 'time'
3
+ require "singleton"
4
+ require "logger"
5
5
 
6
- require_relative 'constants'
7
- require_relative 'custom_errors'
8
- require_relative 'rules'
9
- require_relative 'shard'
10
- require_relative 'validation'
11
- require_relative 'variation_type'
6
+ require_relative "config"
7
+ require_relative "eppo_client"
12
8
 
13
9
  module EppoClient
14
10
  # The main client singleton
15
- # rubocop:disable Metrics/ClassLength
16
11
  class Client
17
- extend Gem::Deprecate
18
12
  include Singleton
19
- attr_accessor :config_requestor, :assignment_logger, :poller
13
+ attr_accessor :assignment_logger
20
14
 
21
- def instance
22
- Client.instance
15
+ def init(config)
16
+ config.validate
17
+
18
+ if !@core.nil? then
19
+ STDERR.puts "Eppo Warning: multiple initialization of the client"
20
+ @core.shutdown
21
+ end
22
+
23
+ @assignment_logger = config.assignment_logger
24
+ @core = EppoClient::Core::Client.new(config)
23
25
  end
24
26
 
25
- def get_string_assignment(
26
- subject_key,
27
- flag_key,
28
- subject_attributes = {},
29
- log_level = EppoClient::DEFAULT_LOGGER_LEVEL
30
- )
31
- logger = Logger.new($stdout)
32
- logger.level = log_level
33
- assigned_variation = get_assignment_variation(
34
- subject_key, flag_key, subject_attributes,
35
- EppoClient::VariationType::STRING_TYPE, logger
36
- )
37
- assigned_variation&.typed_value
38
- end
39
-
40
- def get_numeric_assignment(
41
- subject_key,
42
- flag_key,
43
- subject_attributes = {},
44
- log_level = EppoClient::DEFAULT_LOGGER_LEVEL
45
- )
46
- logger = Logger.new($stdout)
47
- logger.level = log_level
48
- assigned_variation = get_assignment_variation(
49
- subject_key, flag_key, subject_attributes,
50
- EppoClient::VariationType::NUMERIC_TYPE, logger
51
- )
52
- assigned_variation&.typed_value
53
- end
54
-
55
- def get_boolean_assignment(
56
- subject_key,
57
- flag_key,
58
- subject_attributes = {},
59
- log_level = EppoClient::DEFAULT_LOGGER_LEVEL
60
- )
61
- logger = Logger.new($stdout)
62
- logger.level = log_level
63
- assigned_variation = get_assignment_variation(
64
- subject_key, flag_key, subject_attributes,
65
- EppoClient::VariationType::BOOLEAN_TYPE, logger
66
- )
67
- assigned_variation&.typed_value
68
- end
69
-
70
- def get_parsed_json_assignment(
71
- subject_key,
72
- flag_key,
73
- subject_attributes = {},
74
- log_level = EppoClient::DEFAULT_LOGGER_LEVEL
75
- )
76
- logger = Logger.new($stdout)
77
- logger.level = log_level
78
- assigned_variation = get_assignment_variation(
79
- subject_key, flag_key, subject_attributes,
80
- EppoClient::VariationType::JSON_TYPE, logger
81
- )
82
- assigned_variation&.typed_value
83
- end
84
-
85
- def get_json_string_assignment(
86
- subject_key,
87
- flag_key,
88
- subject_attributes = {},
89
- log_level = EppoClient::DEFAULT_LOGGER_LEVEL
90
- )
91
- logger = Logger.new($stdout)
92
- logger.level = log_level
93
- assigned_variation = get_assignment_variation(
94
- subject_key, flag_key, subject_attributes,
95
- EppoClient::VariationType::JSON_TYPE, logger
96
- )
97
- assigned_variation&.value
98
- end
99
-
100
- def get_assignment(
101
- subject_key,
102
- flag_key,
103
- subject_attributes = {},
104
- log_level = EppoClient::DEFAULT_LOGGER_LEVEL
105
- )
27
+ def shutdown
28
+ @core.shutdown
29
+ end
30
+
31
+ def get_string_assignment(flag_key, subject_key, subject_attributes, default_value)
32
+ get_assignment_inner(flag_key, subject_key, subject_attributes, "STRING", default_value)
33
+ end
34
+
35
+ def get_numeric_assignment(flag_key, subject_key, subject_attributes, default_value)
36
+ get_assignment_inner(flag_key, subject_key, subject_attributes, "NUMERIC", default_value)
37
+ end
38
+
39
+ def get_integer_assignment(flag_key, subject_key, subject_attributes, default_value)
40
+ get_assignment_inner(flag_key, subject_key, subject_attributes, "INTEGER", default_value)
41
+ end
42
+
43
+ def get_boolean_assignment(flag_key, subject_key, subject_attributes, default_value)
44
+ get_assignment_inner(flag_key, subject_key, subject_attributes, "BOOLEAN", default_value)
45
+ end
46
+
47
+ def get_json_assignment(flag_key, subject_key, subject_attributes, default_value)
48
+ get_assignment_inner(flag_key, subject_key, subject_attributes, "JSON", default_value)
49
+ end
50
+
51
+ def get_string_assignment_details(flag_key, subject_key, subject_attributes, default_value)
52
+ get_assignment_details_inner(flag_key, subject_key, subject_attributes, "STRING", default_value)
53
+ end
54
+
55
+ def get_numeric_assignment_details(flag_key, subject_key, subject_attributes, default_value)
56
+ get_assignment_details_inner(flag_key, subject_key, subject_attributes, "NUMERIC", default_value)
57
+ end
58
+
59
+ def get_integer_assignment_details(flag_key, subject_key, subject_attributes, default_value)
60
+ get_assignment_details_inner(flag_key, subject_key, subject_attributes, "INTEGER", default_value)
61
+ end
62
+
63
+ def get_boolean_assignment_details(flag_key, subject_key, subject_attributes, default_value)
64
+ get_assignment_details_inner(flag_key, subject_key, subject_attributes, "BOOLEAN", default_value)
65
+ end
66
+
67
+ def get_json_assignment_details(flag_key, subject_key, subject_attributes, default_value)
68
+ get_assignment_details_inner(flag_key, subject_key, subject_attributes, "JSON", default_value)
69
+ end
70
+
71
+ def get_bandit_action(flag_key, subject_key, subject_attributes, actions, default_variation)
72
+ attributes = coerce_context_attributes(subject_attributes)
73
+ actions = actions.to_h { |action, attributes| [action, coerce_context_attributes(attributes)] }
74
+ result = @core.get_bandit_action(flag_key, subject_key, attributes, actions, default_variation)
75
+
76
+ log_assignment(result[:assignment_event])
77
+ log_bandit_action(result[:bandit_event])
78
+
79
+ return {:variation => result[:variation], :action => result[:action]}
80
+ end
81
+
82
+ def get_bandit_action_details(flag_key, subject_key, subject_attributes, actions, default_variation)
83
+ attributes = coerce_context_attributes(subject_attributes)
84
+ actions = actions.to_h { |action, attributes| [action, coerce_context_attributes(attributes)] }
85
+ result, details = @core.get_bandit_action_details(flag_key, subject_key, attributes, actions, default_variation)
86
+
87
+ log_assignment(result[:assignment_event])
88
+ log_bandit_action(result[:bandit_event])
89
+
90
+ return {
91
+ :variation => result[:variation],
92
+ :action => result[:action],
93
+ :evaluationDetails => details
94
+ }
95
+ end
96
+
97
+ private
98
+
99
+ # rubocop:disable Metrics/MethodLength
100
+ def get_assignment_inner(flag_key, subject_key, subject_attributes, expected_type, default_value)
106
101
  logger = Logger.new($stdout)
107
- logger.level = log_level
108
- assigned_variation = get_assignment_variation(subject_key, flag_key,
109
- subject_attributes, nil,
110
- logger)
111
- assigned_variation&.value
112
- end
113
- deprecate :get_assignment, 'the get_<typed>_assignment methods', 2024, 1
114
-
115
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
116
- def get_assignment_variation(
117
- subject_key,
118
- flag_key,
119
- subject_attributes,
120
- expected_variation_type,
121
- logger
122
- )
123
- EppoClient.validate_not_blank('subject_key', subject_key)
124
- EppoClient.validate_not_blank('flag_key', flag_key)
125
- experiment_config = @config_requestor.get_configuration(flag_key)
126
- override = get_subject_variation_override(experiment_config, subject_key)
127
- unless override.nil?
128
- unless expected_variation_type.nil?
129
- variation_is_expected_type =
130
- EppoClient::VariationType.expected_type?(
131
- override, expected_variation_type
132
- )
133
- return nil unless variation_is_expected_type
102
+ begin
103
+ assignment = @core.get_assignment(flag_key, subject_key, subject_attributes, expected_type)
104
+ if not assignment then
105
+ return default_value
134
106
  end
135
- return override
136
- end
137
107
 
138
- if experiment_config.nil? || experiment_config.enabled == false
139
- logger.debug(
140
- '[Eppo SDK] No assigned variation. No active experiment or flag for '\
141
- "key: #{flag_key}"
142
- )
143
- return nil
144
- end
108
+ log_assignment(assignment[:event])
145
109
 
146
- matched_rule = EppoClient.find_matching_rule(subject_attributes, experiment_config.rules)
147
- if matched_rule.nil?
148
- logger.debug(
149
- '[Eppo SDK] No assigned variation. Subject attributes do not match '\
150
- "targeting rules: #{subject_attributes}"
151
- )
152
- return nil
153
- end
110
+ return assignment[:value][:value]
111
+ rescue StandardError => error
112
+ logger.debug("[Eppo SDK] Failed to get assignment: #{error}")
154
113
 
155
- allocation = experiment_config.allocations[matched_rule.allocation_key]
156
- unless in_experiment_sample?(
157
- subject_key,
158
- flag_key,
159
- experiment_config.subject_shards,
160
- allocation.percent_exposure
161
- )
162
- logger.debug(
163
- '[Eppo SDK] No assigned variation. Subject is not part of experiment'\
164
- ' sample population'
165
- )
166
- return nil
114
+ # TODO: non-graceful mode?
115
+ default_value
167
116
  end
117
+ end
118
+ # rubocop:enable Metrics/MethodLength
168
119
 
169
- shard = EppoClient.get_shard(
170
- "assignment-#{subject_key}-#{flag_key}",
171
- experiment_config.subject_shards
172
- )
173
- assigned_variation = allocation.variations.find do |var|
174
- var.shard_range.shard_in_range?(shard)
120
+ # rubocop:disable Metrics/MethodLength
121
+ def get_assignment_details_inner(flag_key, subject_key, subject_attributes, expected_type, default_value)
122
+ result, event = @core.get_assignment_details(flag_key, subject_key, subject_attributes, expected_type)
123
+ log_assignment(event)
124
+
125
+ if not result[:variation] then
126
+ result[:variation] = default_value
127
+ else
128
+ # unwrap from AssignmentValue to untyped value
129
+ result[:variation] = result[:variation][:value]
175
130
  end
176
131
 
177
- assigned_variation_value_to_log = nil
178
- unless assigned_variation.nil?
179
- assigned_variation_value_to_log = assigned_variation.value
180
- unless expected_variation_type.nil?
181
- variation_is_expected_type = EppoClient::VariationType.expected_type?(
182
- assigned_variation, expected_variation_type
183
- )
184
- return nil unless variation_is_expected_type
185
- end
186
- end
132
+ return result
133
+ end
134
+ # rubocop:enable Metrics/MethodLength
187
135
 
188
- assignment_event = {
189
- "allocation": matched_rule.allocation_key,
190
- "experiment": "#{flag_key}-#{matched_rule.allocation_key}",
191
- "featureFlag": flag_key,
192
- "variation": assigned_variation_value_to_log,
193
- "subject": subject_key,
194
- "timestamp": Time.now.utc.iso8601,
195
- "subjectAttributes": subject_attributes
196
- }
136
+ def log_assignment(event)
137
+ if not event then return end
197
138
 
139
+ # Because rust's AssignmentEvent has a #[flatten] extra_logging
140
+ # field, serde_magnus serializes it as a normal HashMap with
141
+ # string keys.
142
+ #
143
+ # Convert keys to symbols here, so that logger sees symbol-keyed
144
+ # events for both flag assignment and bandit actions.
145
+ event = event.to_h { |key, value| [key.to_sym, value]}
146
+
147
+ enrich_event_metadata(event)
198
148
  begin
199
- @assignment_logger.log_assignment(assignment_event)
200
- rescue EppoClient::AssignmentLoggerError => e
201
- # Error means log_assignment was not set up. This is okay to ignore.
202
- rescue StandardError => e
203
- logger.error("[Eppo SDK] Error logging assignment event: #{e}")
149
+ @assignment_logger.log_assignment(event)
150
+ rescue EppoClient::AssignmentLoggerError
151
+ # Error means log_assignment was not set up. This is okay to ignore.
152
+ rescue StandardError => error
153
+ logger = Logger.new($stdout)
154
+ logger.error("[Eppo SDK] Error logging assignment event: #{error}")
204
155
  end
205
-
206
- assigned_variation
207
156
  end
208
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
209
157
 
210
- def shutdown
211
- @poller.stop
212
- end
158
+ def log_bandit_action(event)
159
+ if not event then return end
213
160
 
214
- # rubocop:disable Metrics/MethodLength
215
- def get_subject_variation_override(experiment_config, subject)
216
- subject_hash = Digest::MD5.hexdigest(subject.to_s)
217
- if experiment_config&.overrides &&
218
- experiment_config.overrides[subject_hash] &&
219
- experiment_config.typed_overrides[subject_hash]
220
- EppoClient::VariationDto.new(
221
- 'override',
222
- experiment_config.overrides[subject_hash],
223
- experiment_config.typed_overrides[subject_hash],
224
- EppoClient::ShardRange.new(0, 1000)
225
- )
161
+ enrich_event_metadata(event)
162
+ begin
163
+ @assignment_logger.log_bandit_action(event)
164
+ rescue EppoClient::AssignmentLoggerError
165
+ # Error means log_assignment was not set up. This is okay to ignore.
166
+ rescue StandardError => error
167
+ logger = Logger.new($stdout)
168
+ logger.error("[Eppo SDK] Error logging bandit action event: #{error}")
226
169
  end
227
170
  end
228
- # rubocop:enable Metrics/MethodLength
229
171
 
230
- def in_experiment_sample?(subject, experiment_key, subject_shards,
231
- percent_exposure)
232
- shard = EppoClient.get_shard("exposure-#{subject}-#{experiment_key}",
233
- subject_shards)
234
- shard <= percent_exposure * subject_shards
172
+ def enrich_event_metadata(event)
173
+ event[:metaData]["sdkName"] = "ruby"
174
+ event[:metaData]["sdkVersion"] = EppoClient::VERSION
175
+ end
176
+
177
+ def coerce_context_attributes(attributes)
178
+ numeric_attributes = attributes[:numeric_attributes] || attributes["numericAttributes"]
179
+ categorical_attributes = attributes[:categorical_attributes] || attributes["categoricalAttributes"]
180
+ if numeric_attributes || categorical_attributes then
181
+ {
182
+ numericAttributes: numeric_attributes.to_h do |key, value|
183
+ value.is_a?(Numeric) ? [key, value] : [nil, nil]
184
+ end.compact,
185
+ categoricalAttributes: categorical_attributes.to_h do |key, value|
186
+ value.nil? ? [nil, nil] : [key, value.to_s]
187
+ end.compact,
188
+ }
189
+ end
235
190
  end
236
191
  end
237
- # rubocop:enable Metrics/ClassLength
238
192
  end
@@ -1,21 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'validation'
4
- require_relative 'assignment_logger'
3
+ require_relative "validation"
4
+ require_relative "assignment_logger"
5
5
 
6
6
  module EppoClient
7
7
  # The class for configuring the Eppo client singleton
8
8
  class Config
9
9
  attr_reader :api_key, :assignment_logger, :base_url
10
10
 
11
- def initialize(api_key, assignment_logger: AssignmentLogger.new, base_url: 'https://fscdn.eppo.cloud/api')
11
+ def initialize(api_key, assignment_logger: AssignmentLogger.new, base_url: EppoClient::Core::DEFAULT_BASE_URL)
12
12
  @api_key = api_key
13
13
  @assignment_logger = assignment_logger
14
14
  @base_url = base_url
15
15
  end
16
16
 
17
17
  def validate
18
- EppoClient.validate_not_blank('api_key', @api_key)
18
+ EppoClient.validate_not_blank("api_key", @api_key)
19
19
  end
20
20
 
21
21
  # Hide instance variables (specifically api_key) from logs
@@ -8,23 +8,6 @@ module EppoClient
8
8
  end
9
9
  end
10
10
 
11
- # A custom error class for unauthorized requests
12
- class UnauthorizedError < StandardError
13
- def initialize(message)
14
- super("Unauthorized: #{message}")
15
- end
16
- end
17
-
18
- # A custom error class for HTTP requests
19
- class HttpRequestError < StandardError
20
- attr_reader :status_code
21
-
22
- def initialize(message, status_code)
23
- @status_code = status_code
24
- super("HttpRequestError: #{message}")
25
- end
26
- end
27
-
28
11
  # A custom error class for invalid values
29
12
  class InvalidValueError < StandardError
30
13
  def initialize(message)
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'custom_errors'
3
+ require_relative "custom_errors"
4
4
 
5
5
  # The helper module to validate keys
6
6
  module EppoClient
7
7
  module_function
8
8
 
9
9
  def validate_not_blank(field_name, field_value)
10
- (field_value.nil? || field_value == '') && raise(
10
+ (field_value.nil? || field_value == "") && raise(
11
11
  EppoClient::InvalidValueError, "#{field_name} cannot be blank"
12
12
  )
13
13
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # TODO: this version and ext/eppo_rb/Cargo.toml should be in sync
3
4
  module EppoClient
4
- VERSION = '0.3.0'
5
+ VERSION = "3.1.0"
5
6
  end
data/lib/eppo_client.rb CHANGED
@@ -1,53 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'eppo_client/assignment_logger'
4
- require_relative 'eppo_client/http_client'
5
- require_relative 'eppo_client/poller'
6
- require_relative 'eppo_client/config'
7
- require_relative 'eppo_client/client'
8
- require_relative 'eppo_client/constants'
9
- require_relative 'eppo_client/configuration_requestor'
10
- require_relative 'eppo_client/configuration_store'
11
- require_relative 'eppo_client/version'
3
+ require_relative "eppo_client/client"
4
+ require_relative "eppo_client/version"
12
5
 
13
- # This module scopes all the client SDK's classes and functions
6
+ # EppoClient is the main module for initializing the Eppo client.
7
+ # It provides a method to initialize the client with a given configuration.
14
8
  module EppoClient
15
- # rubocop:disable Metrics/MethodLength
16
- def initialize_client(config_requestor, assignment_logger)
17
- client = EppoClient::Client.instance
18
- !client.poller.nil? && client.shutdown
19
- client.config_requestor = config_requestor
20
- client.assignment_logger = assignment_logger
21
- client.poller = EppoClient::Poller.new(
22
- EppoClient::POLL_INTERVAL_MILLIS,
23
- EppoClient::POLL_JITTER_MILLIS,
24
- proc { client.config_requestor.fetch_and_store_configurations }
25
- )
26
- client.poller.start
27
- client
28
- end
29
- # rubocop:enable Metrics/MethodLength
30
-
31
- # rubocop:disable Metrics/MethodLength
32
9
  def init(config)
33
- config.validate
34
- sdk_params = EppoClient::SdkParams.new(config.api_key, 'ruby',
35
- EppoClient::VERSION)
36
- http_client = EppoClient::HttpClient.new(config.base_url,
37
- sdk_params.formatted)
38
- config_store = EppoClient::ConfigurationStore.new(
39
- EppoClient::MAX_CACHE_ENTRIES
40
- )
41
- config_store.lock.with_write_lock do
42
- EppoClient.initialize_client(
43
- EppoClient::ExperimentConfigurationRequestor.new(
44
- http_client, config_store
45
- ),
46
- config.assignment_logger
47
- )
48
- end
10
+ client = EppoClient::Client.instance
11
+ client.init(config)
49
12
  end
50
- # rubocop:enable Metrics/MethodLength
51
13
 
52
- module_function :init, :initialize_client
14
+ module_function :init
53
15
  end
@@ -0,0 +1,96 @@
1
+ # EppoClient is the main module for initializing the Eppo client.
2
+ # It provides a method to initialize the client with a given configuration.
3
+ module EppoClient
4
+ def self.init: (Config config) -> void
5
+
6
+ # The base assignment logger class to override
7
+ class AssignmentLogger
8
+ def log_assignment: (untyped assignment_event) -> void
9
+
10
+ def log_bandit_action: (untyped assignment_event) -> void
11
+ end
12
+
13
+ # The main client singleton
14
+ class Client
15
+ @assignment_logger: AssignmentLogger
16
+ @core: Core::Client
17
+
18
+ include Singleton
19
+
20
+ attr_accessor assignment_logger: AssignmentLogger
21
+
22
+ def self.instance: () -> Client
23
+
24
+ def init: (Config config) -> void
25
+
26
+ def shutdown: () -> void
27
+
28
+ def get_string_assignment: (String flag_key, String subject_key, Hash[String, untyped] subject_attributes, String default_value) -> String
29
+
30
+ def get_numeric_assignment: (String flag_key, String subject_key, Hash[String, untyped] subject_attributes, Numeric default_value) -> Numeric
31
+
32
+ def get_integer_assignment: (String flag_key, String subject_key, Hash[String, untyped] subject_attributes, Integer default_value) -> Integer
33
+
34
+ def get_boolean_assignment: (String flag_key, String subject_key, Hash[String, untyped] subject_attributes, bool default_value) -> bool
35
+
36
+ def get_json_assignment: (String flag_key, String subject_key, Hash[String, untyped] subject_attributes, Object default_value) -> Object
37
+
38
+ def get_bandit_action: (String flag_key, String subject_key, Hash[String, untyped] subject_attributes, Hash[String, untyped] actions, String default_variation) -> { variation: untyped, action: untyped }
39
+
40
+ private
41
+
42
+ # rubocop:disable Metrics/MethodLength
43
+ def get_assignment_inner: (untyped flag_key, String subject_key, untyped subject_attributes, untyped expected_type, untyped default_value) -> untyped
44
+
45
+ def log_assignment: (untyped event) -> void
46
+
47
+ def log_bandit_action: (untyped event) -> void
48
+
49
+ def enrich_event_metadata: (untyped event) -> void
50
+
51
+ def coerce_context_attributes: (untyped attributes) -> untyped
52
+ end
53
+
54
+ # The class for configuring the Eppo client singleton
55
+ class Config
56
+ @api_key: String
57
+ @assignment_logger: AssignmentLogger
58
+ @base_url: String
59
+
60
+ attr_reader api_key: String
61
+ attr_reader assignment_logger: AssignmentLogger
62
+ attr_reader base_url: String
63
+
64
+ def initialize: (String api_key, ?assignment_logger: AssignmentLogger, ?base_url: String) -> void
65
+
66
+ def validate: () -> void
67
+
68
+ # Hide instance variables (specifically api_key) from logs
69
+ def inspect: () -> ::String
70
+ end
71
+
72
+ # A custom error class for AssignmentLogger
73
+ class AssignmentLoggerError < StandardError
74
+ def initialize: (String message) -> void
75
+ end
76
+
77
+ # A custom error class for invalid values
78
+ class InvalidValueError < StandardError
79
+ def initialize: (String message) -> void
80
+ end
81
+
82
+ def self?.validate_not_blank: (String field_name, String field_value) -> void
83
+
84
+ VERSION: String
85
+ end
86
+
87
+ # Exposed from Rust
88
+ module EppoClient::Core
89
+ DEFAULT_BASE_URL: String
90
+ class Client
91
+ def self.new: (untyped config) -> Client
92
+ def shutdown: () -> void
93
+ def get_assignment: (String flag_key, String subject_key, untyped subject_attributes, String expected_type) -> untyped
94
+ def get_bandit_action: (String flag_key, String subject_key, untyped attributes, untyped actions, String default_variation) -> untyped
95
+ end
96
+ end