flipper 0.21.0.rc1 → 0.22.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +12 -16
- data/.github/workflows/examples.yml +56 -0
- data/Changelog.md +54 -0
- data/Gemfile +1 -0
- data/README.md +2 -1
- data/docker-compose.yml +37 -34
- data/docs/Adapters.md +9 -10
- data/docs/Caveats.md +2 -2
- data/docs/Optimization.md +70 -47
- data/docs/api/README.md +5 -5
- data/docs/http/README.md +12 -11
- data/docs/read-only/README.md +8 -5
- data/examples/api/basic.ru +19 -0
- data/examples/api/custom_memoized.ru +37 -0
- data/examples/api/memoized.ru +43 -0
- data/examples/configuring_default.rb +1 -3
- data/examples/instrumentation_last_accessed_at.rb +37 -0
- data/examples/memoizing.rb +2 -5
- data/lib/flipper/adapters/pstore.rb +4 -0
- data/lib/flipper/adapters/sync/synchronizer.rb +2 -1
- data/lib/flipper/configuration.rb +29 -3
- data/lib/flipper/middleware/memoizer.rb +30 -15
- data/lib/flipper/railtie.rb +46 -0
- data/lib/flipper/version.rb +1 -1
- data/lib/flipper.rb +3 -1
- data/spec/flipper/configuration_spec.rb +16 -1
- data/spec/flipper/middleware/memoizer_spec.rb +94 -34
- data/spec/flipper/railtie_spec.rb +69 -0
- data/spec/helper.rb +2 -2
- data/spec/support/spec_helpers.rb +17 -0
- metadata +12 -4
data/docs/api/README.md
CHANGED
@@ -23,7 +23,7 @@ Or install it yourself as:
|
|
23
23
|
```ruby
|
24
24
|
# config/routes.rb
|
25
25
|
YourRailsApp::Application.routes.draw do
|
26
|
-
mount Flipper::Api.app(
|
26
|
+
mount Flipper::Api.app(Flipper) => '/flipper/api'
|
27
27
|
end
|
28
28
|
```
|
29
29
|
|
@@ -34,8 +34,8 @@ There can be more than one router in your application. Make sure if you choose a
|
|
34
34
|
*bad:*
|
35
35
|
```ruby
|
36
36
|
YourRailsApp::Application.routes.draw do
|
37
|
-
mount Flipper::UI.app(
|
38
|
-
mount Flipper::Api.app(
|
37
|
+
mount Flipper::UI.app(Flipper) => '/flipper'
|
38
|
+
mount Flipper::Api.app(Flipper) => '/flipper/api'
|
39
39
|
end
|
40
40
|
```
|
41
41
|
|
@@ -44,8 +44,8 @@ In this case any requests to /flipper\* will be routed to Flipper::UI - includin
|
|
44
44
|
*good:*
|
45
45
|
```ruby
|
46
46
|
YourRailsApp::Application.routes.draw do
|
47
|
-
mount Flipper::Api.app(
|
48
|
-
mount Flipper::UI.app(
|
47
|
+
mount Flipper::Api.app(Flipper) => '/flipper/api'
|
48
|
+
mount Flipper::UI.app(Flipper) => '/flipper'
|
49
49
|
end
|
50
50
|
````
|
51
51
|
For more advanced mounting techniques and for suggestions on how to mount in a non-Rails application, it is recommend that you review the [`Flipper::UI` usage documentation](https://github.com/jnunemaker/flipper/blob/master/docs/ui/README.md#usage) as the same approaches apply to `Flipper::Api`.
|
data/docs/http/README.md
CHANGED
@@ -8,17 +8,18 @@ Initialize the HTTP adapter with a configuration Hash.
|
|
8
8
|
```ruby
|
9
9
|
require 'flipper/adapters/http'
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
11
|
+
Flipper.configure do |config|
|
12
|
+
config.adapter do
|
13
|
+
Flipper::Adapters::Http.new({
|
14
|
+
url: 'http://app.com/mount-point', # required
|
15
|
+
headers: { 'X-Custom-Header' => 'foo' },
|
16
|
+
basic_auth_username: 'user123',
|
17
|
+
basic_auth_password: 'password123'
|
18
|
+
read_timeout: 5,
|
19
|
+
open_timeout: 2,
|
20
|
+
})
|
21
|
+
end
|
22
|
+
end
|
22
23
|
```
|
23
24
|
|
24
25
|
**Required keys**:
|
data/docs/read-only/README.md
CHANGED
@@ -5,17 +5,20 @@ A [read-only](https://github.com/jnunemaker/flipper/blob/master/lib/flipper/adap
|
|
5
5
|
Use this adapter to wrap another adapter and raise an exception for any writes.
|
6
6
|
|
7
7
|
Any attempted write raises `Flipper::Adapters::ReadOnly::WriteAttempted` with message `'write attempted while in read only mode'`
|
8
|
+
|
8
9
|
## Usage
|
10
|
+
|
9
11
|
```ruby
|
10
12
|
# example wrapping memory adapter
|
11
13
|
require 'flipper/adapters/read_only'
|
12
14
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
15
|
+
Flipper.configure do |config|
|
16
|
+
config.adapter do
|
17
|
+
Flipper::Adapters::ReadOnly.new(Flipper::Adapters::Memory.new)
|
18
|
+
end
|
19
|
+
end
|
17
20
|
|
18
21
|
# Enabling a feature
|
19
|
-
>
|
22
|
+
> Flipper[:dashboard_panel].enable
|
20
23
|
=> Flipper::Adapters::ReadOnly::WriteAttempted: write attempted while in read only mode
|
21
24
|
```
|
@@ -0,0 +1,19 @@
|
|
1
|
+
#
|
2
|
+
# Usage:
|
3
|
+
# # if you want it to not reload and be really fast
|
4
|
+
# bin/rackup examples/api/basic.ru -p 9999
|
5
|
+
#
|
6
|
+
# # if you want reloading
|
7
|
+
# bin/shotgun examples/api/basic.ru -p 9999
|
8
|
+
#
|
9
|
+
# http://localhost:9999/
|
10
|
+
#
|
11
|
+
|
12
|
+
require 'bundler/setup'
|
13
|
+
require "flipper/api"
|
14
|
+
require "flipper/adapters/pstore"
|
15
|
+
|
16
|
+
# You can uncomment this to get some default data:
|
17
|
+
# Flipper.enable :logging
|
18
|
+
|
19
|
+
run Flipper::Api.app
|
@@ -0,0 +1,37 @@
|
|
1
|
+
#
|
2
|
+
# Usage:
|
3
|
+
# # if you want it to not reload and be really fast
|
4
|
+
# bin/rackup examples/api/custom_memoized.ru -p 9999
|
5
|
+
#
|
6
|
+
# # if you want reloading
|
7
|
+
# bin/shotgun examples/api/custom_memoized.ru -p 9999
|
8
|
+
#
|
9
|
+
# http://localhost:9999/
|
10
|
+
#
|
11
|
+
|
12
|
+
require 'bundler/setup'
|
13
|
+
require "active_support/notifications"
|
14
|
+
require "flipper/api"
|
15
|
+
require "flipper/adapters/pstore"
|
16
|
+
|
17
|
+
adapter = Flipper::Adapters::Instrumented.new(
|
18
|
+
Flipper::Adapters::PStore.new,
|
19
|
+
instrumenter: ActiveSupport::Notifications,
|
20
|
+
)
|
21
|
+
flipper = Flipper.new(adapter)
|
22
|
+
|
23
|
+
ActiveSupport::Notifications.subscribe(/.*/, ->(*args) {
|
24
|
+
name, start, finish, id, data = args
|
25
|
+
case name
|
26
|
+
when "adapter_operation.flipper"
|
27
|
+
p data[:adapter_name] => data[:operation]
|
28
|
+
end
|
29
|
+
})
|
30
|
+
|
31
|
+
# You can uncomment this to get some default data:
|
32
|
+
# flipper[:logging].enable_percentage_of_time 5
|
33
|
+
|
34
|
+
run Flipper::Api.app(flipper) { |builder|
|
35
|
+
builder.use Flipper::Middleware::SetupEnv, flipper
|
36
|
+
builder.use Flipper::Middleware::Memoizer, preload: true
|
37
|
+
}
|
@@ -0,0 +1,43 @@
|
|
1
|
+
#
|
2
|
+
# Usage:
|
3
|
+
# # if you want it to not reload and be really fast
|
4
|
+
# bin/rackup examples/api/memoized.ru -p 9999
|
5
|
+
#
|
6
|
+
# # if you want reloading
|
7
|
+
# bin/shotgun examples/api/memoized.ru -p 9999
|
8
|
+
#
|
9
|
+
# http://localhost:9999/
|
10
|
+
#
|
11
|
+
|
12
|
+
require 'bundler/setup'
|
13
|
+
require "active_support/notifications"
|
14
|
+
require "flipper/api"
|
15
|
+
require "flipper/adapters/pstore"
|
16
|
+
|
17
|
+
Flipper.configure do |config|
|
18
|
+
config.adapter {
|
19
|
+
Flipper::Adapters::Instrumented.new(
|
20
|
+
Flipper::Adapters::PStore.new,
|
21
|
+
instrumenter: ActiveSupport::Notifications,
|
22
|
+
)
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
ActiveSupport::Notifications.subscribe(/.*/, ->(*args) {
|
27
|
+
name, start, finish, id, data = args
|
28
|
+
case name
|
29
|
+
when "adapter_operation.flipper"
|
30
|
+
p data[:adapter_name] => data[:operation]
|
31
|
+
end
|
32
|
+
})
|
33
|
+
|
34
|
+
Flipper.register(:admins) { |actor|
|
35
|
+
actor.respond_to?(:admin?) && actor.admin?
|
36
|
+
}
|
37
|
+
|
38
|
+
# You can uncomment this to get some default data:
|
39
|
+
# Flipper.enable :logging
|
40
|
+
|
41
|
+
run Flipper::Api.app { |builder|
|
42
|
+
builder.use Flipper::Middleware::Memoizer, preload: true
|
43
|
+
}
|
@@ -3,9 +3,7 @@ require 'flipper'
|
|
3
3
|
|
4
4
|
# sets up default adapter so Flipper works like Flipper::DSL
|
5
5
|
Flipper.configure do |config|
|
6
|
-
config.
|
7
|
-
Flipper.new Flipper::Adapters::Memory.new
|
8
|
-
end
|
6
|
+
config.adapter { Flipper::Adapters::Memory.new }
|
9
7
|
end
|
10
8
|
|
11
9
|
puts Flipper.enabled?(:search) # => false
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# Quick example of how to keep track of when a feature was last checked.
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'securerandom'
|
4
|
+
require 'active_support/notifications'
|
5
|
+
require 'flipper'
|
6
|
+
|
7
|
+
class FlipperSubscriber
|
8
|
+
def self.stats
|
9
|
+
@stats ||= {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(name, start, finish, id, payload)
|
13
|
+
if payload[:operation] == :enabled?
|
14
|
+
feature_name = payload.fetch(:feature_name)
|
15
|
+
self.class.stats[feature_name] = Time.now
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
ActiveSupport::Notifications.subscribe(/feature_operation.flipper/, new)
|
20
|
+
end
|
21
|
+
|
22
|
+
Flipper.configure do |config|
|
23
|
+
config.default {
|
24
|
+
Flipper.new(config.adapter, instrumenter: ActiveSupport::Notifications)
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
Flipper.enabled?(:search)
|
29
|
+
Flipper.enabled?(:booyeah)
|
30
|
+
Flipper.enabled?(:hooray)
|
31
|
+
sleep 1
|
32
|
+
Flipper.enabled?(:booyeah)
|
33
|
+
Flipper.enabled?(:hooray)
|
34
|
+
sleep 1
|
35
|
+
Flipper.enabled?(:hooray)
|
36
|
+
|
37
|
+
pp FlipperSubscriber.stats
|
data/examples/memoizing.rb
CHANGED
@@ -4,12 +4,9 @@ require 'flipper/adapters/operation_logger'
|
|
4
4
|
require 'flipper/instrumentation/log_subscriber'
|
5
5
|
|
6
6
|
Flipper.configure do |config|
|
7
|
-
config.
|
7
|
+
config.adapter do
|
8
8
|
# pick an adapter, this uses memory, any will do
|
9
|
-
|
10
|
-
|
11
|
-
# pass adapter to handy DSL instance
|
12
|
-
Flipper.new(adapter)
|
9
|
+
Flipper::Adapters::OperationLogger.new(Flipper::Adapters::Memory.new)
|
13
10
|
end
|
14
11
|
end
|
15
12
|
|
@@ -37,7 +37,8 @@ module Flipper
|
|
37
37
|
# Sync all the gate values.
|
38
38
|
remote_get_all.each do |feature_key, remote_gates_hash|
|
39
39
|
feature = Feature.new(feature_key, @local)
|
40
|
-
|
40
|
+
# Check if feature_key is in hash before accessing to prevent unintended hash modification
|
41
|
+
local_gates_hash = local_get_all.key?(feature_key) ? local_get_all[feature_key] : @local.default_config
|
41
42
|
local_gate_values = GateValues.new(local_gates_hash)
|
42
43
|
remote_gate_values = GateValues.new(remote_gates_hash)
|
43
44
|
FeatureSynchronizer.new(feature, local_gate_values, remote_gate_values).call
|
@@ -1,7 +1,33 @@
|
|
1
1
|
module Flipper
|
2
2
|
class Configuration
|
3
|
-
def initialize
|
4
|
-
@default = -> { Flipper.new(
|
3
|
+
def initialize(options = {})
|
4
|
+
@default = -> { Flipper.new(adapter) }
|
5
|
+
@adapter = -> { Flipper::Adapters::Memory.new }
|
6
|
+
end
|
7
|
+
|
8
|
+
# The default adapter to use.
|
9
|
+
#
|
10
|
+
# Pass a block to assign the adapter, and invoke without a block to
|
11
|
+
# return the configured adapter instance.
|
12
|
+
#
|
13
|
+
# Flipper.configure do |config|
|
14
|
+
# config.adapter # => instance of default Memory adapter
|
15
|
+
#
|
16
|
+
# # Configure it to use the ActiveRecord adapter
|
17
|
+
# config.adapter do
|
18
|
+
# require "flipper/adapters/active_record"
|
19
|
+
# Flipper::Adapters::ActiveRecord.new
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# config.adapter # => instance of ActiveRecord adapter
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
def adapter(&block)
|
26
|
+
if block_given?
|
27
|
+
@adapter = block
|
28
|
+
else
|
29
|
+
@adapter.call
|
30
|
+
end
|
5
31
|
end
|
6
32
|
|
7
33
|
# Controls the default instance for flipper. When used with a block it
|
@@ -13,7 +39,7 @@ module Flipper
|
|
13
39
|
#
|
14
40
|
# # sets the default block to generate a new instance using ActiveRecord adapter
|
15
41
|
# configuration.default do
|
16
|
-
# require "flipper
|
42
|
+
# require "flipper/adapters/active_record"
|
17
43
|
# Flipper.new(Flipper::Adapters::ActiveRecord.new)
|
18
44
|
# end
|
19
45
|
#
|
@@ -8,15 +8,14 @@ module Flipper
|
|
8
8
|
#
|
9
9
|
# app - The app this middleware is included in.
|
10
10
|
# opts - The Hash of options.
|
11
|
-
# :
|
12
|
-
# :preload - Array of Symbol feature names to preload.
|
11
|
+
# :preload - Boolean to preload all features or Array of Symbol feature names to preload.
|
13
12
|
#
|
14
13
|
# Examples
|
15
14
|
#
|
16
15
|
# use Flipper::Middleware::Memoizer
|
17
16
|
#
|
18
17
|
# # using with preload_all features
|
19
|
-
# use Flipper::Middleware::Memoizer,
|
18
|
+
# use Flipper::Middleware::Memoizer, preload: true
|
20
19
|
#
|
21
20
|
# # using with preload specific features
|
22
21
|
# use Flipper::Middleware::Memoizer, preload: [:stats, :search, :some_feature]
|
@@ -26,6 +25,11 @@ module Flipper
|
|
26
25
|
raise 'Flipper::Middleware::Memoizer no longer initializes with a flipper instance or block. Read more at: https://git.io/vSo31.'
|
27
26
|
end
|
28
27
|
|
28
|
+
if opts[:preload_all]
|
29
|
+
warn "Flipper::Middleware::Memoizer: `preload_all` is deprecated, use `preload: true`"
|
30
|
+
opts[:preload] = true
|
31
|
+
end
|
32
|
+
|
29
33
|
@app = app
|
30
34
|
@opts = opts
|
31
35
|
@env_key = opts.fetch(:env_key, 'flipper')
|
@@ -34,39 +38,50 @@ module Flipper
|
|
34
38
|
def call(env)
|
35
39
|
request = Rack::Request.new(env)
|
36
40
|
|
37
|
-
if
|
38
|
-
@app.call(env)
|
39
|
-
else
|
41
|
+
if memoize?(request)
|
40
42
|
memoized_call(env)
|
43
|
+
else
|
44
|
+
@app.call(env)
|
41
45
|
end
|
42
46
|
end
|
43
47
|
|
44
48
|
private
|
45
49
|
|
46
|
-
def
|
47
|
-
|
50
|
+
def memoize?(request)
|
51
|
+
if @opts[:if]
|
52
|
+
@opts[:if].call(request)
|
53
|
+
elsif @opts[:unless]
|
54
|
+
!@opts[:unless].call(request)
|
55
|
+
else
|
56
|
+
true
|
57
|
+
end
|
48
58
|
end
|
49
59
|
|
50
60
|
def memoized_call(env)
|
51
61
|
reset_on_body_close = false
|
52
62
|
flipper = env.fetch(@env_key) { Flipper }
|
53
|
-
original = flipper.memoizing?
|
54
|
-
flipper.memoize = true
|
55
63
|
|
56
|
-
|
64
|
+
# Already memoizing. This instance does not need to do anything.
|
65
|
+
if flipper.memoizing?
|
66
|
+
warn "Flipper::Middleware::Memoizer appears to be running twice. Read how to resolve this at https://github.com/jnunemaker/flipper/pull/523"
|
67
|
+
return @app.call(env)
|
68
|
+
end
|
69
|
+
|
70
|
+
flipper.memoize = true
|
57
71
|
|
58
|
-
|
59
|
-
|
72
|
+
case @opts[:preload]
|
73
|
+
when true then flipper.preload_all
|
74
|
+
when Array then flipper.preload(@opts[:preload])
|
60
75
|
end
|
61
76
|
|
62
77
|
response = @app.call(env)
|
63
78
|
response[2] = Rack::BodyProxy.new(response[2]) do
|
64
|
-
flipper.memoize =
|
79
|
+
flipper.memoize = false
|
65
80
|
end
|
66
81
|
reset_on_body_close = true
|
67
82
|
response
|
68
83
|
ensure
|
69
|
-
flipper.memoize =
|
84
|
+
flipper.memoize = false if flipper && !reset_on_body_close
|
70
85
|
end
|
71
86
|
end
|
72
87
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Flipper
|
2
|
+
class Railtie < Rails::Railtie
|
3
|
+
config.before_configuration do
|
4
|
+
config.flipper = ActiveSupport::OrderedOptions.new.update(
|
5
|
+
env_key: "flipper",
|
6
|
+
memoize: true,
|
7
|
+
preload: true,
|
8
|
+
instrumenter: ActiveSupport::Notifications,
|
9
|
+
log: true
|
10
|
+
)
|
11
|
+
end
|
12
|
+
|
13
|
+
initializer "flipper.default", before: :load_config_initializers do |app|
|
14
|
+
Flipper.configure do |config|
|
15
|
+
config.default do
|
16
|
+
Flipper.new(config.adapter, instrumenter: app.config.flipper.instrumenter)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
initializer "flipper.memoizer", after: :load_config_initializers do |app|
|
22
|
+
config = app.config.flipper
|
23
|
+
|
24
|
+
if config.memoize
|
25
|
+
app.middleware.use Flipper::Middleware::Memoizer, {
|
26
|
+
env_key: config.env_key,
|
27
|
+
preload: config.preload,
|
28
|
+
if: config.memoize.respond_to?(:call) ? config.memoize : nil
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
initializer "flipper.log", after: :load_config_initializers do |app|
|
34
|
+
config = app.config.flipper
|
35
|
+
if config.log && config.instrumenter == ActiveSupport::Notifications
|
36
|
+
require "flipper/instrumentation/log_subscriber"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
initializer "flipper.identifier" do
|
41
|
+
ActiveSupport.on_load(:active_record) do
|
42
|
+
ActiveRecord::Base.include Flipper::Identifier
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|