unleash 4.4.4 → 6.4.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/pull_request.yml +15 -15
  3. data/.rubocop.yml +5 -1
  4. data/CHANGELOG.md +147 -1
  5. data/README.md +161 -144
  6. data/bin/unleash-client +1 -1
  7. data/echo_client_spec_version.rb +3 -0
  8. data/examples/extending_unleash_with_opentelemetry.rb +63 -0
  9. data/examples/simple.rb +3 -4
  10. data/examples/streaming.rb +50 -0
  11. data/lib/unleash/bootstrap/handler.rb +2 -1
  12. data/lib/unleash/client.rb +47 -26
  13. data/lib/unleash/configuration.rb +48 -7
  14. data/lib/unleash/context.rb +35 -9
  15. data/lib/unleash/metrics_reporter.rb +39 -28
  16. data/lib/unleash/spec_version.rb +3 -0
  17. data/lib/unleash/strategies.rb +14 -61
  18. data/lib/unleash/streaming_client_executor.rb +85 -0
  19. data/lib/unleash/streaming_event_processor.rb +53 -0
  20. data/lib/unleash/toggle_fetcher.rb +29 -58
  21. data/lib/unleash/util/event_source_wrapper.rb +17 -0
  22. data/lib/unleash/util/http.rb +3 -2
  23. data/lib/unleash/variant.rb +11 -3
  24. data/lib/unleash/version.rb +1 -1
  25. data/lib/unleash.rb +2 -1
  26. data/unleash-client.gemspec +8 -4
  27. data/v6_MIGRATION_GUIDE.md +21 -0
  28. metadata +60 -26
  29. data/lib/unleash/activation_strategy.rb +0 -31
  30. data/lib/unleash/constraint.rb +0 -115
  31. data/lib/unleash/feature_toggle.rb +0 -187
  32. data/lib/unleash/metrics.rb +0 -41
  33. data/lib/unleash/strategy/application_hostname.rb +0 -26
  34. data/lib/unleash/strategy/base.rb +0 -16
  35. data/lib/unleash/strategy/default.rb +0 -13
  36. data/lib/unleash/strategy/flexible_rollout.rb +0 -64
  37. data/lib/unleash/strategy/gradual_rollout_random.rb +0 -24
  38. data/lib/unleash/strategy/gradual_rollout_sessionid.rb +0 -21
  39. data/lib/unleash/strategy/gradual_rollout_userid.rb +0 -21
  40. data/lib/unleash/strategy/remote_address.rb +0 -36
  41. data/lib/unleash/strategy/user_with_id.rb +0 -20
  42. data/lib/unleash/strategy/util.rb +0 -16
  43. data/lib/unleash/variant_definition.rb +0 -26
  44. data/lib/unleash/variant_override.rb +0 -44
@@ -0,0 +1,63 @@
1
+ # example on how to extend the unleash client with opentelemetry by monkey patching it.
2
+ # to be added before initializing the client.
3
+ # in rails it could be added, for example, at:
4
+ # config/initializers/unleash.rb
5
+
6
+ require 'opentelemetry-api'
7
+ require 'unleash'
8
+
9
+ module UnleashExtensions
10
+ module OpenTelemetry
11
+ TRACER = ::OpenTelemetry.tracer_provider.tracer('Unleash-Client', Unleash::VERSION)
12
+
13
+ module Client
14
+ def initialize(*opts)
15
+ UnleashExtensions::OpenTelemetry::TRACER.in_span("#{self.class.name}##{__method__}") do |_span|
16
+ super(*opts)
17
+ end
18
+ end
19
+
20
+ def is_enabled?(feature, *args)
21
+ UnleashExtensions::OpenTelemetry::TRACER.in_span("#{self.class.name}##{__method__}") do |span|
22
+ result = super(feature, *args)
23
+
24
+ # OpenTelemetry::SemanticConventions::Trace::FEATURE_FLAG_* is not in the `opentelemetry-semantic_conventions` gem yet
25
+ span.add_attributes({
26
+ 'feature_flag.provider_name' => 'Unleash',
27
+ 'feature_flag.key' => feature,
28
+ 'feature_flag.variant' => result
29
+ })
30
+
31
+ result
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ module MetricsReporter
38
+ def post
39
+ UnleashExtensions::OpenTelemetry::TRACER.in_span("#{self.class.name}##{__method__}") do |_span|
40
+ super
41
+ end
42
+ end
43
+ end
44
+
45
+ module ToggleFetcher
46
+ def fetch
47
+ UnleashExtensions::OpenTelemetry::TRACER.in_span("#{self.class.name}##{__method__}") do |_span|
48
+ super
49
+ end
50
+ end
51
+
52
+ def save!
53
+ UnleashExtensions::OpenTelemetry::TRACER.in_span("#{self.class.name}##{__method__}") do |_span|
54
+ super
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ # MonkeyPatch here:
61
+ ::Unleash::Client.prepend UnleashExtensions::OpenTelemetry::Client
62
+ ::Unleash::MetricsReporter.prepend UnleashExtensions::OpenTelemetry::MetricsReporter
63
+ ::Unleash::ToggleFetcher.prepend UnleashExtensions::OpenTelemetry::ToggleFetcher
data/examples/simple.rb CHANGED
@@ -18,8 +18,8 @@ puts ">> START simple.rb"
18
18
  # or:
19
19
 
20
20
  @unleash = Unleash::Client.new(
21
- url: 'https://unleash.herokuapp.com/api',
22
- custom_http_headers: { 'Authorization': '943ca9171e2c884c545c5d82417a655fb77cec970cc3b78a8ff87f4406b495d0' },
21
+ url: 'https://app.unleash-hosted.com/demo/api',
22
+ custom_http_headers: { 'Authorization': 'demo-app:dev.9fc74dd72d2b88bea5253c04240b21a54841f08d9918046ed55a06b5' },
23
23
  app_name: 'simple-test',
24
24
  instance_id: 'local-test-cli',
25
25
  refresh_interval: 2,
@@ -27,8 +27,7 @@ puts ">> START simple.rb"
27
27
  retry_limit: 2
28
28
  )
29
29
 
30
- # feature_name = "AwesomeFeature"
31
- feature_name = "4343443"
30
+ feature_name = "example-flag"
32
31
  unleash_context = Unleash::Context.new
33
32
  unleash_context.user_id = 123
34
33
 
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'unleash'
4
+ require 'unleash/context'
5
+
6
+ puts ">> START streaming.rb"
7
+
8
+ @unleash = Unleash::Client.new(
9
+ url: 'https://app.unleash-hosted.com/demo/api',
10
+ custom_http_headers: { 'Authorization': 'demo-app:dev.9fc74dd72d2b88bea5253c04240b21a54841f08d9918046ed55a06b5' },
11
+ app_name: 'streaming-test',
12
+ instance_id: 'local-streaming-cli',
13
+ refresh_interval: 2,
14
+ metrics_interval: 2,
15
+ retry_limit: 2,
16
+ experimental_mode: { type: 'streaming' },
17
+ timeout: 5,
18
+ log_level: Logger::DEBUG
19
+ )
20
+
21
+ feature_name = "example-flag"
22
+ unleash_context = Unleash::Context.new
23
+ unleash_context.user_id = 123
24
+
25
+ puts "Waiting for client to initialize..."
26
+ sleep 2
27
+
28
+ 100.times do
29
+ if @unleash.is_enabled?(feature_name, unleash_context)
30
+ puts "> #{feature_name} is enabled"
31
+ else
32
+ puts "> #{feature_name} is not enabled"
33
+ end
34
+ sleep 1
35
+ puts "---"
36
+ puts ""
37
+ puts ""
38
+ end
39
+ feature_name = "foobar"
40
+ if @unleash.is_enabled?(feature_name, unleash_context, true)
41
+ puts "> #{feature_name} is enabled"
42
+ else
43
+ puts "> #{feature_name} is not enabled"
44
+ end
45
+
46
+ puts "> shutting down client..."
47
+
48
+ @unleash.shutdown
49
+
50
+ puts ">> END streaming.rb"
@@ -15,7 +15,8 @@ module Unleash
15
15
  return configuration.data unless self.configuration.data.nil?
16
16
  return configuration.block.call if self.configuration.block.is_a?(Proc)
17
17
  return Provider::FromFile.read(configuration.file_path) unless self.configuration.file_path.nil?
18
- return Provider::FromUrl.read(configuration.url, configuration.url_headers) unless self.configuration.url.nil?
18
+
19
+ Provider::FromUrl.read(configuration.url, configuration.url_headers) unless self.configuration.url.nil?
19
20
  end
20
21
  end
21
22
  end
@@ -2,8 +2,10 @@ require 'unleash/configuration'
2
2
  require 'unleash/toggle_fetcher'
3
3
  require 'unleash/metrics_reporter'
4
4
  require 'unleash/scheduled_executor'
5
- require 'unleash/feature_toggle'
5
+ require 'unleash/streaming_client_executor'
6
+ require 'unleash/variant'
6
7
  require 'unleash/util/http'
8
+ require 'unleash/util/event_source_wrapper'
7
9
  require 'logger'
8
10
  require 'time'
9
11
 
@@ -11,14 +13,18 @@ module Unleash
11
13
  class Client
12
14
  attr_accessor :fetcher_scheduled_executor, :metrics_scheduled_executor
13
15
 
16
+ # rubocop:disable Metrics/AbcSize
14
17
  def initialize(*opts)
15
18
  Unleash.configuration = Unleash::Configuration.new(*opts) unless opts.empty?
16
19
  Unleash.configuration.validate!
17
20
 
18
21
  Unleash.logger = Unleash.configuration.logger.clone
19
22
  Unleash.logger.level = Unleash.configuration.log_level
23
+ Unleash.engine = YggdrasilEngine.new
24
+ Unleash.engine.register_custom_strategies(Unleash.configuration.strategies.custom_strategies)
25
+
26
+ Unleash.toggle_fetcher = Unleash::ToggleFetcher.new Unleash.engine unless Unleash.configuration.streaming_mode?
20
27
 
21
- Unleash.toggle_fetcher = Unleash::ToggleFetcher.new
22
28
  if Unleash.configuration.disable_client
23
29
  Unleash.logger.warn "Unleash::Client is disabled! Will only return default (or bootstrapped if available) results!"
24
30
  Unleash.logger.warn "Unleash::Client is disabled! Metrics and MetricsReporter are also disabled!"
@@ -27,9 +33,12 @@ module Unleash
27
33
  end
28
34
 
29
35
  register
30
- start_toggle_fetcher
36
+
37
+ initialize_client_mode
38
+
31
39
  start_metrics unless Unleash.configuration.disable_metrics
32
40
  end
41
+ # rubocop:enable Metrics/AbcSize
33
42
 
34
43
  def is_enabled?(feature, context = nil, default_value_param = false, &fallback_blk)
35
44
  Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} with context #{context}"
@@ -40,16 +49,16 @@ module Unleash
40
49
  default_value_param
41
50
  end
42
51
 
43
- toggle_as_hash = Unleash&.toggles&.select{ |toggle| toggle['name'] == feature }&.first
44
-
45
- if toggle_as_hash.nil?
52
+ toggle_enabled = Unleash.engine.enabled?(feature, context)
53
+ if toggle_enabled.nil?
46
54
  Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} not found"
55
+ Unleash.engine.count_toggle(feature, false)
47
56
  return default_value
48
57
  end
49
58
 
50
- toggle = Unleash::FeatureToggle.new(toggle_as_hash, Unleash&.segment_cache)
59
+ Unleash.engine.count_toggle(feature, toggle_enabled)
51
60
 
52
- toggle.is_enabled?(context)
61
+ toggle_enabled
53
62
  end
54
63
 
55
64
  def is_disabled?(feature, context = nil, default_value_param = true, &fallback_blk)
@@ -72,23 +81,19 @@ module Unleash
72
81
  end
73
82
 
74
83
  def get_variant(feature, context = Unleash::Context.new, fallback_variant = disabled_variant)
75
- Unleash.logger.debug "Unleash::Client.get_variant for feature: #{feature} with context #{context}"
76
-
77
- toggle_as_hash = Unleash&.toggles&.select{ |toggle| toggle['name'] == feature }&.first
78
-
79
- if toggle_as_hash.nil?
80
- Unleash.logger.debug "Unleash::Client.get_variant feature: #{feature} not found"
81
- return fallback_variant
82
- end
83
-
84
- toggle = Unleash::FeatureToggle.new(toggle_as_hash)
85
- variant = toggle.get_variant(context, fallback_variant)
84
+ variant = Unleash.engine.get_variant(feature, context)
86
85
 
87
86
  if variant.nil?
88
87
  Unleash.logger.debug "Unleash::Client.get_variant variants for feature: #{feature} not found"
88
+ Unleash.engine.count_toggle(feature, false)
89
89
  return fallback_variant
90
90
  end
91
91
 
92
+ variant = Variant.new(variant)
93
+
94
+ Unleash.engine.count_variant(feature, variant.name)
95
+ Unleash.engine.count_toggle(feature, variant.feature_enabled)
96
+
92
97
  # TODO: Add to README: name, payload, enabled (bool)
93
98
 
94
99
  variant
@@ -97,7 +102,6 @@ module Unleash
97
102
  # safe shutdown: also flush metrics to server and toggles to disk
98
103
  def shutdown
99
104
  unless Unleash.configuration.disable_client
100
- Unleash.toggle_fetcher.save!
101
105
  Unleash.reporter.post unless Unleash.configuration.disable_metrics
102
106
  shutdown!
103
107
  end
@@ -106,7 +110,7 @@ module Unleash
106
110
  # quick shutdown: just kill running threads
107
111
  def shutdown!
108
112
  unless Unleash.configuration.disable_client
109
- self.fetcher_scheduled_executor.exit
113
+ self.fetcher_scheduled_executor&.exit
110
114
  self.metrics_scheduled_executor.exit unless Unleash.configuration.disable_metrics
111
115
  end
112
116
  end
@@ -117,10 +121,15 @@ module Unleash
117
121
  {
118
122
  'appName': Unleash.configuration.app_name,
119
123
  'instanceId': Unleash.configuration.instance_id,
120
- 'sdkVersion': "unleash-client-ruby:" + Unleash::VERSION,
121
- 'strategies': Unleash.strategies.keys,
124
+ 'connectionId': Unleash.configuration.connection_id,
125
+ 'sdkVersion': "unleash-ruby-sdk:" + Unleash::VERSION,
126
+ 'strategies': Unleash.strategies.known_strategies,
122
127
  'started': Time.now.iso8601(Unleash::TIME_RESOLUTION),
123
- 'interval': Unleash.configuration.metrics_interval_in_millis
128
+ 'interval': Unleash.configuration.metrics_interval_in_millis,
129
+ 'platformName': RUBY_ENGINE,
130
+ 'platformVersion': RUBY_VERSION,
131
+ 'yggdrasilVersion': "0.13.3",
132
+ 'specVersion': Unleash::CLIENT_SPECIFICATION_VERSION
124
133
  }
125
134
  end
126
135
 
@@ -136,8 +145,12 @@ module Unleash
136
145
  end
137
146
  end
138
147
 
148
+ def start_streaming_client
149
+ self.fetcher_scheduled_executor = Unleash::StreamingClientExecutor.new('StreamingExecutor', Unleash.engine)
150
+ self.fetcher_scheduled_executor.run
151
+ end
152
+
139
153
  def start_metrics
140
- Unleash.toggle_metrics = Unleash::Metrics.new
141
154
  Unleash.reporter = Unleash::MetricsReporter.new
142
155
  self.metrics_scheduled_executor = Unleash::ScheduledExecutor.new(
143
156
  'MetricsReporter',
@@ -163,11 +176,19 @@ module Unleash
163
176
  end
164
177
 
165
178
  def disabled_variant
166
- @disabled_variant ||= Unleash::FeatureToggle.disabled_variant
179
+ @disabled_variant ||= Unleash::Variant.disabled_variant
167
180
  end
168
181
 
169
182
  def first_fetch_is_eager
170
183
  Unleash.configuration.use_bootstrap?
171
184
  end
185
+
186
+ def initialize_client_mode
187
+ if Unleash.configuration.streaming_mode?
188
+ start_streaming_client
189
+ else
190
+ start_toggle_fetcher
191
+ end
192
+ end
172
193
  end
173
194
  end
@@ -21,7 +21,10 @@ module Unleash
21
21
  :logger,
22
22
  :log_level,
23
23
  :bootstrap_config,
24
- :strategies
24
+ :strategies,
25
+ :use_delta_api,
26
+ :experimental_mode
27
+ attr_reader :connection_id
25
28
 
26
29
  def initialize(opts = {})
27
30
  validate_custom_http_headers!(opts[:custom_http_headers]) if opts.has_key?(:custom_http_headers)
@@ -40,9 +43,9 @@ module Unleash
40
43
  def validate!
41
44
  return if self.disable_client
42
45
 
43
- raise ArgumentError, "URL and app_name are required parameters." if self.app_name.nil? || self.url.nil?
46
+ raise ArgumentError, "app_name is a required parameter." if self.app_name.nil?
44
47
 
45
- validate_custom_http_headers!(self.custom_http_headers)
48
+ validate_custom_http_headers!(self.custom_http_headers) unless self.url.nil?
46
49
  end
47
50
 
48
51
  def refresh_backup_file!
@@ -50,15 +53,29 @@ module Unleash
50
53
  end
51
54
 
52
55
  def http_headers
53
- {
56
+ headers = {
57
+ 'User-Agent' => "UnleashClientRuby/#{Unleash::VERSION} #{RUBY_ENGINE}/#{RUBY_VERSION} [#{RUBY_PLATFORM}]",
54
58
  'UNLEASH-INSTANCEID' => self.instance_id,
55
59
  'UNLEASH-APPNAME' => self.app_name,
56
- 'Unleash-Client-Spec' => '4.2.2'
60
+ 'Unleash-Client-Spec' => CLIENT_SPECIFICATION_VERSION,
61
+ 'UNLEASH-SDK' => "unleash-ruby-sdk:#{Unleash::VERSION}"
57
62
  }.merge!(generate_custom_http_headers)
63
+ headers['UNLEASH-CONNECTION-ID'] = @connection_id
64
+ headers
58
65
  end
59
66
 
60
67
  def fetch_toggles_uri
61
- uri = URI("#{self.url_stripped_of_slash}/client/features")
68
+ uri = nil
69
+ ## Personal feeling but Rubocop's suggestion here is too dense to be properly readable
70
+ # rubocop:disable Style/ConditionalAssignment
71
+ if streaming_mode?
72
+ uri = URI("#{self.url_stripped_of_slash}/client/streaming")
73
+ elsif self.use_delta_api || polling_with_delta?
74
+ uri = URI("#{self.url_stripped_of_slash}/client/delta")
75
+ else
76
+ uri = URI("#{self.url_stripped_of_slash}/client/features")
77
+ end
78
+ # rubocop:enable Style/ConditionalAssignment
62
79
  uri.query = "project=#{self.project_name}" unless self.project_name.nil?
63
80
  uri
64
81
  end
@@ -79,6 +96,17 @@ module Unleash
79
96
  self.bootstrap_config&.valid?
80
97
  end
81
98
 
99
+ def streaming_mode?
100
+ validate_streaming_support! if streaming_configured?
101
+ streaming_configured?
102
+ end
103
+
104
+ def polling_with_delta?
105
+ self.experimental_mode.is_a?(Hash) &&
106
+ self.experimental_mode[:type] == 'polling' &&
107
+ self.experimental_mode[:format] == 'delta'
108
+ end
109
+
82
110
  private
83
111
 
84
112
  def set_defaults
@@ -89,7 +117,7 @@ module Unleash
89
117
  self.project_name = nil
90
118
  self.disable_client = false
91
119
  self.disable_metrics = false
92
- self.refresh_interval = 10
120
+ self.refresh_interval = 15
93
121
  self.metrics_interval = 60
94
122
  self.timeout = 30
95
123
  self.retry_limit = Float::INFINITY
@@ -97,8 +125,11 @@ module Unleash
97
125
  self.log_level = Logger::WARN
98
126
  self.bootstrap_config = nil
99
127
  self.strategies = Unleash::Strategies.new
128
+ self.use_delta_api = false
129
+ self.experimental_mode = nil
100
130
 
101
131
  self.custom_http_headers = {}
132
+ @connection_id = SecureRandom.uuid
102
133
  end
103
134
 
104
135
  def initialize_default_logger
@@ -133,5 +164,15 @@ module Unleash
133
164
  rescue NoMethodError
134
165
  raise ArgumentError, "unknown configuration parameter '#{val}'"
135
166
  end
167
+
168
+ def streaming_configured?
169
+ self.experimental_mode.is_a?(Hash) && self.experimental_mode[:type] == 'streaming'
170
+ end
171
+
172
+ def validate_streaming_support!
173
+ return unless RUBY_ENGINE == 'jruby'
174
+
175
+ raise "Streaming mode is not supported on JRuby. Please use polling mode instead or switch to MRI/CRuby."
176
+ end
136
177
  end
137
178
  end
@@ -7,20 +7,36 @@ module Unleash
7
7
  def initialize(params = {})
8
8
  raise ArgumentError, "Unleash::Context must be initialized with a hash." unless params.is_a?(Hash)
9
9
 
10
- self.app_name = value_for('appName', params, Unleash&.configuration&.app_name)
11
- self.environment = value_for('environment', params, Unleash&.configuration&.environment || 'default')
12
- self.user_id = value_for('userId', params)&.to_s
13
- self.session_id = value_for('sessionId', params)
14
- self.remote_address = value_for('remoteAddress', params)
15
- self.current_time = value_for('currentTime', params, Time.now.utc.iso8601.to_s)
16
-
17
- properties = value_for('properties', params)
10
+ self.app_name = value_for("appName", params, Unleash.configuration&.app_name)
11
+ self.environment = value_for("environment", params, Unleash.configuration&.environment || "default")
12
+ self.user_id = value_for("userId", params)&.to_s
13
+ self.session_id = value_for("sessionId", params)
14
+ self.remote_address = value_for("remoteAddress", params)
15
+ self.current_time = value_for("currentTime", params, Time.now.utc.iso8601.to_s)
16
+
17
+ properties = value_for("properties", params)
18
18
  self.properties = properties.is_a?(Hash) ? properties.transform_keys(&:to_sym) : {}
19
19
  end
20
20
 
21
21
  def to_s
22
22
  "<Context: user_id=#{@user_id},session_id=#{@session_id},remote_address=#{@remote_address},properties=#{@properties}" \
23
- ",app_name=#{@app_name},environment=#{@environment}>"
23
+ ",app_name=#{@app_name},environment=#{@environment},current_time=#{@current_time}>"
24
+ end
25
+
26
+ def as_json(*_options)
27
+ {
28
+ appName: to_safe_value(self.app_name),
29
+ environment: to_safe_value(self.environment),
30
+ userId: to_safe_value(self.user_id),
31
+ sessionId: to_safe_value(self.session_id),
32
+ remoteAddress: to_safe_value(self.remote_address),
33
+ currentTime: to_safe_value(self.current_time),
34
+ properties: self.properties.transform_values{ |value| to_safe_value(value) }
35
+ }
36
+ end
37
+
38
+ def to_json(*options)
39
+ as_json(*options).to_json(*options)
24
40
  end
25
41
 
26
42
  def to_h
@@ -52,6 +68,16 @@ module Unleash
52
68
  params.values_at(key, key.to_sym, underscore(key), underscore(key).to_sym).compact.first || default_value
53
69
  end
54
70
 
71
+ def to_safe_value(value)
72
+ return nil if value.nil?
73
+
74
+ if value.is_a?(Time)
75
+ value.utc.iso8601
76
+ else
77
+ value.to_s
78
+ end
79
+ end
80
+
55
81
  # converts CamelCase to snake_case
56
82
  def underscore(camel_cased_word)
57
83
  camel_cased_word.to_s.gsub(/(.)([A-Z])/, '\1_\2').downcase
@@ -1,5 +1,4 @@
1
1
  require 'unleash/configuration'
2
- require 'unleash/metrics'
3
2
  require 'net/http'
4
3
  require 'json'
5
4
  require 'time'
@@ -15,48 +14,60 @@ module Unleash
15
14
  end
16
15
 
17
16
  def generate_report
18
- now = Time.now
17
+ metrics = Unleash.engine&.get_metrics
18
+ return nil if metrics.nil?
19
19
 
20
- start = self.last_time
21
- stop = now
22
- self.last_time = now
20
+ generate_report_from_bucket metrics
21
+ end
22
+
23
+ def post
24
+ Unleash.logger.debug "post() Report"
25
+
26
+ report = build_report
27
+ return unless report
23
28
 
24
- report = {
29
+ send_report(report)
30
+ end
31
+
32
+ private
33
+
34
+ def generate_report_from_bucket(bucket)
35
+ {
36
+ 'platformName': RUBY_ENGINE,
37
+ 'platformVersion': RUBY_VERSION,
38
+ 'yggdrasilVersion': "0.13.3",
39
+ 'specVersion': Unleash::CLIENT_SPECIFICATION_VERSION,
25
40
  'appName': Unleash.configuration.app_name,
26
41
  'instanceId': Unleash.configuration.instance_id,
27
- 'bucket': {
28
- 'start': start.iso8601(Unleash::TIME_RESOLUTION),
29
- 'stop': stop.iso8601(Unleash::TIME_RESOLUTION),
30
- 'toggles': Unleash.toggle_metrics.features
31
- }
42
+ 'connectionId': Unleash.configuration.connection_id,
43
+ 'bucket': bucket
32
44
  }
33
- Unleash.toggle_metrics.reset
34
-
35
- report
36
45
  end
37
46
 
38
- def post
39
- Unleash.logger.debug "post() Report"
40
-
41
- if bucket_empty? && (Time.now - self.last_time < LONGEST_WITHOUT_A_REPORT) # and last time is less then 10 minutes...
42
- Unleash.logger.debug "Report not posted to server, as it would have been empty. (and has been empty for up to 10 min)"
47
+ def build_report
48
+ report = generate_report
49
+ return nil if report.nil? && Time.now - self.last_time < LONGEST_WITHOUT_A_REPORT
43
50
 
44
- return
45
- end
51
+ report || generate_report_from_bucket({
52
+ 'start': self.last_time.utc.iso8601,
53
+ 'stop': Time.now.utc.iso8601,
54
+ 'toggles': {}
55
+ })
56
+ end
46
57
 
47
- response = Unleash::Util::Http.post(Unleash.configuration.client_metrics_uri, self.generate_report.to_json)
58
+ def send_report(report)
59
+ self.last_time = Time.now
60
+ headers = (Unleash.configuration.http_headers || {}).dup
61
+ headers.merge!({ 'UNLEASH-INTERVAL' => Unleash.configuration.metrics_interval.to_s })
62
+ response = Unleash::Util::Http.post(Unleash.configuration.client_metrics_uri, report.to_json, headers)
48
63
 
49
64
  if ['200', '202'].include? response.code
50
65
  Unleash.logger.debug "Report sent to unleash server successfully. Server responded with http code #{response.code}"
51
66
  else
67
+ # :nocov:
52
68
  Unleash.logger.error "Error when sending report to unleash server. Server responded with http code #{response.code}."
69
+ # :nocov:
53
70
  end
54
71
  end
55
-
56
- private
57
-
58
- def bucket_empty?
59
- Unleash.toggle_metrics.features.empty?
60
- end
61
72
  end
62
73
  end
@@ -0,0 +1,3 @@
1
+ module Unleash
2
+ CLIENT_SPECIFICATION_VERSION = "5.2.0".freeze
3
+ end
@@ -1,80 +1,33 @@
1
- require 'unleash/strategy/base'
2
- Gem.find_files('unleash/strategy/**/*.rb').each{ |path| require path }
3
-
4
1
  module Unleash
2
+ class DefaultOverrideError < RuntimeError
3
+ end
4
+
5
5
  class Strategies
6
+ attr_accessor :strategies
7
+
6
8
  def initialize
7
9
  @strategies = {}
8
- register_strategies
9
- end
10
-
11
- def keys
12
- @strategies.keys
13
10
  end
14
11
 
15
12
  def includes?(name)
16
- @strategies.has_key?(name.to_s)
17
- end
18
-
19
- def fetch(name)
20
- raise Unleash::Strategy::NotImplemented, "Strategy is not implemented" unless (strategy = @strategies[name.to_s])
21
-
22
- strategy
13
+ @strategies.has_key?(name.to_s) || DEFAULT_STRATEGIES.include?(name.to_s)
23
14
  end
24
15
 
25
16
  def add(strategy)
26
- @strategies[strategy.name] = strategy
27
- end
28
-
29
- def []=(key, strategy)
30
- warn_deprecated_registration(strategy, 'modifying Unleash::STRATEGIES')
31
- @strategies[key.to_s] = strategy
32
- end
33
-
34
- def [](key)
35
- @strategies[key.to_s]
36
- end
17
+ raise DefaultOverrideError, "Cannot override a default strategy" if DEFAULT_STRATEGIES.include?(strategy.name)
37
18
 
38
- def register_strategies
39
- register_base_strategies
40
- register_custom_strategies
19
+ @strategies[strategy.name] = strategy
41
20
  end
42
21
 
43
- protected
44
-
45
- # Deprecated: Use Unleash.configuration to add custom strategies
46
- def register_custom_strategies
47
- Unleash::Strategy.constants
48
- .select{ |c| Unleash::Strategy.const_get(c).is_a? Class }
49
- .reject{ |c| ['NotImplemented', 'Base'].include?(c.to_s) } # Reject abstract classes
50
- .map{ |c| Object.const_get("Unleash::Strategy::#{c}") }
51
- .reject{ |c| DEFAULT_STRATEGIES.include?(c) } # Reject base classes
52
- .each do |c|
53
- strategy = c.new
54
- warn_deprecated_registration(strategy, 'adding custom class into Unleash::Strategy namespace')
55
- self.add(strategy)
56
- end
22
+ def custom_strategies
23
+ @strategies.values
57
24
  end
58
25
 
59
- def register_base_strategies
60
- DEFAULT_STRATEGIES.each{ |c| self.add(c.new) }
26
+ def known_strategies
27
+ @strategies.keys.map{ |key| { name: key } }
61
28
  end
62
29
 
63
- DEFAULT_STRATEGIES = [
64
- Unleash::Strategy::ApplicationHostname,
65
- Unleash::Strategy::Default,
66
- Unleash::Strategy::FlexibleRollout,
67
- Unleash::Strategy::GradualRolloutRandom,
68
- Unleash::Strategy::GradualRolloutSessionId,
69
- Unleash::Strategy::GradualRolloutUserId,
70
- Unleash::Strategy::RemoteAddress,
71
- Unleash::Strategy::UserWithId
72
- ].freeze
73
-
74
- def warn_deprecated_registration(strategy, method)
75
- warn "[DEPRECATED] Registering custom Unleash strategy by #{method} is deprecated.
76
- Please use Unleash configuration to register custom strategy: " \
77
- "`Unleash.configure {|c| c.strategies.add(#{strategy.class.name}.new) }`"
78
- end
30
+ DEFAULT_STRATEGIES = ['applicationHostname', 'default', 'flexibleRollout', 'gradualRolloutRandom', 'gradualRolloutSessionId',
31
+ 'gradualRolloutUserId', 'remoteAddress', 'userWithId'].freeze
79
32
  end
80
33
  end