unleash 6.4.0 → 6.5.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: a76eb26bedcade3ed88389aa909f7ead99177e336509bc4fd99d85c4703f1405
4
- data.tar.gz: 43410bf468ac4fd3b56af79a354eb84794c40c67b6c2d96c27bdf26772b1c54d
3
+ metadata.gz: 8f2aacf28333e06f4ca46369b9ece6fd1472a8e363cdc4a0fe1aeb0810d879a2
4
+ data.tar.gz: bc8e366ec18bff858f835260af79650ab6179daa6af93bf84a675eb3d74ad904
5
5
  SHA512:
6
- metadata.gz: 3e43cbef4ba85e84265f10b9414eba95c92aeb59792a40318e15df37313ee5a6b99ce3c3e3e89d465c56e40fff92e61ed4115766d59b6ee9147c2d2c5920ebf7
7
- data.tar.gz: 2401fa3bf2d0f824d7da3cf2e01cc4c20fe740f26d2019f89554adbbc67fdc5c0f4fa65003e143391b14ea2466314db36852edd0ccefca8178f6545e3186170d
6
+ metadata.gz: 9c397ef806b4e77fb8a1d8afbacd3ded420f5979fcf1999b03e524d77dca3f04205fa28e4a1e83037c7670837654c5f0e193f74b53654dedb034c1e9016fb22e
7
+ data.tar.gz: 07bfa665ecb17ffbe86b9dbb66c3247d824a27e27d846000f181ad5783d8af3a6e9adb9a47d2e2f37732bec80c137683609b63726cd58616164efe464a4fcd4c
data/CHANGELOG.md CHANGED
@@ -13,6 +13,16 @@ Note: These changes are not considered notable:
13
13
 
14
14
  ## [Unreleased]
15
15
 
16
+ ## [6.5.0] - 2026-01-29
17
+
18
+ ### Added
19
+ - Impact metrics
20
+
21
+ ## [6.4.1] - 2025-12-04
22
+
23
+ ### Changed
24
+ - Bump yggdrasil-engine
25
+
16
26
  ## [6.4.0] - 2025-08-12
17
27
  ### Added
18
28
  - Experimental streaming support
data/README.md CHANGED
@@ -28,7 +28,7 @@ You can use this client with [Unleash Enterprise](https://www.getunleash.io/pric
28
28
  Add this line to your application's Gemfile:
29
29
 
30
30
  ```ruby
31
- gem 'unleash', '~> 6.3.0'
31
+ gem 'unleash', '~> 6.5.0'
32
32
  ```
33
33
 
34
34
  And then execute:
@@ -534,6 +534,56 @@ Unleash.configure do |config|
534
534
  end
535
535
  ```
536
536
 
537
+ ## Impact metrics
538
+
539
+ Impact metrics are lightweight, application-level time-series metrics stored and visualized directly inside Unleash. They allow you to connect specific application data, such as request counts, error rates, or latency, to your feature flags and release plans.
540
+
541
+ These metrics help validate feature impact and automate release processes. For instance, you can monitor usage patterns or performance to determine if a feature meets its goals.
542
+
543
+ The SDK automatically attaches context labels to metrics: `appName` and `environment`.
544
+
545
+ ### Counters
546
+
547
+ Use counters for cumulative values that only increase (total requests, errors):
548
+
549
+ ```ruby
550
+ client = Unleash::Client.new
551
+
552
+ client.impact_metrics.define_counter(
553
+ 'request_count',
554
+ 'Total number of HTTP requests processed'
555
+ )
556
+
557
+ client.impact_metrics.increment_counter('request_count')
558
+ ```
559
+
560
+ ### Gauges
561
+
562
+ Use gauges for point-in-time values that can go up or down:
563
+
564
+ ```ruby
565
+ client.impact_metrics.define_gauge(
566
+ 'total_users',
567
+ 'Total number of registered users'
568
+ )
569
+
570
+ client.impact_metrics.update_gauge('total_users', user_count)
571
+ ```
572
+
573
+ ### Histograms
574
+
575
+ Histograms measure value distribution (request duration, response size):
576
+
577
+ ```ruby
578
+ client.impact_metrics.define_histogram(
579
+ 'request_time_ms',
580
+ 'Time taken to process a request in milliseconds',
581
+ [50, 100, 200, 500, 1000]
582
+ )
583
+
584
+ client.impact_metrics.observe_histogram('request_time_ms', 125)
585
+ ```
586
+
537
587
  ## Development
538
588
 
539
589
  After checking out the repo, run `bin/setup` to install dependencies.
@@ -0,0 +1,24 @@
1
+ require 'unleash/configuration'
2
+
3
+ module Unleash
4
+ class BackupFileReader
5
+ def self.read!
6
+ Unleash.logger.debug "read!()"
7
+
8
+ backup_file = Unleash.configuration.backup_file
9
+ return nil unless File.exist?(backup_file)
10
+
11
+ File.read(backup_file)
12
+ rescue IOError => e
13
+ # :nocov:
14
+ Unleash.logger.error "Unable to read the backup_file: #{e}"
15
+ # :nocov:
16
+ nil
17
+ rescue StandardError => e
18
+ # :nocov:
19
+ Unleash.logger.error "Unable to extract valid data from backup_file. Exception thrown: #{e}"
20
+ # :nocov:
21
+ nil
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,21 @@
1
+ require 'unleash/configuration'
2
+
3
+ module Unleash
4
+ class BackupFileWriter
5
+ def self.save!(toggle_data)
6
+ Unleash.logger.debug "Will save toggles to disk now"
7
+
8
+ backup_file = Unleash.configuration.backup_file
9
+ backup_file_tmp = "#{backup_file}.tmp-#{Process.pid}"
10
+
11
+ File.open(backup_file_tmp, "w") do |file|
12
+ file.write(toggle_data)
13
+ end
14
+ File.rename(backup_file_tmp, backup_file)
15
+ rescue StandardError => e
16
+ # This is not really the end of the world. Swallowing the exception.
17
+ Unleash.logger.error "Unable to save backup file. Exception thrown #{e.class}:'#{e}'"
18
+ Unleash.logger.error "stacktrace: #{e.backtrace}"
19
+ end
20
+ end
21
+ end
@@ -6,12 +6,15 @@ require 'unleash/streaming_client_executor'
6
6
  require 'unleash/variant'
7
7
  require 'unleash/util/http'
8
8
  require 'unleash/util/event_source_wrapper'
9
+ require 'unleash/environment_resolver'
10
+ require 'unleash/impact_metrics'
9
11
  require 'logger'
10
12
  require 'time'
11
13
 
12
14
  module Unleash
13
15
  class Client
14
16
  attr_accessor :fetcher_scheduled_executor, :metrics_scheduled_executor
17
+ attr_reader :impact_metrics
15
18
 
16
19
  # rubocop:disable Metrics/AbcSize
17
20
  def initialize(*opts)
@@ -23,6 +26,8 @@ module Unleash
23
26
  Unleash.engine = YggdrasilEngine.new
24
27
  Unleash.engine.register_custom_strategies(Unleash.configuration.strategies.custom_strategies)
25
28
 
29
+ @impact_metrics = ImpactMetrics.new(Unleash.engine, Unleash.configuration.app_name)
30
+
26
31
  Unleash.toggle_fetcher = Unleash::ToggleFetcher.new Unleash.engine unless Unleash.configuration.streaming_mode?
27
32
 
28
33
  if Unleash.configuration.disable_client
@@ -107,6 +107,12 @@ module Unleash
107
107
  self.experimental_mode[:format] == 'delta'
108
108
  end
109
109
 
110
+ def generate_custom_http_headers
111
+ return self.custom_http_headers.call if self.custom_http_headers.respond_to?(:call)
112
+
113
+ self.custom_http_headers
114
+ end
115
+
110
116
  private
111
117
 
112
118
  def set_defaults
@@ -153,12 +159,6 @@ module Unleash
153
159
  raise ArgumentError, "custom_http_headers must be a Hash or a Proc."
154
160
  end
155
161
 
156
- def generate_custom_http_headers
157
- return self.custom_http_headers.call if self.custom_http_headers.respond_to?(:call)
158
-
159
- self.custom_http_headers
160
- end
161
-
162
162
  def set_option(opt, val)
163
163
  __send__("#{opt}=", val)
164
164
  rescue NoMethodError
@@ -0,0 +1,27 @@
1
+ module Unleash
2
+ class EnvironmentResolver
3
+ def self.extract_environment_from_custom_headers(custom_headers)
4
+ authorization_header = extract_authorization_header(custom_headers)
5
+ extract_environment_from_header(authorization_header)
6
+ end
7
+
8
+ def self.extract_authorization_header(custom_headers)
9
+ return nil if custom_headers.nil? || !custom_headers.is_a?(Hash)
10
+
11
+ key = custom_headers.keys.find{ |k| k.to_s.downcase == 'authorization' }
12
+ custom_headers[key] if key
13
+ end
14
+
15
+ def self.extract_environment_from_header(authorization_header)
16
+ return nil if authorization_header.nil? || authorization_header.empty?
17
+
18
+ after_colon = authorization_header.split(':', 2)[1]
19
+ return nil unless after_colon&.include?('.')
20
+
21
+ environment = after_colon.split('.')[0]
22
+ environment unless environment.empty?
23
+ end
24
+
25
+ private_class_method :extract_authorization_header, :extract_environment_from_header
26
+ end
27
+ end
@@ -0,0 +1,43 @@
1
+ module Unleash
2
+ class ImpactMetrics
3
+ def initialize(engine, app_name)
4
+ @engine = engine
5
+ @app_name = app_name
6
+ end
7
+
8
+ def define_counter(name, help_text)
9
+ @engine.define_counter(name, help_text)
10
+ end
11
+
12
+ def increment_counter(name, value = 1)
13
+ @engine.inc_counter(name, value, base_labels)
14
+ end
15
+
16
+ def define_gauge(name, help_text)
17
+ @engine.define_gauge(name, help_text)
18
+ end
19
+
20
+ def update_gauge(name, value)
21
+ @engine.set_gauge(name, value, base_labels)
22
+ end
23
+
24
+ def define_histogram(name, help_text, buckets = nil)
25
+ @engine.define_histogram(name, help_text, buckets)
26
+ end
27
+
28
+ def observe_histogram(name, value)
29
+ @engine.observe_histogram(name, value, base_labels)
30
+ end
31
+
32
+ private
33
+
34
+ def base_labels
35
+ {
36
+ 'appName' => @app_name,
37
+ 'environment' => EnvironmentResolver.extract_environment_from_custom_headers(
38
+ Unleash.configuration.generate_custom_http_headers
39
+ ) || Unleash.configuration.environment
40
+ }
41
+ end
42
+ end
43
+ end
@@ -23,7 +23,8 @@ module Unleash
23
23
  def post
24
24
  Unleash.logger.debug "post() Report"
25
25
 
26
- report = build_report
26
+ impact_metrics = collect_impact_metrics_safely
27
+ report = build_report(impact_metrics)
27
28
  return unless report
28
29
 
29
30
  send_report(report)
@@ -44,15 +45,20 @@ module Unleash
44
45
  }
45
46
  end
46
47
 
47
- def build_report
48
+ def build_report(impact_metrics)
48
49
  report = generate_report
49
- return nil if report.nil? && Time.now - self.last_time < LONGEST_WITHOUT_A_REPORT
50
+ has_data = !report.nil? || !impact_metrics.empty?
50
51
 
51
- report || generate_report_from_bucket({
52
+ return nil if !has_data && Time.now - self.last_time < LONGEST_WITHOUT_A_REPORT
53
+
54
+ report ||= generate_report_from_bucket({
52
55
  'start': self.last_time.utc.iso8601,
53
56
  'stop': Time.now.utc.iso8601,
54
57
  'toggles': {}
55
58
  })
59
+
60
+ report[:impactMetrics] = impact_metrics unless impact_metrics.empty?
61
+ report
56
62
  end
57
63
 
58
64
  def send_report(report)
@@ -66,8 +72,24 @@ module Unleash
66
72
  else
67
73
  # :nocov:
68
74
  Unleash.logger.error "Error when sending report to unleash server. Server responded with http code #{response.code}."
75
+ restore_impact_metrics(report[:impactMetrics])
69
76
  # :nocov:
70
77
  end
71
78
  end
79
+
80
+ def restore_impact_metrics(impact_metrics)
81
+ return if impact_metrics.nil? || impact_metrics.empty?
82
+
83
+ Unleash.engine&.restore_impact_metrics(impact_metrics)
84
+ rescue StandardError => e
85
+ Unleash.logger.warn "Failed to restore impact metrics: #{e.message}"
86
+ end
87
+
88
+ def collect_impact_metrics_safely
89
+ Unleash.engine&.collect_impact_metrics || []
90
+ rescue StandardError => e
91
+ Unleash.logger.warn "Failed to collect impact metrics: #{e.message}"
92
+ []
93
+ end
72
94
  end
73
95
  end
@@ -1,3 +1,3 @@
1
1
  module Unleash
2
- CLIENT_SPECIFICATION_VERSION = "5.2.0".freeze
2
+ CLIENT_SPECIFICATION_VERSION = "5.2.2".freeze
3
3
  end
@@ -1,4 +1,6 @@
1
1
  require 'unleash/streaming_event_processor'
2
+ require 'unleash/bootstrap/handler'
3
+ require 'unleash/backup_file_reader'
2
4
  require 'unleash/util/event_source_wrapper'
3
5
 
4
6
  module Unleash
@@ -10,6 +12,20 @@ module Unleash
10
12
  self.event_source = nil
11
13
  self.event_processor = Unleash::StreamingEventProcessor.new(engine)
12
14
  self.running = false
15
+
16
+ begin
17
+ # if bootstrap configuration is available, initialize. Otherwise read backup file
18
+ if Unleash.configuration.use_bootstrap?
19
+ bootstrap(engine)
20
+ else
21
+ read_backup_file!(engine)
22
+ end
23
+ rescue StandardError => e
24
+ # fall back to reading the backup file
25
+ Unleash.logger.warn "StreamingClientExecutor was unable to initialize, attempting to read from backup file."
26
+ Unleash.logger.debug "Exception Caught: #{e}"
27
+ read_backup_file!(engine)
28
+ end
13
29
  end
14
30
 
15
31
  def run(&_block)
@@ -81,5 +97,18 @@ module Unleash
81
97
  Unleash.logger.error "Streaming client #{self.name} threw exception #{e.class}: '#{e}'"
82
98
  Unleash.logger.debug "stacktrace: #{e.backtrace}"
83
99
  end
100
+
101
+ def read_backup_file!(engine)
102
+ backup_data = Unleash::BackupFileReader.read!
103
+ engine.take_state(backup_data) if backup_data
104
+ end
105
+
106
+ def bootstrap(engine)
107
+ bootstrap_payload = Unleash::Bootstrap::Handler.new(Unleash.configuration.bootstrap_config).retrieve_toggles
108
+ engine.take_state(bootstrap_payload)
109
+
110
+ # reset Unleash.configuration.bootstrap_data to free up memory, as we will never use it again
111
+ Unleash.configuration.bootstrap_config = nil
112
+ end
84
113
  end
85
114
  end
@@ -1,4 +1,5 @@
1
1
  require 'json'
2
+ require 'unleash/backup_file_writer'
2
3
 
3
4
  module Unleash
4
5
  class StreamingEventProcessor
@@ -41,7 +42,8 @@ module Unleash
41
42
  def handle_updated_event(event)
42
43
  handle_delta_event(event.data)
43
44
 
44
- # TODO: update backup file
45
+ full_state = @toggle_engine.get_state
46
+ Unleash::BackupFileWriter.save!(full_state)
45
47
  rescue JSON::ParserError => e
46
48
  Unleash.logger.error "Unable to parse JSON from streaming event data. Exception thrown #{e.class}: '#{e}'"
47
49
  Unleash.logger.debug "stacktrace: #{e.backtrace}"
@@ -1,5 +1,7 @@
1
1
  require 'unleash/configuration'
2
2
  require 'unleash/bootstrap/handler'
3
+ require 'unleash/backup_file_writer'
4
+ require 'unleash/backup_file_reader'
3
5
  require 'net/http'
4
6
  require 'json'
5
7
  require 'yggdrasil_engine'
@@ -23,10 +25,10 @@ module Unleash
23
25
  fetch
24
26
  end
25
27
  rescue StandardError => e
26
- # fail back to reading the backup file
28
+ # fall back to reading the backup file
27
29
  Unleash.logger.warn "ToggleFetcher was unable to fetch from the network, attempting to read from backup file."
28
30
  Unleash.logger.debug "Exception Caught: #{e}"
29
- read!
31
+ read_backup_file!
30
32
  end
31
33
 
32
34
  # once initialized, somewhere else you will want to start a loop with fetch()
@@ -54,25 +56,7 @@ module Unleash
54
56
  # always synchronize with the local cache when fetching:
55
57
  update_engine_state!(response.body)
56
58
 
57
- save! response.body
58
- end
59
-
60
- def save!(toggle_data)
61
- Unleash.logger.debug "Will save toggles to disk now"
62
-
63
- backup_file = Unleash.configuration.backup_file
64
- backup_file_tmp = "#{backup_file}.tmp"
65
-
66
- self.toggle_lock.synchronize do
67
- File.open(backup_file_tmp, "w") do |file|
68
- file.write(toggle_data)
69
- end
70
- File.rename(backup_file_tmp, backup_file)
71
- end
72
- rescue StandardError => e
73
- # This is not really the end of the world. Swallowing the exception.
74
- Unleash.logger.error "Unable to save backup file. Exception thrown #{e.class}:'#{e}'"
75
- Unleash.logger.error "stacktrace: #{e.backtrace}"
59
+ Unleash::BackupFileWriter.save! response.body
76
60
  end
77
61
 
78
62
  private
@@ -88,25 +72,9 @@ module Unleash
88
72
  Unleash.logger.error "Failed to hydrate state: #{e.backtrace}"
89
73
  end
90
74
 
91
- def read!
92
- Unleash.logger.debug "read!()"
93
- backup_file = Unleash.configuration.backup_file
94
- return nil unless File.exist?(backup_file)
95
-
96
- backup_data = File.read(backup_file)
97
- update_engine_state!(backup_data)
98
- rescue IOError => e
99
- # :nocov:
100
- Unleash.logger.error "Unable to read the backup_file: #{e}"
101
- # :nocov:
102
- rescue JSON::ParserError => e
103
- # :nocov:
104
- Unleash.logger.error "Unable to parse JSON from existing backup_file: #{e}"
105
- # :nocov:
106
- rescue StandardError => e
107
- # :nocov:
108
- Unleash.logger.error "Unable to extract valid data from backup_file. Exception thrown: #{e}"
109
- # :nocov:
75
+ def read_backup_file!
76
+ backup_data = Unleash::BackupFileReader.read!
77
+ update_engine_state!(backup_data) if backup_data
110
78
  end
111
79
 
112
80
  def bootstrap
@@ -1,3 +1,3 @@
1
1
  module Unleash
2
- VERSION = "6.4.0".freeze
2
+ VERSION = "6.5.0".freeze
3
3
  end
@@ -24,12 +24,11 @@ Gem::Specification.new do |spec|
24
24
  spec.required_ruby_version = ">= 2.7"
25
25
 
26
26
  spec.add_dependency "ld-eventsource", "2.2.4" unless RUBY_ENGINE == 'jruby'
27
- spec.add_dependency "yggdrasil-engine", "~> 1.0.4"
27
+ spec.add_dependency "yggdrasil-engine", "~> 1.2.1"
28
28
 
29
29
  spec.add_dependency "base64", "~> 0.3.0"
30
30
  spec.add_dependency "logger", "~> 1.6"
31
31
 
32
- spec.add_development_dependency "bundler", "~> 2.1"
33
32
  spec.add_development_dependency "rake", "~> 12.3"
34
33
  spec.add_development_dependency "rspec", "~> 3.12"
35
34
  spec.add_development_dependency "rspec-json_expectations", "~> 2.2"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: unleash
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.4.0
4
+ version: 6.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Renato Arruda
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-08-12 00:00:00.000000000 Z
11
+ date: 2026-01-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ld-eventsource
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 1.0.4
33
+ version: 1.2.1
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 1.0.4
40
+ version: 1.2.1
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: base64
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -66,20 +66,6 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '1.6'
69
- - !ruby/object:Gem::Dependency
70
- name: bundler
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: '2.1'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: '2.1'
83
69
  - !ruby/object:Gem::Dependency
84
70
  name: rake
85
71
  requirement: !ruby/object:Gem::Requirement
@@ -209,6 +195,8 @@ files:
209
195
  - examples/simple.rb
210
196
  - examples/streaming.rb
211
197
  - lib/unleash.rb
198
+ - lib/unleash/backup_file_reader.rb
199
+ - lib/unleash/backup_file_writer.rb
212
200
  - lib/unleash/bootstrap/configuration.rb
213
201
  - lib/unleash/bootstrap/handler.rb
214
202
  - lib/unleash/bootstrap/provider/base.rb
@@ -217,6 +205,8 @@ files:
217
205
  - lib/unleash/client.rb
218
206
  - lib/unleash/configuration.rb
219
207
  - lib/unleash/context.rb
208
+ - lib/unleash/environment_resolver.rb
209
+ - lib/unleash/impact_metrics.rb
220
210
  - lib/unleash/metrics_reporter.rb
221
211
  - lib/unleash/scheduled_executor.rb
222
212
  - lib/unleash/spec_version.rb