flipper 0.20.3 → 0.22.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +18 -9
  3. data/Changelog.md +62 -0
  4. data/Gemfile +2 -0
  5. data/README.md +104 -47
  6. data/docs/Adapters.md +9 -9
  7. data/docs/Caveats.md +2 -2
  8. data/docs/Gates.md +74 -74
  9. data/docs/Optimization.md +70 -47
  10. data/docs/api/README.md +5 -5
  11. data/docs/http/README.md +12 -11
  12. data/docs/images/banner.jpg +0 -0
  13. data/docs/read-only/README.md +8 -5
  14. data/examples/api/basic.ru +19 -0
  15. data/examples/api/custom_memoized.ru +37 -0
  16. data/examples/api/memoized.ru +43 -0
  17. data/examples/basic.rb +1 -12
  18. data/examples/configuring_default.rb +2 -5
  19. data/examples/dsl.rb +13 -24
  20. data/examples/enabled_for_actor.rb +8 -15
  21. data/examples/group.rb +3 -6
  22. data/examples/group_dynamic_lookup.rb +5 -19
  23. data/examples/group_with_members.rb +4 -14
  24. data/examples/importing.rb +1 -1
  25. data/examples/individual_actor.rb +2 -5
  26. data/examples/instrumentation.rb +1 -2
  27. data/examples/memoizing.rb +3 -7
  28. data/examples/percentage_of_actors.rb +6 -16
  29. data/examples/percentage_of_actors_enabled_check.rb +7 -10
  30. data/examples/percentage_of_actors_group.rb +5 -18
  31. data/examples/percentage_of_time.rb +3 -6
  32. data/lib/flipper.rb +4 -1
  33. data/lib/flipper/adapters/pstore.rb +4 -0
  34. data/lib/flipper/configuration.rb +33 -7
  35. data/lib/flipper/errors.rb +2 -3
  36. data/lib/flipper/identifier.rb +17 -0
  37. data/lib/flipper/middleware/memoizer.rb +30 -15
  38. data/lib/flipper/railtie.rb +46 -0
  39. data/lib/flipper/version.rb +1 -1
  40. data/spec/flipper/configuration_spec.rb +20 -2
  41. data/spec/flipper/identifier_spec.rb +14 -0
  42. data/spec/flipper/middleware/memoizer_spec.rb +95 -35
  43. data/spec/flipper/middleware/setup_env_spec.rb +0 -16
  44. data/spec/flipper/railtie_spec.rb +69 -0
  45. data/spec/flipper_spec.rb +0 -1
  46. data/spec/helper.rb +2 -2
  47. data/spec/support/spec_helpers.rb +20 -0
  48. data/test/test_helper.rb +1 -0
  49. metadata +12 -3
  50. data/examples/example_setup.rb +0 -8
data/docs/Optimization.md CHANGED
@@ -1,52 +1,63 @@
1
1
  # Optimization
2
2
 
3
- ## Memoizing Middleware
3
+ ## Memoization
4
4
 
5
- One optimization that flipper provides is a memoizing middleware. The memoizing middleware ensures that you only make one adapter call per feature per request. This means if you check the same feature over and over, it will only make one Mongo, Redis, or whatever call per feature for the length of the request.
5
+ By default, Flipper will preload and memoize all features to ensure one adapter call per request. This means no matter how many times you check features, Flipper will only make one network request to Postgres, MySQL, Redis, Mongo or whatever adapter you are using for the length of the request.
6
6
 
7
- You can use the middleware like so for Rails:
7
+ ### Preloading
8
+
9
+ Flipper will preload all features before each request by default, which is recommended if you have a limited number of features (< 100?) and they are used on most requests. If you have a lot of features, but only a few are used on most requests, you may want to customize preloading:
8
10
 
9
11
  ```ruby
10
- # setup default instance (perhaps in config/initializers/flipper.rb)
11
- Flipper.configure do |config|
12
- config.default do
13
- Flipper.new(...)
14
- end
12
+ # config/initializers/flipper.rb
13
+ Rails.application.configure do
14
+ # Load specific features that are used on most requests
15
+ config.flipper.preload = [:stats, :search, :some_feature]
16
+
17
+ # Or completely disable preloading
18
+ config.flipper.preload = false
15
19
  end
20
+ ```
21
+
22
+ Features that are not preloaded are still memoized, ensuring one adapter call per feature during a request.
23
+
24
+ ### Skip memoization
25
+
26
+ Prevent preloading and memoization on specific requests by setting `memoize` to a proc that evaluates to false.
16
27
 
17
- # This assumes you setup a default flipper instance using configure.
18
- Rails.configuration.middleware.use Flipper::Middleware::Memoizer
28
+ ```ruby
29
+ # config/initializers/flipper.rb
30
+ Rails.application.configure do
31
+ config.flipper.memoize = ->(request) { !request.path.start_with?("/assets") }
32
+ end
19
33
  ```
20
34
 
21
- **Note**: Be sure that the middleware is high enough up in your stack that all feature checks are wrapped.
35
+ ### Disable memoization
22
36
 
23
- **Also Note**: If you haven't setup a default instance, you can pass the instance to `SetupEnv` as `Memoizer` uses whatever is setup in the `env`:
37
+ To disable memoization entirely:
24
38
 
25
39
  ```ruby
26
- Rails.configuration.middleware.use Flipper::Middleware::SetupEnv, -> { Flipper.new(...) }
27
- Rails.configuration.middleware.use Flipper::Middleware::Memoizer
40
+ Rails.application.configure do
41
+ config.flipper.memoize = false
42
+ end
28
43
  ```
29
44
 
30
- ### Options
31
-
32
- The Memoizer middleware also supports a few options. Use either `preload` or `preload_all`, not both.
33
-
34
- * **`:preload`** - An `Array` of feature names (`Symbol`) to preload for every request. Useful if you have features that are used on every endpoint. `preload` uses `Adapter#get_multi` to attempt to load the features in one network call instead of N+1 network calls.
35
- ```ruby
36
- Rails.configuration.middleware.use Flipper::Middleware::Memoizer,
37
- preload: [:stats, :search, :some_feature]
38
- ```
39
- * **`:preload_all`** - A Boolean value (default: false) of whether or not all features should be preloaded. Using this results in a `preload_all` call with the result of `Adapter#get_all`. Any subsequent feature checks will be memoized and perform no network calls. I wouldn't recommend using this unless you have few features (< 100?) and nearly all of them are used on every request.
40
- ```ruby
41
- Rails.configuration.middleware.use Flipper::Middleware::Memoizer,
42
- preload_all: true
43
- ```
44
- * **`:unless`** - A block that prevents preloading and memoization if it evaluates to true.
45
- ```ruby
46
- # skip preloading and memoizing if path starts with /assets
47
- Rails.configuration.middleware.use Flipper::Middleware::Memoizer,
48
- unless: ->(request) { request.path.start_with?("/assets") }
49
- ```
45
+ ### Advanced
46
+
47
+ Memoization is implemented as a Rack middleware, which can be used manually in any Ruby app:
48
+
49
+ ```ruby
50
+ use Flipper::Middleware::Memoizer,
51
+ preload: true,
52
+ unless: ->(request) { request.path.start_with?("/assets") }
53
+ ```
54
+
55
+ **Also Note**: If you need to customize the instance of Flipper used by the memoizer, you can pass the instance to `SetupEnv`:
56
+
57
+ ```ruby
58
+ use Flipper::Middleware::SetupEnv, -> { Flipper.new(...) }
59
+ use Flipper::Middleware::Memoizer
60
+ ```
50
61
 
51
62
  ## Cache Adapters
52
63
 
@@ -61,11 +72,15 @@ https://github.com/petergoldstein/dalli
61
72
  Example using the Dalli cache adapter with the Memory adapter and a TTL of 600 seconds:
62
73
 
63
74
  ```ruby
64
- dalli_client = Dalli::Client.new('localhost:11211')
65
- memory_adapter = Flipper::Adapters::Memory.new
66
- adapter = Flipper::Adapters::Dalli.new(memory_adapter, dalli_client, 600)
67
- flipper = Flipper.new(adapter)
75
+ Flipper.configure do |config|
76
+ config.adapter do
77
+ dalli = Dalli::Client.new('localhost:11211')
78
+ adapter = Flipper::Adapters::Memory.new
79
+ Flipper::Adapters::Dalli.new(adapter, dalli, 600)
80
+ end
81
+ end
68
82
  ```
83
+
69
84
  ### RedisCache
70
85
 
71
86
  Applications using [Redis](https://redis.io/) via the [redis-rb](https://github.com/redis/redis-rb) client can take advantage of the RedisCache adapter.
@@ -75,12 +90,15 @@ Initialize `RedisCache` with a flipper [adapter](https://github.com/jnunemaker/
75
90
  Example using the RedisCache adapter with the Memory adapter and a TTL of 4800 seconds:
76
91
 
77
92
  ```ruby
78
- require 'flipper/adapters/redis_cache'
93
+ require 'flipper/adapters/redis_cache'
79
94
 
80
- redis = Redis.new(url: ENV['REDIS_URL'])
81
- memory_adapter = Flipper::Adapters::Memory.new
82
- adapter = Flipper::Adapters::RedisCache.new(memory_adapter, redis, 4800)
83
- flipper = Flipper.new(adapter)
95
+ Flipper.configure do |config|
96
+ config.adapter do
97
+ redis = Redis.new(url: ENV['REDIS_URL'])
98
+ memory_adapter = Flipper::Adapters::Memory.new
99
+ Flipper::Adapters::RedisCache.new(memory_adapter, redis, 4800)
100
+ end
101
+ end
84
102
  ```
85
103
 
86
104
  ### ActiveSupportCacheStore
@@ -105,10 +123,15 @@ Example using the ActiveSupportCacheStore adapter with ActiveSupport's [MemorySt
105
123
  require 'active_support/cache'
106
124
  require 'flipper/adapters/active_support_cache_store'
107
125
 
108
- memory_adapter = Flipper::Adapters::Memory.new
109
- cache = ActiveSupport::Cache::MemoryStore.new
110
- adapter = Flipper::Adapters::ActiveSupportCacheStore.new(memory_adapter, cache, expires_in: 5.minutes)
111
- flipper = Flipper.new(adapter)
126
+ Flipper.configure do |config|
127
+ config.adapter do
128
+ Flipper::Adapters::ActiveSupportCacheStore.new(
129
+ Flipper::Adapters::Memory.new,
130
+ ActiveSupport::Cache::MemoryStore.new # Or Rails.cache,
131
+ expires_in: 5.minutes
132
+ )
133
+ end
134
+ end
112
135
  ```
113
136
 
114
137
  Setting `expires_in` is optional and will set an expiration time on Flipper cache keys. If specified, all flipper keys will use this `expires_in` over the `expires_in` passed to your ActiveSupport cache constructor.
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(flipper) => '/flipper/api'
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(flipper) => '/flipper'
38
- mount Flipper::Api.app(flipper) => '/flipper/api'
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(flipper) => '/flipper/api'
48
- mount Flipper::UI.app(flipper) => '/flipper'
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
- configuration = {
12
- url: 'http://app.com/mount-point', # required
13
- headers: { 'X-Custom-Header' => 'foo' },
14
- basic_auth_username: 'user123',
15
- basic_auth_password: 'password123'
16
- read_timeout: 5,
17
- open_timeout: 2,
18
- }
19
-
20
- adapter = Flipper::Adapters::Http.new(configuration)
21
- flipper = Flipper.new(adapter)
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**:
Binary file
@@ -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
- memory_adapter = Flipper::Adapters::Memory.new
14
- read_only_adapter = Flipper::Adapters::ReadOnly.new(memory_adapter)
15
-
16
- flipper = Flipper.new(read_only_adapter)
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
- > flipper[:dashboard_panel].enable
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
+ }
data/examples/basic.rb CHANGED
@@ -1,17 +1,6 @@
1
- require File.expand_path('../example_setup', __FILE__)
2
-
1
+ require 'bundler/setup'
3
2
  require 'flipper'
4
3
 
5
- Flipper.configure do |config|
6
- config.default do
7
- # pick an adapter, this uses memory, any will do
8
- adapter = Flipper::Adapters::Memory.new
9
-
10
- # pass adapter to handy DSL instance
11
- Flipper.new(adapter)
12
- end
13
- end
14
-
15
4
  # check if search is enabled
16
5
  if Flipper.enabled?(:search)
17
6
  puts 'Search away!'
@@ -1,12 +1,9 @@
1
- require File.expand_path('../example_setup', __FILE__)
2
-
1
+ require 'bundler/setup'
3
2
  require 'flipper'
4
3
 
5
4
  # sets up default adapter so Flipper works like Flipper::DSL
6
5
  Flipper.configure do |config|
7
- config.default do
8
- Flipper.new Flipper::Adapters::Memory.new
9
- end
6
+ config.adapter { Flipper::Adapters::Memory.new }
10
7
  end
11
8
 
12
9
  puts Flipper.enabled?(:search) # => false
data/examples/dsl.rb CHANGED
@@ -1,20 +1,9 @@
1
- require File.expand_path('../example_setup', __FILE__)
2
-
1
+ require 'bundler/setup'
3
2
  require 'flipper'
4
3
 
5
- adapter = Flipper::Adapters::Memory.new
6
- flipper = Flipper.new(adapter)
7
-
8
4
  # create a thing with an identifier
9
- class Person
10
- attr_reader :id
11
-
12
- def initialize(id)
13
- @id = id
14
- end
15
-
16
- # Must respond to flipper_id
17
- alias_method :flipper_id, :id
5
+ class Person < Struct.new(:id)
6
+ include Flipper::Identifier
18
7
  end
19
8
 
20
9
  person = Person.new(1)
@@ -22,14 +11,14 @@ person = Person.new(1)
22
11
  puts "Stats are disabled by default\n\n"
23
12
 
24
13
  # is a feature enabled
25
- puts "flipper.enabled? :stats: #{flipper.enabled? :stats}"
14
+ puts "flipper.enabled? :stats: #{Flipper.enabled? :stats}"
26
15
 
27
16
  # is a feature on or off for a particular person
28
- puts "flipper.enabled? :stats, person: #{flipper.enabled? :stats, person}"
17
+ puts "Flipper.enabled? :stats, person: #{Flipper.enabled? :stats, person}"
29
18
 
30
19
  # get at a feature
31
- puts "\nYou can also get an individual feature like this:\nstats = flipper[:stats]\n\n"
32
- stats = flipper[:stats]
20
+ puts "\nYou can also get an individual feature like this:\nstats = Flipper[:stats]\n\n"
21
+ stats = Flipper[:stats]
33
22
 
34
23
  # is that feature enabled
35
24
  puts "stats.enabled?: #{stats.enabled?}"
@@ -39,7 +28,7 @@ puts "stats.enabled? person: #{stats.enabled? person}"
39
28
 
40
29
  # enable a feature by name
41
30
  puts "\nEnabling stats\n\n"
42
- flipper.enable :stats
31
+ Flipper.enable :stats
43
32
 
44
33
  # or, you can use the feature to enable
45
34
  stats.enable
@@ -49,7 +38,7 @@ puts "stats.enabled? person: #{stats.enabled? person}"
49
38
 
50
39
  # oh, no, let's turn this baby off
51
40
  puts "\nDisabling stats\n\n"
52
- flipper.disable :stats
41
+ Flipper.disable :stats
53
42
 
54
43
  # or we can disable using feature obviously
55
44
  stats.disable
@@ -59,18 +48,18 @@ puts "stats.enabled? person: #{stats.enabled? person}"
59
48
  puts
60
49
 
61
50
  # get an instance of the percentage of time type set to 5
62
- puts flipper.time(5).inspect
51
+ puts Flipper.time(5).inspect
63
52
 
64
53
  # get an instance of the percentage of actors type set to 15
65
- puts flipper.actors(15).inspect
54
+ puts Flipper.actors(15).inspect
66
55
 
67
56
  # get an instance of an actor using an object that responds to flipper_id
68
57
  responds_to_flipper_id = Struct.new(:flipper_id).new(10)
69
- puts flipper.actor(responds_to_flipper_id).inspect
58
+ puts Flipper.actor(responds_to_flipper_id).inspect
70
59
 
71
60
  # get an instance of an actor using an object
72
61
  thing = Struct.new(:flipper_id).new(22)
73
- puts flipper.actor(thing).inspect
62
+ puts Flipper.actor(thing).inspect
74
63
 
75
64
  # register a top level group
76
65
  admins = Flipper.register(:admins) { |actor|