flipper 1.1.2 → 1.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 +4 -4
- data/.github/workflows/ci.yml +9 -2
- data/.github/workflows/examples.yml +8 -2
- data/Changelog.md +1 -647
- data/Gemfile +3 -2
- data/README.md +3 -1
- data/Rakefile +2 -2
- data/docs/images/banner.jpg +0 -0
- data/exe/flipper +5 -0
- data/flipper.gemspec +5 -1
- data/lib/flipper/adapters/actor_limit.rb +28 -0
- data/lib/flipper/adapters/cache_base.rb +143 -0
- data/lib/flipper/adapters/http/client.rb +25 -16
- data/lib/flipper/adapters/operation_logger.rb +18 -88
- data/lib/flipper/adapters/read_only.rb +6 -39
- data/lib/flipper/adapters/strict.rb +16 -18
- data/lib/flipper/adapters/wrapper.rb +54 -0
- data/lib/flipper/cli.rb +263 -0
- data/lib/flipper/cloud/configuration.rb +9 -4
- data/lib/flipper/cloud/middleware.rb +5 -5
- data/lib/flipper/cloud/telemetry/instrumenter.rb +4 -8
- data/lib/flipper/cloud/telemetry/submitter.rb +2 -2
- data/lib/flipper/cloud/telemetry.rb +10 -2
- data/lib/flipper/cloud.rb +1 -1
- data/lib/flipper/engine.rb +32 -17
- data/lib/flipper/instrumentation/log_subscriber.rb +12 -3
- data/lib/flipper/metadata.rb +3 -1
- data/lib/flipper/poller.rb +6 -5
- data/lib/flipper/serializers/gzip.rb +3 -5
- data/lib/flipper/serializers/json.rb +3 -5
- data/lib/flipper/spec/shared_adapter_specs.rb +17 -16
- data/lib/flipper/test/shared_adapter_test.rb +17 -17
- data/lib/flipper/test_help.rb +43 -0
- data/lib/flipper/typecast.rb +3 -3
- data/lib/flipper/version.rb +11 -1
- data/lib/flipper.rb +3 -1
- data/lib/generators/flipper/setup_generator.rb +63 -0
- data/package-lock.json +41 -0
- data/package.json +10 -0
- data/spec/fixtures/environment.rb +1 -0
- data/spec/flipper/adapter_builder_spec.rb +1 -2
- data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
- data/spec/flipper/adapters/http/client_spec.rb +61 -0
- data/spec/flipper/adapters/http_spec.rb +102 -76
- data/spec/flipper/adapters/strict_spec.rb +11 -9
- data/spec/flipper/cli_spec.rb +164 -0
- data/spec/flipper/cloud/configuration_spec.rb +35 -36
- data/spec/flipper/cloud/dsl_spec.rb +5 -5
- data/spec/flipper/cloud/middleware_spec.rb +8 -8
- data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +8 -9
- data/spec/flipper/cloud/telemetry/submitter_spec.rb +24 -24
- data/spec/flipper/cloud/telemetry_spec.rb +53 -1
- data/spec/flipper/cloud_spec.rb +10 -9
- data/spec/flipper/engine_spec.rb +140 -58
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +9 -2
- data/spec/flipper/middleware/memoizer_spec.rb +7 -4
- data/spec/flipper_spec.rb +1 -1
- data/spec/spec_helper.rb +1 -0
- data/spec/support/fail_on_output.rb +8 -0
- data/spec/support/spec_helpers.rb +12 -5
- data/test/adapters/actor_limit_test.rb +20 -0
- data/test_rails/generators/flipper/setup_generator_test.rb +64 -0
- data/test_rails/system/test_help_test.rb +51 -0
- metadata +31 -9
- data/spec/support/climate_control.rb +0 -7
data/lib/flipper/cli.rb
ADDED
@@ -0,0 +1,263 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module Flipper
|
4
|
+
class CLI < OptionParser
|
5
|
+
def self.run(argv = ARGV)
|
6
|
+
new.run(argv)
|
7
|
+
end
|
8
|
+
|
9
|
+
# Path to the local Rails application's environment configuration.
|
10
|
+
DEFAULT_REQUIRE = "./config/environment"
|
11
|
+
|
12
|
+
attr_accessor :shell
|
13
|
+
|
14
|
+
def initialize(stdout: $stdout, stderr: $stderr, shell: Bundler::Thor::Base.shell.new)
|
15
|
+
super
|
16
|
+
|
17
|
+
# Program is always flipper, no matter how it's invoked
|
18
|
+
@program_name = 'flipper'
|
19
|
+
@require = ENV.fetch("FLIPPER_REQUIRE", DEFAULT_REQUIRE)
|
20
|
+
@commands = {}
|
21
|
+
|
22
|
+
# Extend whatever shell to support output redirection
|
23
|
+
@shell = shell.extend(ShellOutput)
|
24
|
+
shell.redirect(stdout: stdout, stderr: stderr)
|
25
|
+
|
26
|
+
%w[enable disable].each do |action|
|
27
|
+
command action do |c|
|
28
|
+
c.banner = "Usage: #{c.program_name} [options] <feature>"
|
29
|
+
c.description = "#{action.to_s.capitalize} a feature"
|
30
|
+
|
31
|
+
values = []
|
32
|
+
|
33
|
+
c.on('-a id', '--actor=id', "#{action} for an actor") do |id|
|
34
|
+
values << Actor.new(id)
|
35
|
+
end
|
36
|
+
c.on('-g name', '--group=name', "#{action} for a group") do |name|
|
37
|
+
values << Types::Group.new(name)
|
38
|
+
end
|
39
|
+
c.on('-p NUM', '--percentage-of-actors=NUM', Numeric, "#{action} for a percentage of actors") do |num|
|
40
|
+
values << Types::PercentageOfActors.new(num)
|
41
|
+
end
|
42
|
+
c.on('-t NUM', '--percentage-of-time=NUM', Numeric, "#{action} for a percentage of time") do |num|
|
43
|
+
values << Types::PercentageOfTime.new(num)
|
44
|
+
end
|
45
|
+
c.on('-x expressions', '--expression=NUM', "#{action} for the given expression") do |expression|
|
46
|
+
begin
|
47
|
+
values << Flipper::Expression.build(JSON.parse(expression))
|
48
|
+
rescue JSON::ParserError => e
|
49
|
+
ui.error "JSON parse error #{e.message}"
|
50
|
+
ui.trace(e)
|
51
|
+
exit 1
|
52
|
+
rescue ArgumentError => e
|
53
|
+
ui.error "Invalid expression: #{e.message}"
|
54
|
+
ui.trace(e)
|
55
|
+
exit 1
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
c.action do |feature|
|
60
|
+
f = Flipper.feature(feature)
|
61
|
+
|
62
|
+
if values.empty?
|
63
|
+
f.send(action)
|
64
|
+
else
|
65
|
+
values.each { |value| f.send(action, value) }
|
66
|
+
end
|
67
|
+
|
68
|
+
ui.info feature_details(f)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
command 'list' do |c|
|
74
|
+
c.description = "List defined features"
|
75
|
+
c.action do
|
76
|
+
ui.info feature_summary(Flipper.features)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
command 'show' do |c|
|
81
|
+
c.description = "Show a defined feature"
|
82
|
+
c.action do |feature|
|
83
|
+
ui.info feature_details(Flipper.feature(feature))
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
command 'help' do |c|
|
88
|
+
c.load_environment = false
|
89
|
+
c.action do |command = nil|
|
90
|
+
ui.info command ? @commands[command].help : help
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
on_tail('-r path', "The path to load your application. Default: #{@require}") do |path|
|
95
|
+
@require = path
|
96
|
+
end
|
97
|
+
|
98
|
+
# Options available on all commands
|
99
|
+
on_tail('-h', '--help', 'Print help message') do
|
100
|
+
ui.info help
|
101
|
+
exit
|
102
|
+
end
|
103
|
+
|
104
|
+
# Set help documentation
|
105
|
+
self.banner = "Usage: #{program_name} [options] <command>"
|
106
|
+
separator ""
|
107
|
+
separator "Commands:"
|
108
|
+
|
109
|
+
pad = @commands.keys.map(&:length).max + 2
|
110
|
+
@commands.each do |name, command|
|
111
|
+
separator " #{name.to_s.ljust(pad, " ")} #{command.description}" if command.description
|
112
|
+
end
|
113
|
+
|
114
|
+
separator ""
|
115
|
+
separator "Options:"
|
116
|
+
end
|
117
|
+
|
118
|
+
def run(argv)
|
119
|
+
command, *args = order(argv)
|
120
|
+
|
121
|
+
if @commands[command]
|
122
|
+
load_environment! if @commands[command].load_environment
|
123
|
+
@commands[command].run(args)
|
124
|
+
else
|
125
|
+
ui.info help
|
126
|
+
|
127
|
+
if command
|
128
|
+
ui.error "Unknown command: #{command}"
|
129
|
+
exit 1
|
130
|
+
end
|
131
|
+
end
|
132
|
+
rescue OptionParser::InvalidOption => e
|
133
|
+
ui.error e.message
|
134
|
+
exit 1
|
135
|
+
end
|
136
|
+
|
137
|
+
# Helper method to define a new command
|
138
|
+
def command(name, &block)
|
139
|
+
@commands[name] = Command.new(program_name: "#{program_name} #{name}")
|
140
|
+
block.call(@commands[name])
|
141
|
+
end
|
142
|
+
|
143
|
+
def load_environment!
|
144
|
+
ENV["FLIPPER_CLOUD_LOGGING_ENABLED"] ||= "false"
|
145
|
+
require File.expand_path(@require)
|
146
|
+
# Ensure all of flipper gets loaded if it hasn't already.
|
147
|
+
require 'flipper'
|
148
|
+
rescue LoadError => e
|
149
|
+
ui.error e.message
|
150
|
+
exit 1
|
151
|
+
end
|
152
|
+
|
153
|
+
def feature_summary(features)
|
154
|
+
features = Array(features)
|
155
|
+
padding = features.map { |f| f.key.to_s.length }.max
|
156
|
+
|
157
|
+
features.map do |feature|
|
158
|
+
summary = case feature.state
|
159
|
+
when :on
|
160
|
+
colorize("⏺ enabled", [:GREEN])
|
161
|
+
when :off
|
162
|
+
"⦸ disabled"
|
163
|
+
else
|
164
|
+
"#{colorize("◯ enabled", [:YELLOW])} for " + feature.enabled_gates.map do |gate|
|
165
|
+
case gate.name
|
166
|
+
when :actor
|
167
|
+
pluralize feature.actors_value.size, 'actor', 'actors'
|
168
|
+
when :group
|
169
|
+
pluralize feature.groups_value.size, 'group', 'groups'
|
170
|
+
when :percentage_of_actors
|
171
|
+
"#{feature.percentage_of_actors_value}% of actors"
|
172
|
+
when :percentage_of_time
|
173
|
+
"#{feature.percentage_of_time_value}% of time"
|
174
|
+
when :expression
|
175
|
+
"an expression"
|
176
|
+
end
|
177
|
+
end.join(', ')
|
178
|
+
end
|
179
|
+
|
180
|
+
colorize("%-#{padding}s" % feature.key, [:BOLD, :WHITE]) + " is #{summary}"
|
181
|
+
end.join("\n")
|
182
|
+
end
|
183
|
+
|
184
|
+
def feature_details(feature)
|
185
|
+
summary = case feature.state
|
186
|
+
when :on
|
187
|
+
colorize("⏺ enabled", [:GREEN])
|
188
|
+
when :off
|
189
|
+
"⦸ disabled"
|
190
|
+
else
|
191
|
+
lines = feature.enabled_gates.map do |gate|
|
192
|
+
case gate.name
|
193
|
+
when :actor
|
194
|
+
[ pluralize(feature.actors_value.size, 'actor', 'actors') ] +
|
195
|
+
feature.actors_value.map { |actor| "- #{actor}" }
|
196
|
+
when :group
|
197
|
+
[ pluralize(feature.groups_value.size, 'group', 'groups') ] +
|
198
|
+
feature.groups_value.map { |group| " - #{group}" }
|
199
|
+
when :percentage_of_actors
|
200
|
+
"#{feature.percentage_of_actors_value}% of actors"
|
201
|
+
when :percentage_of_time
|
202
|
+
"#{feature.percentage_of_time_value}% of time"
|
203
|
+
when :expression
|
204
|
+
json = indent(JSON.pretty_generate(feature.expression_value), 2)
|
205
|
+
"the expression: \n#{colorize(json, [:MAGENTA])}"
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
"#{colorize("◯ conditionally enabled", [:YELLOW])} for:\n" +
|
210
|
+
indent(lines.flatten.join("\n"), 2)
|
211
|
+
end
|
212
|
+
|
213
|
+
"#{colorize(feature.key, [:BOLD, :WHITE])} is #{summary}"
|
214
|
+
end
|
215
|
+
|
216
|
+
def pluralize(count, singular, plural)
|
217
|
+
"#{count} #{count == 1 ? singular : plural}"
|
218
|
+
end
|
219
|
+
|
220
|
+
def colorize(text, colors)
|
221
|
+
ui.add_color(text, *colors)
|
222
|
+
end
|
223
|
+
|
224
|
+
def ui
|
225
|
+
@ui ||= Bundler::UI::Shell.new.tap do |ui|
|
226
|
+
ui.shell = shell
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def indent(text, spaces)
|
231
|
+
text.gsub(/^/, " " * spaces)
|
232
|
+
end
|
233
|
+
|
234
|
+
# Redirect the shell's output to the given stdout and stderr streams
|
235
|
+
module ShellOutput
|
236
|
+
attr_reader :stdout, :stderr
|
237
|
+
|
238
|
+
def redirect(stdout: $stdout, stderr: $stderr)
|
239
|
+
@stdout, @stderr = stdout, stderr
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
class Command < OptionParser
|
244
|
+
attr_accessor :description, :load_environment
|
245
|
+
|
246
|
+
def initialize(program_name: nil)
|
247
|
+
super()
|
248
|
+
@program_name = program_name
|
249
|
+
@load_environment = true
|
250
|
+
@action = lambda { }
|
251
|
+
end
|
252
|
+
|
253
|
+
def run(argv)
|
254
|
+
# Parse argv and call action with arguments
|
255
|
+
@action.call(*permute(argv))
|
256
|
+
end
|
257
|
+
|
258
|
+
def action(&block)
|
259
|
+
@action = block
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
@@ -163,7 +163,8 @@ module Flipper
|
|
163
163
|
max_retries: 0, # we'll handle retries ourselves
|
164
164
|
debug_output: @debug_output,
|
165
165
|
headers: {
|
166
|
-
"
|
166
|
+
"flipper-cloud-token" => @token,
|
167
|
+
"accept-encoding" => "gzip",
|
167
168
|
},
|
168
169
|
})
|
169
170
|
end
|
@@ -173,7 +174,7 @@ module Flipper
|
|
173
174
|
end
|
174
175
|
|
175
176
|
def setup_log(options)
|
176
|
-
set_option :logging_enabled, options, default:
|
177
|
+
set_option :logging_enabled, options, default: false, typecast: :boolean
|
177
178
|
set_option :logger, options, from_env: false, default: -> {
|
178
179
|
if logging_enabled
|
179
180
|
Logger.new(STDOUT)
|
@@ -186,6 +187,11 @@ module Flipper
|
|
186
187
|
def setup_http(options)
|
187
188
|
set_option :url, options, default: DEFAULT_URL
|
188
189
|
set_option :debug_output, options, from_env: false
|
190
|
+
|
191
|
+
if @debug_output.nil? && Flipper::Typecast.to_boolean(ENV["FLIPPER_CLOUD_DEBUG_OUTPUT_STDOUT"])
|
192
|
+
@debug_output = STDOUT
|
193
|
+
end
|
194
|
+
|
189
195
|
set_option :read_timeout, options, default: 5, typecast: :float, minimum: 0.1
|
190
196
|
set_option :open_timeout, options, default: 2, typecast: :float, minimum: 0.1
|
191
197
|
set_option :write_timeout, options, default: 5, typecast: :float, minimum: 0.1
|
@@ -208,8 +214,7 @@ module Flipper
|
|
208
214
|
Telemetry.instance_for(self)
|
209
215
|
}
|
210
216
|
|
211
|
-
|
212
|
-
set_option :telemetry_enabled, options, default: false, typecast: :boolean
|
217
|
+
set_option :telemetry_enabled, options, default: true, typecast: :boolean
|
213
218
|
instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
|
214
219
|
@instrumenter = if telemetry_enabled
|
215
220
|
Telemetry::Instrumenter.new(self, instrumenter)
|
@@ -24,7 +24,7 @@ module Flipper
|
|
24
24
|
if request.post? && (request.path_info.match(ROOT_PATH) || request.path_info.match(WEBHOOK_PATH))
|
25
25
|
status = 200
|
26
26
|
headers = {
|
27
|
-
|
27
|
+
Rack::CONTENT_TYPE => "application/json",
|
28
28
|
}
|
29
29
|
body = "{}"
|
30
30
|
payload = request.body.read
|
@@ -41,12 +41,12 @@ module Flipper
|
|
41
41
|
})
|
42
42
|
rescue Flipper::Adapters::Http::Error => error
|
43
43
|
status = error.response.code.to_i == 402 ? 402 : 500
|
44
|
-
headers["
|
45
|
-
headers["
|
44
|
+
headers["flipper-cloud-response-error-class"] = error.class.name
|
45
|
+
headers["flipper-cloud-response-error-message"] = error.message
|
46
46
|
rescue => error
|
47
47
|
status = 500
|
48
|
-
headers["
|
49
|
-
headers["
|
48
|
+
headers["flipper-cloud-response-error-class"] = error.class.name
|
49
|
+
headers["flipper-cloud-response-error-message"] = error.message
|
50
50
|
end
|
51
51
|
end
|
52
52
|
rescue MessageVerifier::InvalidSignature
|
@@ -3,9 +3,11 @@ require "delegate"
|
|
3
3
|
module Flipper
|
4
4
|
module Cloud
|
5
5
|
class Telemetry
|
6
|
-
class Instrumenter
|
6
|
+
class Instrumenter
|
7
|
+
attr_reader :instrumenter
|
8
|
+
|
7
9
|
def initialize(cloud_configuration, instrumenter)
|
8
|
-
|
10
|
+
@instrumenter = instrumenter
|
9
11
|
@cloud_configuration = cloud_configuration
|
10
12
|
end
|
11
13
|
|
@@ -14,12 +16,6 @@ module Flipper
|
|
14
16
|
@cloud_configuration.telemetry.record(name, payload)
|
15
17
|
return_value
|
16
18
|
end
|
17
|
-
|
18
|
-
private
|
19
|
-
|
20
|
-
def instrumenter
|
21
|
-
__getobj__
|
22
|
-
end
|
23
19
|
end
|
24
20
|
end
|
25
21
|
end
|
@@ -78,8 +78,8 @@ module Flipper
|
|
78
78
|
|
79
79
|
def submit(body)
|
80
80
|
client = @cloud_configuration.http_client
|
81
|
-
client.add_header
|
82
|
-
client.add_header
|
81
|
+
client.add_header "schema-version", SCHEMA_VERSION
|
82
|
+
client.add_header "content-encoding", GZIP_ENCODING
|
83
83
|
|
84
84
|
response = client.post PATH, body
|
85
85
|
code = response.code.to_i
|
@@ -160,8 +160,16 @@ module Flipper
|
|
160
160
|
# thus may have a telemetry-interval header for us to respect.
|
161
161
|
response ||= error.response if error && error.respond_to?(:response)
|
162
162
|
|
163
|
-
if response
|
164
|
-
|
163
|
+
if response
|
164
|
+
if Flipper::Typecast.to_boolean(response["telemetry-shutdown"])
|
165
|
+
debug "action=telemetry_shutdown message=The server has requested that telemetry be shut down."
|
166
|
+
stop
|
167
|
+
return
|
168
|
+
end
|
169
|
+
|
170
|
+
if interval = response["telemetry-interval"]
|
171
|
+
self.interval = interval.to_f
|
172
|
+
end
|
165
173
|
end
|
166
174
|
rescue => error
|
167
175
|
error "action=post_to_cloud error=#{error.inspect}"
|
data/lib/flipper/cloud.rb
CHANGED
@@ -24,7 +24,7 @@ module Flipper
|
|
24
24
|
env_key = options.fetch(:env_key, 'flipper')
|
25
25
|
memoizer_options = options.fetch(:memoizer_options, {})
|
26
26
|
|
27
|
-
app = ->(_) { [404, {
|
27
|
+
app = ->(_) { [404, { Rack::CONTENT_TYPE => 'application/json'.freeze }, ['{}'.freeze]] }
|
28
28
|
builder = Rack::Builder.new
|
29
29
|
yield builder if block_given?
|
30
30
|
builder.use Flipper::Middleware::SetupEnv, flipper, env_key: env_key
|
data/lib/flipper/engine.rb
CHANGED
@@ -1,5 +1,19 @@
|
|
1
1
|
module Flipper
|
2
2
|
class Engine < Rails::Engine
|
3
|
+
def self.default_strict_value
|
4
|
+
value = ENV["FLIPPER_STRICT"]
|
5
|
+
if value.in?(["warn", "raise", "noop"])
|
6
|
+
value.to_sym
|
7
|
+
elsif value
|
8
|
+
Typecast.to_boolean(value) ? :raise : false
|
9
|
+
elsif Rails.env.production?
|
10
|
+
false
|
11
|
+
else
|
12
|
+
# Warn in development for now. Future versions may default to :raise in development and test
|
13
|
+
Rails.env.development? && :warn
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
3
17
|
paths["config/routes.rb"] = ["lib/flipper/cloud/routes.rb"]
|
4
18
|
|
5
19
|
config.before_configuration do
|
@@ -10,7 +24,9 @@ module Flipper
|
|
10
24
|
instrumenter: ENV.fetch('FLIPPER_INSTRUMENTER', 'ActiveSupport::Notifications').constantize,
|
11
25
|
log: ENV.fetch('FLIPPER_LOG', 'true').casecmp('true').zero?,
|
12
26
|
cloud_path: "_flipper",
|
13
|
-
strict: default_strict_value
|
27
|
+
strict: default_strict_value,
|
28
|
+
actor_limit: ENV["FLIPPER_ACTOR_LIMIT"]&.to_i || 100,
|
29
|
+
test_help: Flipper::Typecast.to_boolean(ENV["FLIPPER_TEST_HELP"] || Rails.env.test?),
|
14
30
|
)
|
15
31
|
end
|
16
32
|
|
@@ -29,10 +45,6 @@ module Flipper
|
|
29
45
|
require 'flipper/cloud' if cloud?
|
30
46
|
|
31
47
|
Flipper.configure do |config|
|
32
|
-
if app.config.flipper.strict
|
33
|
-
config.use Flipper::Adapters::Strict, app.config.flipper.strict
|
34
|
-
end
|
35
|
-
|
36
48
|
config.default do
|
37
49
|
if cloud?
|
38
50
|
Flipper::Cloud.new(
|
@@ -54,6 +66,15 @@ module Flipper
|
|
54
66
|
end
|
55
67
|
end
|
56
68
|
|
69
|
+
initializer "flipper.adapters", after: :load_config_initializers do |app|
|
70
|
+
flipper = app.config.flipper
|
71
|
+
|
72
|
+
Flipper.configure do |config|
|
73
|
+
config.use Flipper::Adapters::Strict, flipper.strict if flipper.strict
|
74
|
+
config.use Flipper::Adapters::ActorLimit, flipper.actor_limit if flipper.actor_limit
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
57
78
|
initializer "flipper.memoizer", after: :load_config_initializers do |app|
|
58
79
|
flipper = app.config.flipper
|
59
80
|
|
@@ -66,22 +87,16 @@ module Flipper
|
|
66
87
|
end
|
67
88
|
end
|
68
89
|
|
90
|
+
initializer "flipper.test" do |app|
|
91
|
+
require "flipper/test_help" if app.config.flipper.test_help
|
92
|
+
end
|
93
|
+
|
69
94
|
def cloud?
|
70
95
|
!!ENV["FLIPPER_CLOUD_TOKEN"]
|
71
96
|
end
|
72
97
|
|
73
|
-
def self.
|
74
|
-
|
75
|
-
if value.in?(["warn", "raise", "noop"])
|
76
|
-
value.to_sym
|
77
|
-
elsif value
|
78
|
-
Typecast.to_boolean(value) ? :raise : false
|
79
|
-
elsif Rails.env.production?
|
80
|
-
false
|
81
|
-
else
|
82
|
-
# Warn for now. Future versions will default to :raise in development and test
|
83
|
-
:warn
|
84
|
-
end
|
98
|
+
def self.deprecated_rails_version?
|
99
|
+
Gem::Version.new(Rails.version) < Gem::Version.new(Flipper::NEXT_REQUIRED_RAILS_VERSION)
|
85
100
|
end
|
86
101
|
end
|
87
102
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'securerandom'
|
2
|
+
require 'active_support/gem_version'
|
2
3
|
require 'active_support/notifications'
|
3
4
|
require 'active_support/log_subscriber'
|
4
5
|
|
@@ -71,11 +72,19 @@ module Flipper
|
|
71
72
|
self.class.logger
|
72
73
|
end
|
73
74
|
|
75
|
+
def self.attach
|
76
|
+
attach_to InstrumentationNamespace
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.detach
|
80
|
+
# Rails 5.2 doesn't support this, that's fine
|
81
|
+
detach_from InstrumentationNamespace if respond_to?(:detach_from)
|
82
|
+
end
|
83
|
+
|
74
84
|
private
|
75
85
|
|
76
86
|
# Rails 7.1 changed the signature of this function.
|
77
|
-
|
78
|
-
COLOR_OPTIONS = if Rails.gem_version > Gem::Version.new('7.0.99')
|
87
|
+
COLOR_OPTIONS = if Gem::Requirement.new(">=7.1").satisfied_by?(ActiveSupport.gem_version)
|
79
88
|
{ bold: true }.freeze
|
80
89
|
else
|
81
90
|
true
|
@@ -88,5 +97,5 @@ module Flipper
|
|
88
97
|
end
|
89
98
|
end
|
90
99
|
|
91
|
-
Instrumentation::LogSubscriber.
|
100
|
+
Instrumentation::LogSubscriber.attach
|
92
101
|
end
|
data/lib/flipper/metadata.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
+
require_relative './version'
|
2
|
+
|
1
3
|
module Flipper
|
2
4
|
METADATA = {
|
3
5
|
"documentation_uri" => "https://www.flippercloud.io/docs",
|
4
6
|
"homepage_uri" => "https://www.flippercloud.io",
|
5
7
|
"source_code_uri" => "https://github.com/flippercloud/flipper",
|
6
8
|
"bug_tracker_uri" => "https://github.com/flippercloud/flipper/issues",
|
7
|
-
"changelog_uri" => "https://github.com/flippercloud/flipper/
|
9
|
+
"changelog_uri" => "https://github.com/flippercloud/flipper/releases/tag/v#{Flipper::VERSION}",
|
8
10
|
}.freeze
|
9
11
|
end
|
data/lib/flipper/poller.rb
CHANGED
@@ -20,6 +20,8 @@ module Flipper
|
|
20
20
|
instances.each {|_, instance| instance.stop }.clear
|
21
21
|
end
|
22
22
|
|
23
|
+
MINIMUM_POLL_INTERVAL = 10
|
24
|
+
|
23
25
|
def initialize(options = {})
|
24
26
|
@thread = nil
|
25
27
|
@pid = Process.pid
|
@@ -30,9 +32,9 @@ module Flipper
|
|
30
32
|
@last_synced_at = Concurrent::AtomicFixnum.new(0)
|
31
33
|
@adapter = Adapters::Memory.new(nil, threadsafe: true)
|
32
34
|
|
33
|
-
if @interval <
|
34
|
-
warn "Flipper::Cloud poll interval must be greater than or equal to
|
35
|
-
@interval =
|
35
|
+
if @interval < MINIMUM_POLL_INTERVAL
|
36
|
+
warn "Flipper::Cloud poll interval must be greater than or equal to #{MINIMUM_POLL_INTERVAL} but was #{@interval}. Setting @interval to #{MINIMUM_POLL_INTERVAL}."
|
37
|
+
@interval = MINIMUM_POLL_INTERVAL
|
36
38
|
end
|
37
39
|
|
38
40
|
@start_automatically = options.fetch(:start_automatically, true)
|
@@ -64,8 +66,7 @@ module Flipper
|
|
64
66
|
# you can instrument these using poller.flipper
|
65
67
|
end
|
66
68
|
|
67
|
-
|
68
|
-
sleep sleep_interval if sleep_interval.positive?
|
69
|
+
sleep interval
|
69
70
|
end
|
70
71
|
end
|
71
72
|
|
@@ -3,10 +3,8 @@ require "stringio"
|
|
3
3
|
|
4
4
|
module Flipper
|
5
5
|
module Serializers
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
def serialize(source)
|
6
|
+
class Gzip
|
7
|
+
def self.serialize(source)
|
10
8
|
return if source.nil?
|
11
9
|
output = StringIO.new
|
12
10
|
gz = Zlib::GzipWriter.new(output)
|
@@ -15,7 +13,7 @@ module Flipper
|
|
15
13
|
output.string
|
16
14
|
end
|
17
15
|
|
18
|
-
def deserialize(source)
|
16
|
+
def self.deserialize(source)
|
19
17
|
return if source.nil?
|
20
18
|
Zlib::GzipReader.wrap(StringIO.new(source), &:read)
|
21
19
|
end
|
@@ -2,15 +2,13 @@ require "json"
|
|
2
2
|
|
3
3
|
module Flipper
|
4
4
|
module Serializers
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
def serialize(source)
|
5
|
+
class Json
|
6
|
+
def self.serialize(source)
|
9
7
|
return if source.nil?
|
10
8
|
JSON.generate(source)
|
11
9
|
end
|
12
10
|
|
13
|
-
def deserialize(source)
|
11
|
+
def self.deserialize(source)
|
14
12
|
return if source.nil?
|
15
13
|
JSON.parse(source)
|
16
14
|
end
|
@@ -108,19 +108,19 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
108
108
|
actor22 = Flipper::Actor.new('22')
|
109
109
|
actor_asdf = Flipper::Actor.new('asdf')
|
110
110
|
|
111
|
-
expect(
|
112
|
-
expect(
|
111
|
+
expect(feature.enable(actor22)).to be(true)
|
112
|
+
expect(feature.enable(actor_asdf)).to be(true)
|
113
113
|
|
114
|
-
|
115
|
-
expect(
|
114
|
+
expect(feature).to be_enabled(actor22)
|
115
|
+
expect(feature).to be_enabled(actor_asdf)
|
116
116
|
|
117
|
-
expect(
|
118
|
-
|
119
|
-
expect(
|
117
|
+
expect(feature.disable(actor22)).to be(true)
|
118
|
+
expect(feature).not_to be_enabled(actor22)
|
119
|
+
expect(feature).to be_enabled(actor_asdf)
|
120
120
|
|
121
|
-
expect(
|
122
|
-
|
123
|
-
expect(
|
121
|
+
expect(feature.disable(actor_asdf)).to eq(true)
|
122
|
+
expect(feature).not_to be_enabled(actor22)
|
123
|
+
expect(feature).not_to be_enabled(actor_asdf)
|
124
124
|
end
|
125
125
|
|
126
126
|
it 'can enable, disable and get value for percentage of actors gate' do
|
@@ -182,9 +182,10 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
182
182
|
end
|
183
183
|
|
184
184
|
it 'converts the actor value to a string' do
|
185
|
-
|
186
|
-
|
187
|
-
|
185
|
+
actor = Flipper::Actor.new(22)
|
186
|
+
expect(feature).not_to be_enabled(actor)
|
187
|
+
feature.enable_actor actor
|
188
|
+
expect(feature).to be_enabled(actor)
|
188
189
|
end
|
189
190
|
|
190
191
|
it 'converts group value to a string' do
|
@@ -295,9 +296,9 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
295
296
|
|
296
297
|
it 'can double enable an actor without error' do
|
297
298
|
actor = Flipper::Actor.new('Flipper::Actor;22')
|
298
|
-
expect(
|
299
|
-
expect(
|
300
|
-
expect(
|
299
|
+
expect(feature.enable(actor)).to be(true)
|
300
|
+
expect(feature.enable(actor)).to be(true)
|
301
|
+
expect(feature).to be_enabled(actor)
|
301
302
|
end
|
302
303
|
|
303
304
|
it 'can double enable a group without error' do
|