flipper 1.1.2 → 1.2.1

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +7 -1
  3. data/.github/workflows/examples.yml +7 -1
  4. data/Changelog.md +1 -647
  5. data/Gemfile +3 -1
  6. data/README.md +1 -1
  7. data/Rakefile +2 -2
  8. data/exe/flipper +5 -0
  9. data/flipper.gemspec +5 -1
  10. data/lib/flipper/adapters/http/client.rb +25 -16
  11. data/lib/flipper/adapters/strict.rb +11 -8
  12. data/lib/flipper/cli.rb +240 -0
  13. data/lib/flipper/cloud/configuration.rb +7 -1
  14. data/lib/flipper/cloud/middleware.rb +5 -5
  15. data/lib/flipper/cloud/telemetry/submitter.rb +2 -2
  16. data/lib/flipper/cloud.rb +1 -1
  17. data/lib/flipper/engine.rb +32 -17
  18. data/lib/flipper/instrumentation/log_subscriber.rb +12 -3
  19. data/lib/flipper/metadata.rb +3 -1
  20. data/lib/flipper/test_help.rb +38 -0
  21. data/lib/flipper/version.rb +11 -1
  22. data/lib/generators/flipper/setup_generator.rb +63 -0
  23. data/spec/fixtures/environment.rb +1 -0
  24. data/spec/flipper/adapter_builder_spec.rb +1 -2
  25. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  26. data/spec/flipper/adapters/http_spec.rb +92 -75
  27. data/spec/flipper/adapters/strict_spec.rb +11 -9
  28. data/spec/flipper/cli_spec.rb +164 -0
  29. data/spec/flipper/cloud/configuration_spec.rb +9 -2
  30. data/spec/flipper/cloud/dsl_spec.rb +5 -5
  31. data/spec/flipper/cloud/middleware_spec.rb +8 -8
  32. data/spec/flipper/cloud/telemetry/submitter_spec.rb +24 -24
  33. data/spec/flipper/cloud/telemetry_spec.rb +1 -1
  34. data/spec/flipper/cloud_spec.rb +4 -4
  35. data/spec/flipper/engine_spec.rb +76 -11
  36. data/spec/flipper/instrumentation/log_subscriber_spec.rb +9 -2
  37. data/spec/flipper_spec.rb +1 -1
  38. data/spec/spec_helper.rb +1 -0
  39. data/spec/support/spec_helpers.rb +10 -4
  40. data/test_rails/generators/flipper/setup_generator_test.rb +64 -0
  41. data/test_rails/system/test_help_test.rb +46 -0
  42. metadata +20 -7
data/Gemfile CHANGED
@@ -13,7 +13,7 @@ gem 'rspec', '~> 3.0'
13
13
  gem 'rack-test'
14
14
  gem 'rackup'
15
15
  gem 'sqlite3', "~> #{ENV['SQLITE3_VERSION'] || '1.4.1'}"
16
- gem 'rails', "~> #{ENV['RAILS_VERSION'] || '7.0.4'}"
16
+ gem 'rails', "~> #{ENV['RAILS_VERSION'] || '7.1'}"
17
17
  gem 'minitest', '~> 5.18'
18
18
  gem 'minitest-documentation'
19
19
  gem 'webmock'
@@ -27,6 +27,8 @@ gem 'flamegraph'
27
27
  gem 'climate_control'
28
28
  gem 'mysql2'
29
29
  gem 'pg'
30
+ gem 'cuprite'
31
+ gem 'puma'
30
32
 
31
33
  group(:guard) do
32
34
  gem 'guard'
data/README.md CHANGED
@@ -99,7 +99,7 @@ We also have a [free plan](https://www.flippercloud.io?utm_source=oss&utm_medium
99
99
 
100
100
  1. Update the version to be whatever it should be and commit.
101
101
  2. `script/release`
102
- 3. Profit.
102
+ 3. Create a new [GitHub Release](https://github.com/flippercloud/flipper/releases/new)
103
103
 
104
104
  ## Brought To You By
105
105
 
data/Rakefile CHANGED
@@ -27,7 +27,8 @@ end
27
27
 
28
28
  require 'rspec/core/rake_task'
29
29
  RSpec::Core::RakeTask.new(:spec) do |t|
30
- t.rspec_opts = %w(--color --format documentation)
30
+ t.rspec_opts = %w(--color)
31
+ t.verbose = false
31
32
  end
32
33
 
33
34
  namespace :spec do
@@ -41,7 +42,6 @@ end
41
42
  Rake::TestTask.new do |t|
42
43
  t.libs = %w(lib test)
43
44
  t.pattern = 'test/**/*_test.rb'
44
- t.options = '--documentation'
45
45
  t.warning = false
46
46
  end
47
47
 
data/exe/flipper ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "flipper/cli"
4
+
5
+ Flipper::CLI.run(ARGV)
data/flipper.gemspec CHANGED
@@ -6,7 +6,7 @@ plugin_files = []
6
6
  plugin_test_files = []
7
7
 
8
8
  Dir['flipper-*.gemspec'].map do |gemspec|
9
- spec = eval(File.read(gemspec))
9
+ spec = Gem::Specification.load(gemspec)
10
10
  plugin_files << spec.files
11
11
  plugin_test_files << spec.files
12
12
  end
@@ -27,6 +27,8 @@ Gem::Specification.new do |gem|
27
27
  gem.homepage = 'https://www.flippercloud.io/docs'
28
28
  gem.license = 'MIT'
29
29
 
30
+ gem.bindir = "exe"
31
+ gem.executables = `git ls-files -- exe/*`.split("\n").map { |f| File.basename(f) }
30
32
  gem.files = `git ls-files`.split("\n") - ignored_files + ['lib/flipper/version.rb']
31
33
  gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") - ignored_test_files
32
34
  gem.name = 'flipper'
@@ -35,4 +37,6 @@ Gem::Specification.new do |gem|
35
37
  gem.metadata = Flipper::METADATA
36
38
 
37
39
  gem.add_dependency 'concurrent-ruby', '< 2'
40
+
41
+ gem.required_ruby_version = ">= #{Flipper::REQUIRED_RUBY_VERSION}"
38
42
  end
@@ -7,26 +7,28 @@ module Flipper
7
7
  class Http
8
8
  class Client
9
9
  DEFAULT_HEADERS = {
10
- 'Content-Type' => 'application/json',
11
- 'Accept' => 'application/json',
12
- 'User-Agent' => "Flipper HTTP Adapter v#{VERSION}",
10
+ 'content-type' => 'application/json',
11
+ 'accept' => 'application/json',
12
+ 'user-agent' => "Flipper HTTP Adapter v#{VERSION}",
13
13
  }.freeze
14
14
 
15
15
  HTTPS_SCHEME = "https".freeze
16
16
 
17
17
  CLIENT_FRAMEWORKS = {
18
- rails: -> { Rails.version if defined?(Rails) },
19
- sinatra: -> { Sinatra::VERSION if defined?(Sinatra) },
20
- hanami: -> { Hanami::VERSION if defined?(Hanami) },
18
+ rails: -> { Rails.version if defined?(Rails) },
19
+ sinatra: -> { Sinatra::VERSION if defined?(Sinatra) },
20
+ hanami: -> { Hanami::VERSION if defined?(Hanami) },
21
+ sidekiq: -> { Sidekiq::VERSION if defined?(Sidekiq) },
22
+ good_job: -> { GoodJob::VERSION if defined?(GoodJob) },
21
23
  }
22
24
 
23
25
  attr_reader :uri, :headers
24
26
  attr_reader :basic_auth_username, :basic_auth_password
25
- attr_reader :read_timeout, :open_timeout, :write_timeout, :max_retries, :debug_output
27
+ attr_reader :read_timeout, :open_timeout, :write_timeout
28
+ attr_reader :max_retries, :debug_output
26
29
 
27
30
  def initialize(options = {})
28
31
  @uri = URI(options.fetch(:url))
29
- @headers = DEFAULT_HEADERS.merge(options[:headers] || {})
30
32
  @basic_auth_username = options[:basic_auth_username]
31
33
  @basic_auth_password = options[:basic_auth_password]
32
34
  @read_timeout = options[:read_timeout]
@@ -34,9 +36,16 @@ module Flipper
34
36
  @write_timeout = options[:write_timeout]
35
37
  @max_retries = options.key?(:max_retries) ? options[:max_retries] : 0
36
38
  @debug_output = options[:debug_output]
39
+
40
+ @headers = {}
41
+ DEFAULT_HEADERS.each { |key, value| add_header key, value }
42
+ if options[:headers]
43
+ options[:headers].each { |key, value| add_header key, value }
44
+ end
37
45
  end
38
46
 
39
47
  def add_header(key, value)
48
+ key = key.to_s.downcase.gsub('_'.freeze, '-'.freeze).freeze
40
49
  @headers[key] = value
41
50
  end
42
51
 
@@ -87,13 +96,13 @@ module Flipper
87
96
 
88
97
  def build_request(http_method, uri, headers, options)
89
98
  request_headers = {
90
- client_language: "ruby",
91
- client_language_version: "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})",
92
- client_platform: RUBY_PLATFORM,
93
- client_engine: defined?(RUBY_ENGINE) ? RUBY_ENGINE : "",
94
- client_pid: Process.pid.to_s,
95
- client_thread: Thread.current.object_id.to_s,
96
- client_hostname: Socket.gethostname,
99
+ 'client-language' => "ruby",
100
+ 'client-language-version' => "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})",
101
+ 'client-platform' => RUBY_PLATFORM,
102
+ 'client-engine' => defined?(RUBY_ENGINE) ? RUBY_ENGINE : "",
103
+ 'client-pid' => Process.pid.to_s,
104
+ 'client-thread' => Thread.current.object_id.to_s,
105
+ 'client-hostname' => Socket.gethostname,
97
106
  }.merge(headers)
98
107
 
99
108
  body = options[:body]
@@ -101,7 +110,7 @@ module Flipper
101
110
  request.initialize_http_header(request_headers)
102
111
 
103
112
  client_frameworks.each do |framework, version|
104
- request.add_field("Client-Framework", [framework, version].join("="))
113
+ request.add_field("client-framework", [framework, version].join("="))
105
114
  end
106
115
 
107
116
  request.body = body if body
@@ -12,18 +12,12 @@ module Flipper
12
12
  end
13
13
  end
14
14
 
15
- HANDLERS = {
16
- raise: ->(feature) { raise NotFound.new(feature.key) },
17
- warn: ->(feature) { warn NotFound.new(feature.key).message },
18
- noop: ->(_) { },
19
- }
20
-
21
15
  def_delegators :@adapter, :features, :get_all, :add, :remove, :clear, :enable, :disable
22
16
 
23
17
  def initialize(adapter, handler = nil, &block)
24
18
  @name = :strict
25
19
  @adapter = adapter
26
- @handler = block || HANDLERS.fetch(handler)
20
+ @handler = block || handler
27
21
  end
28
22
 
29
23
  def get(feature)
@@ -39,7 +33,16 @@ module Flipper
39
33
  private
40
34
 
41
35
  def assert_feature_exists(feature)
42
- @handler.call(feature) unless @adapter.features.include?(feature.key)
36
+ return if @adapter.features.include?(feature.key)
37
+
38
+ case handler
39
+ when Proc then handler.call(feature)
40
+ when :warn then warn NotFound.new(feature.key).message
41
+ when :noop, false, nil
42
+ # noop
43
+ else # truthy or :raise
44
+ raise NotFound.new(feature.key)
45
+ end
43
46
  end
44
47
 
45
48
  end
@@ -0,0 +1,240 @@
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
+ def initialize
13
+ super
14
+
15
+ # Program is always flipper, no matter how it's invoked
16
+ @program_name = 'flipper'
17
+ @require = ENV.fetch("FLIPPER_REQUIRE", DEFAULT_REQUIRE)
18
+ @commands = {}
19
+
20
+ %w[enable disable].each do |action|
21
+ command action do |c|
22
+ c.banner = "Usage: #{c.program_name} [options] <feature>"
23
+ c.description = "#{action.to_s.capitalize} a feature"
24
+
25
+ values = []
26
+
27
+ c.on('-a id', '--actor=id', "#{action} for an actor") do |id|
28
+ values << Actor.new(id)
29
+ end
30
+ c.on('-g name', '--group=name', "#{action} for a group") do |name|
31
+ values << Types::Group.new(name)
32
+ end
33
+ c.on('-p NUM', '--percentage-of-actors=NUM', Numeric, "#{action} for a percentage of actors") do |num|
34
+ values << Types::PercentageOfActors.new(num)
35
+ end
36
+ c.on('-t NUM', '--percentage-of-time=NUM', Numeric, "#{action} for a percentage of time") do |num|
37
+ values << Types::PercentageOfTime.new(num)
38
+ end
39
+ c.on('-x expressions', '--expression=NUM', "#{action} for the given expression") do |expression|
40
+ begin
41
+ values << Flipper::Expression.build(JSON.parse(expression))
42
+ rescue JSON::ParserError => e
43
+ warn "JSON parse error: #{e.message}"
44
+ exit 1
45
+ rescue ArgumentError => e
46
+ warn "Invalid expression: #{e.message}"
47
+ exit 1
48
+ end
49
+ end
50
+
51
+ c.action do |feature|
52
+ f = Flipper.feature(feature)
53
+
54
+ if values.empty?
55
+ f.send(action)
56
+ else
57
+ values.each { |value| f.send(action, value) }
58
+ end
59
+
60
+ puts feature_details(f)
61
+ end
62
+ end
63
+ end
64
+
65
+ command 'list' do |c|
66
+ c.description = "List defined features"
67
+ c.action do
68
+ puts feature_summary(Flipper.features)
69
+ end
70
+ end
71
+
72
+ command 'show' do |c|
73
+ c.description = "Show a defined feature"
74
+ c.action do |feature|
75
+ puts feature_details(Flipper.feature(feature))
76
+ end
77
+ end
78
+
79
+ command 'help' do |c|
80
+ c.load_environment = false
81
+ c.action do |command = nil|
82
+ puts command ? @commands[command].help : help
83
+ end
84
+ end
85
+
86
+ on_tail('-r path', "The path to load your application. Default: #{@require}") do |path|
87
+ @require = path
88
+ end
89
+
90
+ # Options available on all commands
91
+ on_tail('-h', '--help', 'Print help message') do
92
+ puts help
93
+ exit
94
+ end
95
+
96
+ # Set help documentation
97
+ self.banner = "Usage: #{program_name} [options] <command>"
98
+ separator ""
99
+ separator "Commands:"
100
+
101
+ pad = @commands.keys.map(&:length).max + 2
102
+ @commands.each do |name, command|
103
+ separator " #{name.to_s.ljust(pad, " ")} #{command.description}" if command.description
104
+ end
105
+
106
+ separator ""
107
+ separator "Options:"
108
+ end
109
+
110
+ def run(argv)
111
+ command, *args = order(argv)
112
+
113
+ if @commands[command]
114
+ load_environment! if @commands[command].load_environment
115
+ @commands[command].run(args)
116
+ else
117
+ puts help
118
+
119
+ if command
120
+ warn "Unknown command: #{command}"
121
+ exit 1
122
+ end
123
+ end
124
+ rescue OptionParser::InvalidOption => e
125
+ warn e.message
126
+ exit 1
127
+ end
128
+
129
+ # Helper method to define a new command
130
+ def command(name, &block)
131
+ @commands[name] = Command.new(program_name: "#{program_name} #{name}")
132
+ block.call(@commands[name])
133
+ end
134
+
135
+ def load_environment!
136
+ ENV["FLIPPER_CLOUD_LOGGING_ENABLED"] ||= "false"
137
+ require File.expand_path(@require)
138
+ # Ensure all of flipper gets loaded if it hasn't already.
139
+ require 'flipper'
140
+ rescue LoadError => e
141
+ warn e.message
142
+ exit 1
143
+ end
144
+
145
+ def feature_summary(features)
146
+ features = Array(features)
147
+ padding = features.map { |f| f.key.to_s.length }.max
148
+
149
+ features.map do |feature|
150
+ summary = case feature.state
151
+ when :on
152
+ colorize("⏺ enabled", [:GREEN])
153
+ when :off
154
+ "⦸ disabled"
155
+ else
156
+ "#{colorize("◯ enabled", [:YELLOW])} for " + feature.enabled_gates.map do |gate|
157
+ case gate.name
158
+ when :actor
159
+ pluralize feature.actors_value.size, 'actor', 'actors'
160
+ when :group
161
+ pluralize feature.groups_value.size, 'group', 'groups'
162
+ when :percentage_of_actors
163
+ "#{feature.percentage_of_actors_value}% of actors"
164
+ when :percentage_of_time
165
+ "#{feature.percentage_of_time_value}% of time"
166
+ when :expression
167
+ "an expression"
168
+ end
169
+ end.join(', ')
170
+ end
171
+
172
+ colorize("%-#{padding}s" % feature.key, [:BOLD, :WHITE]) + " is #{summary}"
173
+ end
174
+ end
175
+
176
+ def feature_details(feature)
177
+ summary = case feature.state
178
+ when :on
179
+ colorize("⏺ enabled", [:GREEN])
180
+ when :off
181
+ "⦸ disabled"
182
+ else
183
+ lines = feature.enabled_gates.map do |gate|
184
+ case gate.name
185
+ when :actor
186
+ [ pluralize(feature.actors_value.size, 'actor', 'actors') ] +
187
+ feature.actors_value.map { |actor| "- #{actor}" }
188
+ when :group
189
+ [ pluralize(feature.groups_value.size, 'group', 'groups') ] +
190
+ feature.groups_value.map { |group| " - #{group}" }
191
+ when :percentage_of_actors
192
+ "#{feature.percentage_of_actors_value}% of actors"
193
+ when :percentage_of_time
194
+ "#{feature.percentage_of_time_value}% of time"
195
+ when :expression
196
+ json = indent(JSON.pretty_generate(feature.expression_value), 2)
197
+ "the expression: \n#{colorize(json, [:MAGENTA])}"
198
+ end
199
+ end
200
+
201
+ "#{colorize("◯ conditionally enabled", [:YELLOW])} for:\n" +
202
+ indent(lines.flatten.join("\n"), 2)
203
+ end
204
+
205
+ "#{colorize(feature.key, [:BOLD, :WHITE])} is #{summary}"
206
+ end
207
+
208
+ def pluralize(count, singular, plural)
209
+ "#{count} #{count == 1 ? singular : plural}"
210
+ end
211
+
212
+ def colorize(text, options)
213
+ IRB::Color.colorize(text, options)
214
+ end
215
+
216
+ def indent(text, spaces)
217
+ text.gsub(/^/, " " * spaces)
218
+ end
219
+
220
+ class Command < OptionParser
221
+ attr_accessor :description, :load_environment
222
+
223
+ def initialize(program_name: nil)
224
+ super()
225
+ @program_name = program_name
226
+ @load_environment = true
227
+ @action = lambda { }
228
+ end
229
+
230
+ def run(argv)
231
+ # Parse argv and call action with arguments
232
+ @action.call(*permute(argv))
233
+ end
234
+
235
+ def action(&block)
236
+ @action = block
237
+ end
238
+ end
239
+ end
240
+ 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
- "Flipper-Cloud-Token" => @token,
166
+ "flipper-cloud-token" => @token,
167
+ "accept-encoding" => "gzip",
167
168
  },
168
169
  })
169
170
  end
@@ -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
@@ -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
- "content-type" => "application/json",
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["Flipper-Cloud-Response-Error-Class"] = error.class.name
45
- headers["Flipper-Cloud-Response-Error-Message"] = error.message
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["Flipper-Cloud-Response-Error-Class"] = error.class.name
49
- headers["Flipper-Cloud-Response-Error-Message"] = error.message
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
@@ -78,8 +78,8 @@ module Flipper
78
78
 
79
79
  def submit(body)
80
80
  client = @cloud_configuration.http_client
81
- client.add_header :schema_version, SCHEMA_VERSION
82
- client.add_header :content_encoding, GZIP_ENCODING
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
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, { 'content-type'.freeze => 'application/json'.freeze }, ['{}'.freeze]] }
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
@@ -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,8 @@ 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
+ test_help: Flipper::Typecast.to_boolean(ENV["FLIPPER_TEST_HELP"] || Rails.env.test?),
14
29
  )
15
30
  end
16
31
 
@@ -29,10 +44,6 @@ module Flipper
29
44
  require 'flipper/cloud' if cloud?
30
45
 
31
46
  Flipper.configure do |config|
32
- if app.config.flipper.strict
33
- config.use Flipper::Adapters::Strict, app.config.flipper.strict
34
- end
35
-
36
47
  config.default do
37
48
  if cloud?
38
49
  Flipper::Cloud.new(
@@ -54,6 +65,16 @@ module Flipper
54
65
  end
55
66
  end
56
67
 
68
+ initializer "flipper.strict", after: :load_config_initializers do |app|
69
+ flipper = app.config.flipper
70
+
71
+ if flipper.strict
72
+ Flipper.configure do |config|
73
+ config.use Flipper::Adapters::Strict, flipper.strict
74
+ end
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.default_strict_value
74
- value = ENV["FLIPPER_STRICT"]
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
- # Checking if > 7.0.99 rather than >= 7.1 so that 7.1 pre-release versions are included.
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.attach_to InstrumentationNamespace
100
+ Instrumentation::LogSubscriber.attach
92
101
  end
@@ -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/blob/main/Changelog.md",
9
+ "changelog_uri" => "https://github.com/flippercloud/flipper/releases/tag/v#{Flipper::VERSION}",
8
10
  }.freeze
9
11
  end