-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathflagsmith.rb
More file actions
359 lines (298 loc) · 12.2 KB
/
flagsmith.rb
File metadata and controls
359 lines (298 loc) · 12.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
# frozen_string_literal: true
require 'faraday'
require 'faraday/retry'
# Hash#slice was added in ruby version 2.5
# This is the patch to use slice in earler versions
require 'flagsmith/hash_slice'
require 'flagsmith/version'
require 'flagsmith/sdk/analytics_processor'
require 'flagsmith/sdk/api_client'
require 'flagsmith/sdk/config'
require 'flagsmith/sdk/errors'
require 'flagsmith/sdk/intervals'
require 'flagsmith/sdk/utils'
require 'flagsmith/sdk/pooling_manager'
require 'flagsmith/sdk/models/flags'
require 'flagsmith/sdk/models/segments'
require 'flagsmith/sdk/offline_handlers'
require 'flagsmith/sdk/realtime_client'
require 'flagsmith/engine/core'
# no-doc
module Flagsmith
# Ruby client for flagsmith.com
class Client # rubocop:disable Metrics/ClassLength
extend Forwardable
# A Flagsmith client.
#
# Provides an interface for interacting with the Flagsmith http API.
# Basic Usage::
#
# flagsmith = Flagsmith::Client.new(environment_key: '<your API key>')
#
# environment_flags = flagsmith.get_environment_flags
# feature_enabled = environment_flags.is_feature_enabled('foo')
# feature_value = identity_flags.get_feature_value('foo')
#
# identity_flags = flagsmith.get_identity_flags('identifier', {'foo': 'bar'})
# feature_enabled_for_identity = identity_flags.is_feature_enabled('foo')
# feature_value_for_identity = identity_flags.get_feature_value('foo')
#
# identity_segments = flagsmith.get_identity_segments('identifier', {'foo': 'bar'})
# Available Configs.
#
# :environment_key, :api_url, :custom_headers, :request_timeout_seconds, :enable_local_evaluation,
# :environment_refresh_interval_seconds, :retries, :enable_analytics, :default_flag_handler,
# :offline_mode, :offline_handler, :polling_manager_failure_limit
# :realtime_api_url, :enable_realtime_updates, :logger
#
# You can see full description in the Flagsmith::Config
attr_reader :config, :environment, :identity_overrides_by_identifier
delegate Flagsmith::Config::OPTIONS => :@config
def initialize(config)
@_mutex = Mutex.new
@config = Flagsmith::Config.new(config)
@identity_overrides_by_identifier = {}
validate_offline_mode!
validate_realtime_mode!
api_client
analytics_processor
environment_data_polling_manager
load_offline_handler
end
def validate_offline_mode!
if @config.offline_mode? && !@config.offline_handler
raise Flagsmith::ClientError,
'The offline_mode config param requires a matching offline_handler.'
end
return unless @config.offline_handler && @config.default_flag_handler
raise Flagsmith::ClientError,
'Cannot use offline_handler and default_flag_handler at the same time.'
end
def validate_realtime_mode!
return unless @config.realtime_mode? && !@config.local_evaluation?
raise Flagsmith::ClientError,
'The enable_realtime_updates config param requires a matching enable_local_evaluation param.'
end
def api_client
@api_client ||= Flagsmith::ApiClient.new(@config)
end
def realtime_client
@realtime_client ||= Flagsmith::RealtimeClient.new(@config)
end
def analytics_processor
return nil unless @config.enable_analytics?
@analytics_processor ||=
Flagsmith::AnalyticsProcessor.new(
api_client: api_client,
timeout: request_timeout_seconds,
logger: @config.logger
)
end
def load_offline_handler
@environment = offline_handler.environment if offline_handler
end
def environment_data_polling_manager
return nil unless @config.local_evaluation?
# Bypass the environment data polling manager if realtime
# is present in the configuration.
if @config.realtime_mode?
update_environment
realtime_client.listen self unless realtime_client.running
return
end
update_environment if @environment_data_polling_manager.nil?
@environment_data_polling_manager ||= Flagsmith::EnvironmentDataPollingManager.new(
self, environment_refresh_interval_seconds, @config.polling_manager_failure_limit
).tap(&:start)
end
# Updates the environment state for local flag evaluation.
# You only need to call this if you wish to bypass environment_refresh_interval_seconds.
def update_environment
@_mutex.synchronize { @environment = environment_from_api }
update_identity_overrides
end
def update_identity_overrides
return unless @environment
@identity_overrides_by_identifier = {}
@environment.identity_overrides.each do |identity|
@identity_overrides_by_identifier[identity.identifier] = identity
end
end
def environment_from_api
environment_data = api_client.get(@config.environment_url).body
Flagsmith::Engine::Environment.build(environment_data)
end
# Get all the default for flags for the current environment.
# @returns Flags object holding all the flags for the current environment.
def get_environment_flags # rubocop:disable Naming/AccessorMethodName
return environment_flags_from_document if @config.local_evaluation? || @config.offline_mode
environment_flags_from_api
end
# Get all the flags for the current environment for a given identity. Will also
# upsert all traits to the Flagsmith API for future evaluations. Providing a
# trait with a value of None will remove the trait from the identity if it exists.
#
# identifier a unique identifier for the identity in the current
# environment, e.g. email address, username, uuid
# traits { key => value } is a dictionary of traits to add / update on the identity in
# Flagsmith, e.g. { "num_orders": 10 }
# in lieu of a trait value, a trait coniguration dictionary can be provided,
# e.g. { "num_orders": { "value": 10, "transient": true } }
# returns Flags object holding all the flags for the given identity.
def get_identity_flags(identifier, transient = false, **traits) # rubocop:disable Style/OptionalBooleanParameter
return get_identity_flags_from_document(identifier, traits) if environment
get_identity_flags_from_api(identifier, traits, transient)
end
def feature_enabled?(feature_name, default: false)
flag = get_environment_flags[feature_name]
return default if flag.nil?
flag.enabled?
end
def feature_enabled_for_identity?(feature_name, user_id, default: false)
flag = get_identity_flags(user_id)[feature_name]
return default if flag.nil?
flag.enabled?
end
def get_value(feature_name, default: nil)
flag = get_environment_flags[feature_name]
return default if flag.nil?
flag.value
end
def get_value_for_identity(feature_name, user_id = nil, default: nil)
flag = get_identity_flags(user_id)[feature_name]
return default if flag.nil?
flag.value
end
def get_identity_segments(identifier, traits = {})
raise Flagsmith::ClientError, 'Local evaluation or offline handler is required to obtain identity segments.' unless environment
identity_model = get_identity_model(identifier, traits)
context = Flagsmith::Engine::Mappers.get_evaluation_context(environment, identity_model)
raise Flagsmith::ClientError, 'Local evaluation required to obtain identity segments' unless context
evaluation_result = Flagsmith::Engine.get_evaluation_result(context)
evaluation_result[:segments].filter_map do |segment_result|
id = segment_result.dig(:metadata, :id)
Flagsmith::Segments::Segment.new(id: id, name: segment_result[:name]) if id
end
end
private
def environment_flags_from_document # rubocop:disable Metrics/MethodLength
context = Flagsmith::Engine::Mappers.get_evaluation_context(environment)
unless context
raise Flagsmith::ClientError,
'Unable to get flags. No environment present.'
end
evaluation_result = Flagsmith::Engine.get_evaluation_result(context)
Flagsmith::Flags::Collection.from_evaluation_result(
evaluation_result,
analytics_processor: analytics_processor,
default_flag_handler: default_flag_handler,
offline_handler: offline_handler
)
end
def get_identity_flags_from_document(identifier, traits = {})
identity_model = get_identity_model(identifier, traits)
context = Flagsmith::Engine::Mappers.get_evaluation_context(environment, identity_model)
raise Flagsmith::ClientError, 'Unable to get flags. No environment present.' unless context
evaluation_result = Flagsmith::Engine.get_evaluation_result(context)
Flagsmith::Flags::Collection.from_evaluation_result(
evaluation_result,
analytics_processor: analytics_processor, default_flag_handler: default_flag_handler,
offline_handler: offline_handler
)
end
# rubocop:disable Metrics/MethodLength
def environment_flags_from_api
if offline_handler
begin
process_environment_flags_from_api
rescue StandardError
environment_flags_from_document
end
else
begin
process_environment_flags_from_api
rescue StandardError
if default_flag_handler
return Flagsmith::Flags::Collection.new(
{},
default_flag_handler: default_flag_handler
)
end
raise
end
end
end
# rubocop:enable Metrics/MethodLength
def process_environment_flags_from_api
api_flags = api_client.get(@config.environment_flags_url).body
api_flags = api_flags.select { |flag| flag[:feature_segment].nil? }
Flagsmith::Flags::Collection.from_api(
api_flags,
analytics_processor: analytics_processor,
default_flag_handler: default_flag_handler,
offline_handler: offline_handler
)
end
# rubocop:disable Metrics/MethodLength
def get_identity_flags_from_api(identifier, traits, transient)
if offline_handler
begin
process_identity_flags_from_api(identifier, traits, transient)
rescue StandardError
get_identity_flags_from_document(identifier, traits)
end
else
begin
process_identity_flags_from_api(identifier, traits, transient)
rescue StandardError
if default_flag_handler
return Flagsmith::Flags::Collection.new(
{},
default_flag_handler: default_flag_handler
)
end
raise
end
end
end
# rubocop:enable Metrics/MethodLength
def process_identity_flags_from_api(identifier, traits, transient)
data = generate_identities_data(identifier, traits, transient)
json_response = api_client.post(@config.identities_url, data.to_json).body
Flagsmith::Flags::Collection.from_api(
json_response[:flags],
analytics_processor: analytics_processor,
default_flag_handler: default_flag_handler,
offline_handler: offline_handler
)
end
# rubocop:disable Metrics/MethodLength
def get_identity_model(identifier, traits = {})
unless environment
raise Flagsmith::ClientError,
'Unable to get identity model when no local environment present.'
end
trait_models = traits.map do |key, value|
Flagsmith::Engine::Identities::Trait.new(trait_key: key, trait_value: value)
end
if identity_overrides_by_identifier.key? identifier
identity = identity_overrides_by_identifier[identifier]
identity.update_traits trait_models
return identity
end
Flagsmith::Engine::Identity.new(
identity_traits: trait_models, environment_api_key: environment_key, identifier: identifier
)
end
# rubocop:enable Metrics/MethodLength
def generate_identities_data(identifier, traits, transient)
{
identifier: identifier,
transient: transient,
traits: traits.map do |key, value|
value.is_a?(Hash) ? { trait_key: key, trait_value: value[:value], transient: value[:transient] || false } : { trait_key: key, trait_value: value }
end
}
end
end
end