flipper 0.20.2 → 0.21.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 -1
- data/Changelog.md +59 -0
- data/Gemfile +1 -0
- data/README.md +103 -47
- data/docs/Adapters.md +9 -9
- data/docs/Caveats.md +2 -2
- data/docs/Gates.md +74 -74
- data/docs/Optimization.md +70 -47
- data/docs/http/README.md +12 -11
- data/docs/images/banner.jpg +0 -0
- data/docs/read-only/README.md +8 -5
- data/examples/basic.rb +1 -12
- data/examples/configuring_default.rb +2 -5
- data/examples/dsl.rb +13 -24
- data/examples/enabled_for_actor.rb +8 -15
- data/examples/group.rb +3 -6
- data/examples/group_dynamic_lookup.rb +5 -19
- data/examples/group_with_members.rb +4 -14
- data/examples/importing.rb +1 -1
- data/examples/individual_actor.rb +2 -5
- data/examples/instrumentation.rb +1 -2
- data/examples/memoizing.rb +3 -7
- data/examples/percentage_of_actors.rb +6 -16
- data/examples/percentage_of_actors_enabled_check.rb +7 -10
- data/examples/percentage_of_actors_group.rb +5 -18
- data/examples/percentage_of_time.rb +3 -6
- data/lib/flipper.rb +4 -1
- data/lib/flipper/adapters/memory.rb +20 -94
- data/lib/flipper/adapters/pstore.rb +4 -0
- data/lib/flipper/configuration.rb +33 -7
- data/lib/flipper/errors.rb +2 -3
- data/lib/flipper/identifier.rb +17 -0
- data/lib/flipper/middleware/memoizer.rb +29 -14
- data/lib/flipper/railtie.rb +38 -0
- data/lib/flipper/version.rb +1 -1
- data/spec/flipper/adapters/memory_spec.rb +21 -1
- data/spec/flipper/configuration_spec.rb +20 -2
- data/spec/flipper/identifier_spec.rb +14 -0
- data/spec/flipper/middleware/memoizer_spec.rb +95 -35
- data/spec/flipper/middleware/setup_env_spec.rb +0 -16
- data/spec/flipper/railtie_spec.rb +69 -0
- data/spec/flipper_spec.rb +0 -1
- data/spec/helper.rb +2 -2
- data/spec/support/spec_helpers.rb +20 -0
- data/test/test_helper.rb +1 -0
- metadata +9 -3
- data/examples/example_setup.rb +0 -8
data/lib/flipper/errors.rb
CHANGED
@@ -16,9 +16,8 @@ module Flipper
|
|
16
16
|
# use it.
|
17
17
|
class DefaultNotSet < Flipper::Error
|
18
18
|
def initialize(message = nil)
|
19
|
-
|
20
|
-
|
21
|
-
super(message || default)
|
19
|
+
warn "Flipper::DefaultNotSet is deprecated and will be removed in 1.0"
|
20
|
+
super
|
22
21
|
end
|
23
22
|
end
|
24
23
|
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Flipper
|
2
|
+
# A default implementation of `#flipper_id` for actors.
|
3
|
+
#
|
4
|
+
# class User < Struct.new(:id)
|
5
|
+
# include Flipper::Identifier
|
6
|
+
# end
|
7
|
+
#
|
8
|
+
# user = User.new(99)
|
9
|
+
# Flipper.enable :thing, user
|
10
|
+
# Flipper.enabled? :thing, user #=> true
|
11
|
+
#
|
12
|
+
module Identifier
|
13
|
+
def flipper_id
|
14
|
+
"#{self.class.name};#{id}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -8,8 +8,7 @@ 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
|
#
|
@@ -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,38 @@
|
|
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
|
+
)
|
10
|
+
end
|
11
|
+
|
12
|
+
initializer "flipper.default", before: :load_config_initializers do |app|
|
13
|
+
Flipper.configure do |config|
|
14
|
+
config.default do
|
15
|
+
Flipper.new(config.adapter, instrumenter: app.config.flipper.instrumenter)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
initializer "flipper.memoizer", after: :load_config_initializers do |app|
|
21
|
+
config = app.config.flipper
|
22
|
+
|
23
|
+
if config.memoize
|
24
|
+
app.middleware.use Flipper::Middleware::Memoizer, {
|
25
|
+
env_key: config.env_key,
|
26
|
+
preload: config.preload,
|
27
|
+
if: config.memoize.respond_to?(:call) ? config.memoize : nil
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
initializer "flipper.identifier" do
|
33
|
+
ActiveSupport.on_load(:active_record) do
|
34
|
+
ActiveRecord::Base.include Flipper::Identifier
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/flipper/version.rb
CHANGED
@@ -2,7 +2,27 @@ require 'helper'
|
|
2
2
|
require 'flipper/spec/shared_adapter_specs'
|
3
3
|
|
4
4
|
RSpec.describe Flipper::Adapters::Memory do
|
5
|
-
|
5
|
+
let(:source) { {} }
|
6
|
+
subject { described_class.new(source) }
|
6
7
|
|
7
8
|
it_should_behave_like 'a flipper adapter'
|
9
|
+
|
10
|
+
it "can initialize from big hash" do
|
11
|
+
flipper = Flipper.new(subject)
|
12
|
+
flipper.enable :subscriptions
|
13
|
+
flipper.disable :search
|
14
|
+
flipper.enable_percentage_of_actors :pro_deal, 20
|
15
|
+
flipper.enable_percentage_of_time :logging, 30
|
16
|
+
flipper.enable_actor :following, Flipper::Actor.new('1')
|
17
|
+
flipper.enable_actor :following, Flipper::Actor.new('3')
|
18
|
+
flipper.enable_group :following, Flipper::Types::Group.new(:staff)
|
19
|
+
|
20
|
+
expect(source).to eq({
|
21
|
+
"subscriptions" => subject.default_config.merge(boolean: "true"),
|
22
|
+
"search" => subject.default_config,
|
23
|
+
"logging" => subject.default_config.merge(:percentage_of_time => "30"),
|
24
|
+
"pro_deal" => subject.default_config.merge(:percentage_of_actors => "20"),
|
25
|
+
"following" => subject.default_config.merge(actors: Set["1", "3"], groups: Set["staff"]),
|
26
|
+
})
|
27
|
+
end
|
8
28
|
end
|
@@ -2,13 +2,31 @@ require 'helper'
|
|
2
2
|
require 'flipper/configuration'
|
3
3
|
|
4
4
|
RSpec.describe Flipper::Configuration do
|
5
|
+
describe '#adapter' do
|
6
|
+
it 'returns instance using Memory adapter' do
|
7
|
+
expect(subject.adapter).to be_a(Flipper::Adapters::Memory)
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'can be set' do
|
11
|
+
instance = Flipper::Adapters::Memory.new
|
12
|
+
expect(subject.adapter).not_to be(instance)
|
13
|
+
subject.adapter { instance }
|
14
|
+
expect(subject.adapter).to be(instance)
|
15
|
+
# All adapters are wrapped in Memoizable
|
16
|
+
expect(subject.default.adapter.adapter).to be(instance)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
5
20
|
describe '#default' do
|
6
|
-
it '
|
7
|
-
expect
|
21
|
+
it 'returns instance using Memory adapter' do
|
22
|
+
expect(subject.default).to be_a(Flipper::DSL)
|
23
|
+
# All adapters are wrapped in Memoizable
|
24
|
+
expect(subject.default.adapter.adapter).to be_a(Flipper::Adapters::Memory)
|
8
25
|
end
|
9
26
|
|
10
27
|
it 'can be set default' do
|
11
28
|
instance = Flipper.new(Flipper::Adapters::Memory.new)
|
29
|
+
expect(subject.default).not_to be(instance)
|
12
30
|
subject.default { instance }
|
13
31
|
expect(subject.default).to be(instance)
|
14
32
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'flipper/identifier'
|
3
|
+
|
4
|
+
RSpec.describe Flipper::Identifier do
|
5
|
+
describe '#flipper_id' do
|
6
|
+
class User < Struct.new(:id)
|
7
|
+
include Flipper::Identifier
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'uses class name and id' do
|
11
|
+
expect(User.new(5).flipper_id).to eq('User;5')
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -15,10 +15,6 @@ RSpec.describe Flipper::Middleware::Memoizer do
|
|
15
15
|
let(:flipper) { Flipper.new(adapter) }
|
16
16
|
let(:env) { { 'flipper' => flipper } }
|
17
17
|
|
18
|
-
after do
|
19
|
-
flipper.memoize = nil
|
20
|
-
end
|
21
|
-
|
22
18
|
it 'raises if initialized with app and flipper instance' do
|
23
19
|
expect do
|
24
20
|
described_class.new(app, flipper)
|
@@ -103,14 +99,14 @@ RSpec.describe Flipper::Middleware::Memoizer do
|
|
103
99
|
end
|
104
100
|
end
|
105
101
|
|
106
|
-
context 'with
|
102
|
+
context 'with preload: true' do
|
107
103
|
let(:app) do
|
108
104
|
# ensure scoped for builder block, annoying...
|
109
105
|
instance = flipper
|
110
106
|
middleware = described_class
|
111
107
|
|
112
108
|
Rack::Builder.new do
|
113
|
-
use middleware,
|
109
|
+
use middleware, preload: true
|
114
110
|
|
115
111
|
map '/' do
|
116
112
|
run ->(_env) { [200, {}, []] }
|
@@ -139,7 +135,7 @@ RSpec.describe Flipper::Middleware::Memoizer do
|
|
139
135
|
[200, {}, []]
|
140
136
|
end
|
141
137
|
|
142
|
-
middleware = described_class.new(app,
|
138
|
+
middleware = described_class.new(app, preload: true)
|
143
139
|
middleware.call(env)
|
144
140
|
|
145
141
|
expect(adapter.operations.size).to be(1)
|
@@ -156,7 +152,7 @@ RSpec.describe Flipper::Middleware::Memoizer do
|
|
156
152
|
[200, {}, []]
|
157
153
|
end
|
158
154
|
|
159
|
-
middleware = described_class.new(app,
|
155
|
+
middleware = described_class.new(app, preload: true)
|
160
156
|
middleware.call(env)
|
161
157
|
|
162
158
|
expect(adapter.count(:get)).to be(1)
|
@@ -222,6 +218,44 @@ RSpec.describe Flipper::Middleware::Memoizer do
|
|
222
218
|
end
|
223
219
|
end
|
224
220
|
|
221
|
+
context 'with multiple instances' do
|
222
|
+
let(:app) do
|
223
|
+
# ensure scoped for builder block, annoying...
|
224
|
+
instance = flipper
|
225
|
+
middleware = described_class
|
226
|
+
|
227
|
+
Rack::Builder.new do
|
228
|
+
use middleware, preload: %i(stats)
|
229
|
+
# Second instance should be a noop
|
230
|
+
use middleware, preload: true
|
231
|
+
|
232
|
+
map '/' do
|
233
|
+
run ->(_env) { [200, {}, []] }
|
234
|
+
end
|
235
|
+
|
236
|
+
map '/fail' do
|
237
|
+
run ->(_env) { raise 'FAIL!' }
|
238
|
+
end
|
239
|
+
end.to_app
|
240
|
+
end
|
241
|
+
|
242
|
+
def get(uri, params = {}, env = {}, &block)
|
243
|
+
silence { super(uri, params, env, &block) }
|
244
|
+
end
|
245
|
+
|
246
|
+
include_examples 'flipper middleware'
|
247
|
+
|
248
|
+
it 'does not call preload in second instance' do
|
249
|
+
expect(flipper).not_to receive(:preload_all)
|
250
|
+
|
251
|
+
output = get '/', {}, 'flipper' => flipper
|
252
|
+
|
253
|
+
expect(output).to match(/Flipper::Middleware::Memoizer appears to be running twice/)
|
254
|
+
expect(adapter.count(:get_multi)).to be(1)
|
255
|
+
expect(adapter.last(:get_multi).args).to eq([[flipper[:stats]]])
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
225
259
|
context 'when an app raises an exception' do
|
226
260
|
it 'resets memoize' do
|
227
261
|
begin
|
@@ -259,10 +293,9 @@ RSpec.describe Flipper::Middleware::Memoizer do
|
|
259
293
|
context 'with Flipper setup in env' do
|
260
294
|
it 'caches getting a feature for duration of request' do
|
261
295
|
Flipper.configure do |config|
|
262
|
-
config.
|
296
|
+
config.adapter do
|
263
297
|
memory = Flipper::Adapters::Memory.new
|
264
|
-
|
265
|
-
Flipper.new(logged_adapter)
|
298
|
+
Flipper::Adapters::OperationLogger.new(memory)
|
266
299
|
end
|
267
300
|
end
|
268
301
|
Flipper.enable(:stats)
|
@@ -308,14 +341,16 @@ RSpec.describe Flipper::Middleware::Memoizer do
|
|
308
341
|
end
|
309
342
|
end
|
310
343
|
|
311
|
-
context 'with
|
344
|
+
context 'with preload:true' do
|
345
|
+
let(:options) { {preload: true} }
|
346
|
+
|
312
347
|
let(:app) do
|
313
348
|
# ensure scoped for builder block, annoying...
|
314
349
|
middleware = described_class
|
350
|
+
opts = options
|
315
351
|
|
316
352
|
Rack::Builder.new do
|
317
|
-
use middleware,
|
318
|
-
unless: ->(request) { request.path.start_with?("/assets") }
|
353
|
+
use middleware, opts
|
319
354
|
|
320
355
|
map '/' do
|
321
356
|
run ->(_env) { [200, {}, []] }
|
@@ -327,22 +362,57 @@ RSpec.describe Flipper::Middleware::Memoizer do
|
|
327
362
|
end.to_app
|
328
363
|
end
|
329
364
|
|
330
|
-
|
331
|
-
|
332
|
-
|
365
|
+
|
366
|
+
context 'and unless option' do
|
367
|
+
before do
|
368
|
+
options[:unless] = ->(request) { request.path.start_with?("/assets") }
|
369
|
+
end
|
370
|
+
|
371
|
+
it 'does NOT preload if request matches unless block' do
|
372
|
+
expect(flipper).to receive(:preload_all).never
|
373
|
+
get '/assets/foo.png', {}, 'flipper' => flipper
|
374
|
+
end
|
375
|
+
|
376
|
+
it 'does preload if request does NOT match unless block' do
|
377
|
+
expect(flipper).to receive(:preload_all).once
|
378
|
+
get '/some/other/path', {}, 'flipper' => flipper
|
379
|
+
end
|
333
380
|
end
|
334
381
|
|
335
|
-
|
336
|
-
|
337
|
-
|
382
|
+
context 'and if option' do
|
383
|
+
before do
|
384
|
+
options[:if] = ->(request) { !request.path.start_with?("/assets") }
|
385
|
+
end
|
386
|
+
|
387
|
+
it 'does NOT preload if request does not match if block' do
|
388
|
+
expect(flipper).to receive(:preload_all).never
|
389
|
+
get '/assets/foo.png', {}, 'flipper' => flipper
|
390
|
+
end
|
391
|
+
|
392
|
+
it 'does preload if request matches if block' do
|
393
|
+
expect(flipper).to receive(:preload_all).once
|
394
|
+
get '/some/other/path', {}, 'flipper' => flipper
|
395
|
+
end
|
338
396
|
end
|
339
397
|
end
|
340
398
|
|
341
|
-
context 'with
|
399
|
+
context 'with preload:true and caching adapter' do
|
400
|
+
let(:app) do
|
401
|
+
app = lambda do |_env|
|
402
|
+
flipper[:stats].enabled?
|
403
|
+
flipper[:stats].enabled?
|
404
|
+
flipper[:shiny].enabled?
|
405
|
+
flipper[:shiny].enabled?
|
406
|
+
[200, {}, []]
|
407
|
+
end
|
408
|
+
|
409
|
+
described_class.new(app, preload: true)
|
410
|
+
end
|
411
|
+
|
342
412
|
it 'eagerly caches known features for duration of request' do
|
343
413
|
memory = Flipper::Adapters::Memory.new
|
344
414
|
logged_memory = Flipper::Adapters::OperationLogger.new(memory)
|
345
|
-
cache = ActiveSupport::Cache::
|
415
|
+
cache = ActiveSupport::Cache::MemoryStore.new
|
346
416
|
cache.clear
|
347
417
|
cached = Flipper::Adapters::ActiveSupportCacheStore.new(logged_memory, cache, expires_in: 10)
|
348
418
|
logged_cached = Flipper::Adapters::OperationLogger.new(cached)
|
@@ -355,25 +425,15 @@ RSpec.describe Flipper::Middleware::Memoizer do
|
|
355
425
|
logged_memory.reset
|
356
426
|
logged_cached.reset
|
357
427
|
|
358
|
-
|
359
|
-
flipper[:stats].enabled?
|
360
|
-
flipper[:stats].enabled?
|
361
|
-
flipper[:shiny].enabled?
|
362
|
-
flipper[:shiny].enabled?
|
363
|
-
[200, {}, []]
|
364
|
-
end
|
365
|
-
|
366
|
-
middleware = described_class.new(app, preload_all: true)
|
367
|
-
|
368
|
-
middleware.call('flipper' => flipper)
|
428
|
+
get '/', {}, 'flipper' => flipper
|
369
429
|
expect(logged_cached.count(:get_all)).to be(1)
|
370
430
|
expect(logged_memory.count(:get_all)).to be(1)
|
371
431
|
|
372
|
-
|
432
|
+
get '/', {}, 'flipper' => flipper
|
373
433
|
expect(logged_cached.count(:get_all)).to be(2)
|
374
434
|
expect(logged_memory.count(:get_all)).to be(1)
|
375
435
|
|
376
|
-
|
436
|
+
get '/', {}, 'flipper' => flipper
|
377
437
|
expect(logged_cached.count(:get_all)).to be(3)
|
378
438
|
expect(logged_memory.count(:get_all)).to be(1)
|
379
439
|
end
|