eppo-server-sdk 0.3.0 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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