flagsmith 4.1.1 → 4.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8b908dee8f4935d4c754400d2e314e8a61e545dc242490ca396704f6b00ef1e1
4
- data.tar.gz: 4c9203ad57f71fb3699b78ccb9a50aa7b4c89eaf1433803d0c16e3b7644a91c3
3
+ metadata.gz: 0d20d6b2aee787d2d25478b00eea5f1e06ad4f345097102b1381faf7455774d2
4
+ data.tar.gz: c782d13647ae7c5905e0a0b46cb0085a395adbb71c95101e0b424b719e2b1a52
5
5
  SHA512:
6
- metadata.gz: a41d33186ccd7bf6b7405a123186888ea9aef9e0890762b2384d1681b897ceed602e471eb4903190bf09b0c1c48a6b99a831053f10dde55cbcd6e9875851dfde
7
- data.tar.gz: e654da93bb040ce2a08fa0cb6b15d167a2b557853fec1e8deb3cd45df124177998d7fb40e6ccae777cf8e62b0cdc72efd0cbd3a8e6fd4fcd894ed2b23d228c90
6
+ metadata.gz: 135da46a33bde44fb78a54c769dc25caab7566b18c68a870306ef645cad2de6de75ee599af0fd851fa3a3562bae92bec57e57089f31fa4f9cb282fadf47df01a
7
+ data.tar.gz: ead1e0df360be3a4637892f8fec1c10415967a072604b42dc113ee5034a06a235294300de59014ab35c829aceb46acca9454f29138e02dd610fd97abaadae293
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- flagsmith (4.1.1)
4
+ flagsmith (4.3.0)
5
5
  faraday (>= 2.0.1)
6
6
  faraday-retry
7
7
  semantic
@@ -39,8 +39,7 @@ GEM
39
39
  rainbow (3.1.1)
40
40
  rake (13.1.0)
41
41
  regexp_parser (2.9.0)
42
- rexml (3.2.8)
43
- strscan (>= 3.0.9)
42
+ rexml (3.3.9)
44
43
  rspec (3.12.0)
45
44
  rspec-core (~> 3.12.0)
46
45
  rspec-expectations (~> 3.12.0)
@@ -69,7 +68,6 @@ GEM
69
68
  parser (>= 3.2.1.0)
70
69
  ruby-progressbar (1.13.0)
71
70
  semantic (1.6.1)
72
- strscan (3.1.0)
73
71
  unicode-display_width (2.5.0)
74
72
  uri (0.13.0)
75
73
 
@@ -37,15 +37,15 @@ module Flagsmith
37
37
 
38
38
  MATCHING_FUNCTIONS = {
39
39
  EQUAL => ->(other_value, self_value) { other_value == self_value },
40
- GREATER_THAN => ->(other_value, self_value) { other_value > self_value },
41
- GREATER_THAN_INCLUSIVE => ->(other_value, self_value) { other_value >= self_value },
42
- LESS_THAN => ->(other_value, self_value) { other_value < self_value },
43
- LESS_THAN_INCLUSIVE => ->(other_value, self_value) { other_value <= self_value },
40
+ GREATER_THAN => ->(other_value, self_value) { (other_value || false) && other_value > self_value },
41
+ GREATER_THAN_INCLUSIVE => ->(other_value, self_value) { (other_value || false) && other_value >= self_value },
42
+ LESS_THAN => ->(other_value, self_value) { (other_value || false) && other_value < self_value },
43
+ LESS_THAN_INCLUSIVE => ->(other_value, self_value) { (other_value || false) && other_value <= self_value },
44
44
  NOT_EQUAL => ->(other_value, self_value) { other_value != self_value },
45
- CONTAINS => ->(other_value, self_value) { other_value.include? self_value },
45
+ CONTAINS => ->(other_value, self_value) { (other_value || false) && other_value.include?(self_value) },
46
46
 
47
- NOT_CONTAINS => ->(other_value, self_value) { !other_value.include? self_value },
48
- REGEX => ->(other_value, self_value) { other_value.match? self_value }
47
+ NOT_CONTAINS => ->(other_value, self_value) { (other_value || false) && !other_value.include?(self_value) },
48
+ REGEX => ->(other_value, self_value) { (other_value || false) && other_value.match?(self_value) }
49
49
  }.freeze
50
50
 
51
51
  def initialize(operator:, value:, property: nil)
@@ -4,10 +4,13 @@ module Flagsmith
4
4
  # Config options shared around Engine
5
5
  class Config
6
6
  DEFAULT_API_URL = 'https://edge.api.flagsmith.com/api/v1/'
7
+ DEFAULT_REALTIME_API_URL = 'https://realtime.flagsmith.com/'
8
+
7
9
  OPTIONS = %i[
8
10
  environment_key api_url custom_headers request_timeout_seconds enable_local_evaluation
9
11
  environment_refresh_interval_seconds retries enable_analytics default_flag_handler
10
- offline_mode offline_handler polling_manager_failure_limit logger
12
+ offline_mode offline_handler polling_manager_failure_limit
13
+ realtime_api_url enable_realtime_updates logger
11
14
  ].freeze
12
15
 
13
16
  # Available Configs
@@ -40,6 +43,9 @@ module Flagsmith
40
43
  # the entire environment, project, flags, etc.
41
44
  # +polling_manager_failure_limit+ - An integer to control how long to suppress errors in
42
45
  # the polling manager for local evaluation mode.
46
+ # +realtime_api_url+ - Override the realtime api URL to communicate with a
47
+ # non-standard realtime endpoint.
48
+ # +enable_realtime_updates+ - A boolean to enable realtime updates.
43
49
  # +logger+ - Pass your logger, default is Logger.new($stdout)
44
50
  #
45
51
  attr_reader(*OPTIONS)
@@ -62,6 +68,10 @@ module Flagsmith
62
68
  @offline_mode
63
69
  end
64
70
 
71
+ def realtime_mode?
72
+ @enable_realtime_updates
73
+ end
74
+
65
75
  def environment_flags_url
66
76
  'flags/'
67
77
  end
@@ -92,6 +102,9 @@ module Flagsmith
92
102
  @offline_mode = opts.fetch(:offline_mode, false)
93
103
  @offline_handler = opts[:offline_handler]
94
104
  @polling_manager_failure_limit = opts.fetch(:polling_manager_failure_limit, 10)
105
+ @realtime_api_url = opts.fetch(:realtime_api_url, Flagsmith::Config::DEFAULT_REALTIME_API_URL)
106
+ @realtime_api_url << '/' unless @realtime_api_url.end_with? '/'
107
+ @enable_realtime_updates = opts.fetch(:enable_realtime_updates, false)
95
108
  @logger = options.fetch(:logger, Logger.new($stdout).tap { |l| l.level = :debug })
96
109
  end
97
110
  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
@@ -71,7 +71,7 @@ module Flagsmith
71
71
  def from_api(json_flag_data)
72
72
  new(
73
73
  enabled: json_flag_data[:enabled],
74
- value: json_flag_data[:feature_state_value] || json_flag_data[:value],
74
+ value: json_flag_data.fetch(:feature_state_value) { json_flag_data[:value] },
75
75
  feature_name: json_flag_data.dig(:feature, :name),
76
76
  feature_id: json_flag_data.dig(:feature, :id)
77
77
  )
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require 'faraday'
5
+ require 'json'
6
+
7
+ module Flagsmith
8
+ # Ruby client for realtime access to flagsmith.com
9
+ class RealtimeClient
10
+ attr_accessor :running
11
+
12
+ def initialize(config)
13
+ @config = config
14
+ @thread = nil
15
+ @running = false
16
+ @main = nil
17
+ end
18
+
19
+ def endpoint
20
+ "#{@config.realtime_api_url}sse/environments/#{@main.environment.api_key}/stream"
21
+ end
22
+
23
+ def listen(main, remaining_attempts: Float::INFINITY, retry_interval: 0.5) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/MethodLength
24
+ last_updated_at = 0
25
+ @main = main
26
+ @running = true
27
+ @thread = Thread.new do
28
+ while @running && remaining_attempts.positive?
29
+ remaining_attempts -= 1
30
+ @config.logger.warn 'Beginning to pull down realtime endpoint'
31
+ begin
32
+ sleep retry_interval
33
+ # Open connection to SSE endpoint
34
+ Faraday.new(url: endpoint).get do |req|
35
+ req.options.timeout = nil # Keep connection alive indefinitely
36
+ req.options.open_timeout = 10
37
+ end.body.each_line do |line| # rubocop:disable Style/MultilineBlockChain
38
+ # SSE protocol: Skip non-event lines
39
+ next if line.strip.empty? || line.start_with?(':')
40
+
41
+ # Parse SSE fields
42
+ next unless line.start_with?('data: ')
43
+
44
+ data = JSON.parse(line[6..].strip)
45
+ updated_at = data['updated_at']
46
+ next unless updated_at > last_updated_at
47
+
48
+ @config.logger.info "Realtime updating environment from #{last_updated_at} to #{updated_at}"
49
+ @main.update_environment
50
+ last_updated_at = updated_at
51
+ end
52
+ rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
53
+ @config.logger.warn "Connection failed: #{e.message}. Retrying in #{retry_interval} seconds..."
54
+ rescue StandardError => e
55
+ @config.logger.error "Error: #{e.message}. Retrying in #{retry_interval} seconds..."
56
+ end
57
+ end
58
+ end
59
+
60
+ @running = false
61
+ end
62
+ end
63
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Flagsmith
4
- VERSION = '4.1.1'
4
+ VERSION = '4.3.0'
5
5
  end
data/lib/flagsmith.rb CHANGED
@@ -16,6 +16,7 @@ require 'flagsmith/sdk/pooling_manager'
16
16
  require 'flagsmith/sdk/models/flags'
17
17
  require 'flagsmith/sdk/models/segments'
18
18
  require 'flagsmith/sdk/offline_handlers'
19
+ require 'flagsmith/sdk/realtime_client'
19
20
 
20
21
  require 'flagsmith/engine/core'
21
22
 
@@ -46,6 +47,7 @@ module Flagsmith
46
47
  # :environment_key, :api_url, :custom_headers, :request_timeout_seconds, :enable_local_evaluation,
47
48
  # :environment_refresh_interval_seconds, :retries, :enable_analytics, :default_flag_handler,
48
49
  # :offline_mode, :offline_handler, :polling_manager_failure_limit
50
+ # :realtime_api_url, :enable_realtime_updates, :logger
49
51
  #
50
52
  # You can see full description in the Flagsmith::Config
51
53
 
@@ -59,6 +61,7 @@ module Flagsmith
59
61
  @identity_overrides_by_identifier = {}
60
62
 
61
63
  validate_offline_mode!
64
+ validate_realtime_mode!
62
65
 
63
66
  api_client
64
67
  analytics_processor
@@ -78,10 +81,21 @@ module Flagsmith
78
81
  'Cannot use offline_handler and default_flag_handler at the same time.'
79
82
  end
80
83
 
84
+ def validate_realtime_mode!
85
+ return unless @config.realtime_mode? && !@config.local_evaluation?
86
+
87
+ raise Flagsmith::ClientError,
88
+ 'The enable_realtime_updates config param requires a matching enable_local_evaluation param.'
89
+ end
90
+
81
91
  def api_client
82
92
  @api_client ||= Flagsmith::ApiClient.new(@config)
83
93
  end
84
94
 
95
+ def realtime_client
96
+ @realtime_client ||= Flagsmith::RealtimeClient.new(@config)
97
+ end
98
+
85
99
  def engine
86
100
  @engine ||= Flagsmith::Engine::Engine.new
87
101
  end
@@ -104,6 +118,14 @@ module Flagsmith
104
118
  def environment_data_polling_manager
105
119
  return nil unless @config.local_evaluation?
106
120
 
121
+ # Bypass the environment data polling manager if realtime
122
+ # is present in the configuration.
123
+ if @config.realtime_mode?
124
+ update_environment
125
+ realtime_client.listen self unless realtime_client.running
126
+ return
127
+ end
128
+
107
129
  update_environment if @environment_data_polling_manager.nil?
108
130
 
109
131
  @environment_data_polling_manager ||= Flagsmith::EnvironmentDataPollingManager.new(
@@ -148,11 +170,13 @@ module Flagsmith
148
170
  # environment, e.g. email address, username, uuid
149
171
  # traits { key => value } is a dictionary of traits to add / update on the identity in
150
172
  # Flagsmith, e.g. { "num_orders": 10 }
173
+ # in lieu of a trait value, a trait coniguration dictionary can be provided,
174
+ # e.g. { "num_orders": { "value": 10, "transient": true } }
151
175
  # returns Flags object holding all the flags for the given identity.
152
- def get_identity_flags(identifier, **traits)
176
+ def get_identity_flags(identifier, transient = false, **traits) # rubocop:disable Style/OptionalBooleanParameter
153
177
  return get_identity_flags_from_document(identifier, traits) if environment
154
178
 
155
- get_identity_flags_from_api(identifier, traits)
179
+ get_identity_flags_from_api(identifier, traits, transient)
156
180
  end
157
181
 
158
182
  def feature_enabled?(feature_name, default: false)
@@ -253,16 +277,16 @@ module Flagsmith
253
277
  end
254
278
 
255
279
  # rubocop:disable Metrics/MethodLength
256
- def get_identity_flags_from_api(identifier, traits = {})
280
+ def get_identity_flags_from_api(identifier, traits, transient)
257
281
  if offline_handler
258
282
  begin
259
- process_identity_flags_from_api(identifier, traits)
283
+ process_identity_flags_from_api(identifier, traits, transient)
260
284
  rescue StandardError
261
285
  get_identity_flags_from_document(identifier, traits)
262
286
  end
263
287
  else
264
288
  begin
265
- process_identity_flags_from_api(identifier, traits)
289
+ process_identity_flags_from_api(identifier, traits, transient)
266
290
  rescue StandardError
267
291
  if default_flag_handler
268
292
  return Flagsmith::Flags::Collection.new(
@@ -276,8 +300,8 @@ module Flagsmith
276
300
  end
277
301
  # rubocop:enable Metrics/MethodLength
278
302
 
279
- def process_identity_flags_from_api(identifier, traits = {})
280
- data = generate_identities_data(identifier, traits)
303
+ def process_identity_flags_from_api(identifier, traits, transient)
304
+ data = generate_identities_data(identifier, traits, transient)
281
305
  json_response = api_client.post(@config.identities_url, data.to_json).body
282
306
 
283
307
  Flagsmith::Flags::Collection.from_api(
@@ -311,10 +335,13 @@ module Flagsmith
311
335
  end
312
336
  # rubocop:enable Metrics/MethodLength
313
337
 
314
- def generate_identities_data(identifier, traits = {})
338
+ def generate_identities_data(identifier, traits, transient)
315
339
  {
316
340
  identifier: identifier,
317
- traits: traits.map { |key, value| { trait_key: key, trait_value: value } }
341
+ transient: transient,
342
+ traits: traits.map do |key, value|
343
+ value.is_a?(Hash) ? { trait_key: key, trait_value: value[:value], transient: value[:transient] || false } : { trait_key: key, trait_value: value }
344
+ end
318
345
  }
319
346
  end
320
347
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flagsmith
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.1
4
+ version: 4.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tom Stuart
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2024-05-22 00:00:00.000000000 Z
13
+ date: 2024-12-06 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: bundler
@@ -193,6 +193,7 @@ files:
193
193
  - lib/flagsmith/sdk/models/segments.rb
194
194
  - lib/flagsmith/sdk/offline_handlers.rb
195
195
  - lib/flagsmith/sdk/pooling_manager.rb
196
+ - lib/flagsmith/sdk/realtime_client.rb
196
197
  - lib/flagsmith/version.rb
197
198
  homepage: https://flagsmith.com
198
199
  licenses: []