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.
- checksums.yaml +4 -4
- data/.github/workflows/pull_request.yml +15 -15
- data/.rubocop.yml +5 -1
- data/CHANGELOG.md +147 -1
- data/README.md +161 -144
- data/bin/unleash-client +1 -1
- data/echo_client_spec_version.rb +3 -0
- data/examples/extending_unleash_with_opentelemetry.rb +63 -0
- data/examples/simple.rb +3 -4
- data/examples/streaming.rb +50 -0
- data/lib/unleash/bootstrap/handler.rb +2 -1
- data/lib/unleash/client.rb +47 -26
- data/lib/unleash/configuration.rb +48 -7
- data/lib/unleash/context.rb +35 -9
- data/lib/unleash/metrics_reporter.rb +39 -28
- data/lib/unleash/spec_version.rb +3 -0
- data/lib/unleash/strategies.rb +14 -61
- data/lib/unleash/streaming_client_executor.rb +85 -0
- data/lib/unleash/streaming_event_processor.rb +53 -0
- data/lib/unleash/toggle_fetcher.rb +29 -58
- data/lib/unleash/util/event_source_wrapper.rb +17 -0
- data/lib/unleash/util/http.rb +3 -2
- data/lib/unleash/variant.rb +11 -3
- data/lib/unleash/version.rb +1 -1
- data/lib/unleash.rb +2 -1
- data/unleash-client.gemspec +8 -4
- data/v6_MIGRATION_GUIDE.md +21 -0
- metadata +60 -26
- data/lib/unleash/activation_strategy.rb +0 -31
- data/lib/unleash/constraint.rb +0 -115
- data/lib/unleash/feature_toggle.rb +0 -187
- data/lib/unleash/metrics.rb +0 -41
- data/lib/unleash/strategy/application_hostname.rb +0 -26
- data/lib/unleash/strategy/base.rb +0 -16
- data/lib/unleash/strategy/default.rb +0 -13
- data/lib/unleash/strategy/flexible_rollout.rb +0 -64
- data/lib/unleash/strategy/gradual_rollout_random.rb +0 -24
- data/lib/unleash/strategy/gradual_rollout_sessionid.rb +0 -21
- data/lib/unleash/strategy/gradual_rollout_userid.rb +0 -21
- data/lib/unleash/strategy/remote_address.rb +0 -36
- data/lib/unleash/strategy/user_with_id.rb +0 -20
- data/lib/unleash/strategy/util.rb +0 -16
- data/lib/unleash/variant_definition.rb +0 -26
- 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.
|
22
|
-
custom_http_headers: { 'Authorization': '
|
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
|
-
|
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
|
-
|
18
|
+
|
19
|
+
Provider::FromUrl.read(configuration.url, configuration.url_headers) unless self.configuration.url.nil?
|
19
20
|
end
|
20
21
|
end
|
21
22
|
end
|
data/lib/unleash/client.rb
CHANGED
@@ -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/
|
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
|
-
|
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
|
-
|
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
|
-
|
59
|
+
Unleash.engine.count_toggle(feature, toggle_enabled)
|
51
60
|
|
52
|
-
|
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.
|
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
|
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
|
-
'
|
121
|
-
'
|
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::
|
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, "
|
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' =>
|
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 =
|
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 =
|
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
|
data/lib/unleash/context.rb
CHANGED
@@ -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
|
11
|
-
self.environment = value_for(
|
12
|
-
self.user_id
|
13
|
-
self.session_id
|
14
|
-
self.remote_address = value_for(
|
15
|
-
self.current_time = value_for(
|
16
|
-
|
17
|
-
properties = value_for(
|
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
|
-
|
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
|
-
|
17
|
+
metrics = Unleash.engine&.get_metrics
|
18
|
+
return nil if metrics.nil?
|
19
19
|
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
'
|
28
|
-
|
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
|
39
|
-
|
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
|
-
|
45
|
-
|
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
|
-
|
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
|
data/lib/unleash/strategies.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
39
|
-
register_base_strategies
|
40
|
-
register_custom_strategies
|
19
|
+
@strategies[strategy.name] = strategy
|
41
20
|
end
|
42
21
|
|
43
|
-
|
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
|
60
|
-
|
26
|
+
def known_strategies
|
27
|
+
@strategies.keys.map{ |key| { name: key } }
|
61
28
|
end
|
62
29
|
|
63
|
-
DEFAULT_STRATEGIES = [
|
64
|
-
|
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
|