flipper 1.3.0 → 1.3.6

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +19 -5
  3. data/.github/workflows/examples.yml +19 -5
  4. data/CLAUDE.md +74 -0
  5. data/Gemfile +7 -3
  6. data/README.md +1 -1
  7. data/examples/cloud/backoff_policy.rb +1 -1
  8. data/lib/flipper/adapter_builder.rb +1 -1
  9. data/lib/flipper/adapters/http/error.rb +1 -1
  10. data/lib/flipper/adapters/http.rb +2 -2
  11. data/lib/flipper/adapters/poll.rb +15 -0
  12. data/lib/flipper/cloud/configuration.rb +5 -2
  13. data/lib/flipper/cloud/telemetry/backoff_policy.rb +6 -3
  14. data/lib/flipper/cloud/telemetry/submitter.rb +3 -1
  15. data/lib/flipper/cloud/telemetry.rb +2 -2
  16. data/lib/flipper/export.rb +0 -2
  17. data/lib/flipper/expressions/all.rb +0 -2
  18. data/lib/flipper/feature.rb +8 -1
  19. data/lib/flipper/gate.rb +1 -1
  20. data/lib/flipper/instrumentation/log_subscriber.rb +1 -2
  21. data/lib/flipper/instrumentation/statsd.rb +4 -2
  22. data/lib/flipper/instrumentation/subscriber.rb +0 -4
  23. data/lib/flipper/metadata.rb +1 -0
  24. data/lib/flipper/poller.rb +2 -2
  25. data/lib/flipper/version.rb +1 -1
  26. data/lib/generators/flipper/setup_generator.rb +5 -0
  27. data/lib/generators/flipper/templates/initializer.rb +45 -0
  28. data/spec/flipper/adapters/http_spec.rb +1 -0
  29. data/spec/flipper/adapters/poll_spec.rb +41 -0
  30. data/spec/flipper/adapters/strict_spec.rb +2 -2
  31. data/spec/flipper/cli_spec.rb +4 -2
  32. data/spec/flipper/cloud/dsl_spec.rb +1 -1
  33. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +3 -3
  34. data/spec/flipper/cloud/telemetry/submitter_spec.rb +4 -4
  35. data/spec/flipper/cloud/telemetry_spec.rb +6 -6
  36. data/spec/flipper/cloud_spec.rb +9 -4
  37. data/spec/flipper/dsl_spec.rb +0 -3
  38. data/spec/flipper/engine_spec.rb +8 -7
  39. data/spec/flipper/feature_spec.rb +22 -11
  40. data/spec/flipper/instrumentation/log_subscriber_spec.rb +1 -0
  41. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +1 -1
  42. data/spec/flipper/middleware/memoizer_spec.rb +5 -6
  43. data/spec/flipper/model/active_record_spec.rb +11 -0
  44. data/spec/flipper_spec.rb +1 -1
  45. data/spec/spec_helper.rb +11 -5
  46. data/spec/support/fail_on_output.rb +1 -1
  47. data/spec/support/spec_helpers.rb +13 -2
  48. data/test_rails/generators/flipper/setup_generator_test.rb +5 -0
  49. data/test_rails/generators/flipper/update_generator_test.rb +1 -1
  50. data/test_rails/helper.rb +3 -0
  51. data/test_rails/system/test_help_test.rb +1 -0
  52. metadata +9 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 934e0e50f2aea9b4294ed435a780f5eb55ad8b547df9c80f609022b880a4dd9c
4
- data.tar.gz: 8799605783af26860795d336a66d511f35b0f644424aca80cd3f81baaadc6475
3
+ metadata.gz: 8f53e9dadf45ea1d00e7abd24358748347d516d761ea347c438057f62fdbc185
4
+ data.tar.gz: 8c2a73d793d41ce64ed26c559bf532127f73e17e20944930aa6bf14aff355a57
5
5
  SHA512:
6
- metadata.gz: d0e041360e5d15966dd4f4a39be10616aff35dd56a7491297adce42b8d13d3ba7126feaf2d3798194c72fac73afad7e2355379ff51e40481f4a0ee25c49e6c8a
7
- data.tar.gz: c16307b9a21775b1db67e9fd99c72c5206e7b0d6e0324e7625f349228757aea1243be676ba838ac0b853a7a48469fe8536da41085982556c0492ad985daf51ca
6
+ metadata.gz: af26df5e8c10540348e2b8dd31f11198dafdc8f3bcb6ef8e5361e3a764caa5a872734c64807746f1d077cfbc95df9143959e4c3446429dce1d5d871e46861fa7
7
+ data.tar.gz: 2005a736ea665c359154bf85f56495fcf9b326e9ecb740d2e1aca0c52a698367b44334a90ae2b1277c2a3c0dcdddcc478eab3126975f210fc796a454202913f4
@@ -7,7 +7,7 @@ jobs:
7
7
  services:
8
8
  redis:
9
9
  image: redis
10
- ports: ['6379:6379']
10
+ ports: ["6379:6379"]
11
11
  options: >-
12
12
  --health-cmd "redis-cli ping"
13
13
  --health-interval 10s
@@ -28,21 +28,35 @@ jobs:
28
28
  strategy:
29
29
  fail-fast: false
30
30
  matrix:
31
- ruby: ['2.6', '2.7', '3.0', '3.1', '3.2', '3.3']
32
- rails: ['5.2', '6.0.0', '6.1.0', '7.0.0', '7.1.0']
31
+ ruby: ["2.6", "2.7", "3.0", "3.1", "3.2", "3.3"]
32
+ rails: ["5.2", "6.0.0", "6.1.0", "7.0.0", "7.1.0", "7.2.0", "8.0.0"]
33
33
  exclude:
34
34
  - ruby: "2.6"
35
35
  rails: "7.1.0"
36
36
  - ruby: "2.6"
37
37
  rails: "7.0.0"
38
+ - ruby: "2.6"
39
+ rails: "7.2.0"
40
+ - ruby: "2.6"
41
+ rails: "8.0.0"
38
42
  - ruby: "2.7"
39
43
  rails: "7.1.0"
44
+ - ruby: "2.7"
45
+ rails: "7.2.0"
46
+ - ruby: "2.7"
47
+ rails: "8.0.0"
40
48
  - ruby: "3.0"
41
49
  rails: "5.2"
50
+ - ruby: "3.0"
51
+ rails: "7.2.0"
52
+ - ruby: "3.0"
53
+ rails: "8.0.0"
42
54
  - ruby: "3.1"
43
55
  rails: "5.2"
44
56
  - ruby: "3.1"
45
57
  rails: "6.0.0"
58
+ - ruby: "3.1"
59
+ rails: "8.0.0"
46
60
  - ruby: "3.2"
47
61
  rails: "5.2"
48
62
  - ruby: "3.2"
@@ -56,7 +70,7 @@ jobs:
56
70
  - ruby: "3.3"
57
71
  rails: "6.1.0"
58
72
  env:
59
- SQLITE3_VERSION: 1.4.1
73
+ SQLITE3_VERSION: ${{ matrix.rails == '8.0.0' && '2.1.0' || '1.4.1' }}
60
74
  REDIS_URL: redis://localhost:6379/0
61
75
  CI: true
62
76
  RAILS_VERSION: ${{ matrix.rails }}
@@ -70,7 +84,7 @@ jobs:
70
84
  - name: Setup memcached
71
85
  uses: KeisukeYamashita/memcached-actions@v1
72
86
  - name: Start MongoDB
73
- uses: supercharge/mongodb-github-action@v1.10.0
87
+ uses: supercharge/mongodb-github-action@1.12.0
74
88
  with:
75
89
  mongodb-version: 4.0
76
90
  - name: Check out repository code
@@ -8,7 +8,7 @@ jobs:
8
8
  services:
9
9
  redis:
10
10
  image: redis
11
- ports: ['6379:6379']
11
+ ports: ["6379:6379"]
12
12
  options: >-
13
13
  --health-cmd "redis-cli ping"
14
14
  --health-interval 10s
@@ -16,21 +16,35 @@ jobs:
16
16
  --health-retries 5
17
17
  strategy:
18
18
  matrix:
19
- ruby: ['2.6', '2.7', '3.0', '3.1', '3.2', '3.3']
20
- rails: ['5.2', '6.0.0', '6.1.0', '7.0.0', '7.1.0']
19
+ ruby: ["2.6", "2.7", "3.0", "3.1", "3.2", "3.3"]
20
+ rails: ["5.2", "6.0.0", "6.1.0", "7.0.0", "7.1.0", "7.2.0", "8.0.0"]
21
21
  exclude:
22
22
  - ruby: "2.6"
23
23
  rails: "7.1.0"
24
24
  - ruby: "2.6"
25
25
  rails: "7.0.0"
26
+ - ruby: "2.6"
27
+ rails: "7.2.0"
28
+ - ruby: "2.6"
29
+ rails: "8.0.0"
26
30
  - ruby: "2.7"
27
31
  rails: "7.1.0"
32
+ - ruby: "2.7"
33
+ rails: "7.2.0"
34
+ - ruby: "2.7"
35
+ rails: "8.0.0"
28
36
  - ruby: "3.0"
29
37
  rails: "5.2"
38
+ - ruby: "3.0"
39
+ rails: "7.2.0"
40
+ - ruby: "3.0"
41
+ rails: "8.0.0"
30
42
  - ruby: "3.1"
31
43
  rails: "5.2"
32
44
  - ruby: "3.1"
33
45
  rails: "6.0.0"
46
+ - ruby: "3.1"
47
+ rails: "8.0.0"
34
48
  - ruby: "3.2"
35
49
  rails: "5.2"
36
50
  - ruby: "3.2"
@@ -44,7 +58,7 @@ jobs:
44
58
  - ruby: "3.3"
45
59
  rails: "6.1.0"
46
60
  env:
47
- SQLITE3_VERSION: 1.4.1
61
+ SQLITE3_VERSION: ${{ matrix.rails == '8.0.0' && '2.1.0' || '1.4.1' }}
48
62
  REDIS_URL: redis://localhost:6379/0
49
63
  CI: true
50
64
  RAILS_VERSION: ${{ matrix.rails }}
@@ -52,7 +66,7 @@ jobs:
52
66
  - name: Setup memcached
53
67
  uses: KeisukeYamashita/memcached-actions@v1
54
68
  - name: Start MongoDB
55
- uses: supercharge/mongodb-github-action@v1.10.0
69
+ uses: supercharge/mongodb-github-action@1.12.0
56
70
  with:
57
71
  mongodb-version: 4.0
58
72
  - name: Check out repository code
data/CLAUDE.md ADDED
@@ -0,0 +1,74 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Development Commands
6
+
7
+ ### Testing
8
+ - `bundle exec rake` - Run all tests (RSpec, Minitest, and Rails tests)
9
+ - `bundle exec rspec` - Run RSpec tests only
10
+ - `bundle exec rake spec:ui` - Run UI-specific specs
11
+ - `bundle exec rake test` - Run Minitest tests only
12
+ - `bundle exec rake test_rails` - Run Rails generator tests
13
+ - `script/test` - Bootstrap and run tests across multiple Rails versions (5.0-8.0)
14
+
15
+ ### Development Setup
16
+ - `script/bootstrap` - Bundle install dependencies and setup binstubs
17
+ - `script/console` - Start interactive console with Flipper loaded (uses Pry)
18
+ - `script/server` - Start local UI server on port 9999 for testing web interface
19
+
20
+ ### Building and Releasing
21
+ - `bundle exec rake build` - Build all gems into pkg/ directory
22
+ - `bundle exec rake release` - Tag version, push to remote, and push gems (requires OTP)
23
+
24
+ ## Architecture Overview
25
+
26
+ Flipper is a feature flag library for Ruby with a modular adapter-based architecture:
27
+
28
+ ### Core Components
29
+
30
+ **DSL Layer** (`lib/flipper/dsl.rb`):
31
+ - Main interface for feature flag operations
32
+ - Delegates to Feature instances
33
+ - Handles memoization and instrumentation
34
+ - Thread-safe instance management
35
+
36
+ **Feature** (`lib/flipper/feature.rb`):
37
+ - Represents individual feature flags
38
+ - Manages enable/disable operations through gates
39
+ - Handles instrumentation events
40
+ - Works with adapters for persistence
41
+
42
+ **Adapters** (`lib/flipper/adapters/`):
43
+ - Pluggable storage backends (Redis, ActiveRecord, Memory, etc.)
44
+ - Common interface for all storage implementations
45
+ - Support for caching, failover, and synchronization patterns
46
+
47
+ **Gates** (`lib/flipper/gates/`):
48
+ - Different targeting mechanisms:
49
+ - Boolean (on/off for everyone)
50
+ - Actor (specific users/entities)
51
+ - Group (predefined user groups)
52
+ - Percentage of Actors (rollout to X% of users)
53
+ - Percentage of Time (probabilistic enabling)
54
+ - Expression (complex conditional logic)
55
+
56
+ ### Multi-Gem Structure
57
+
58
+ The project is structured as multiple gems:
59
+ - `flipper` - Core library
60
+ - `flipper-ui` - Web interface
61
+ - `flipper-api` - REST API
62
+ - `flipper-cloud` - Cloud service integration
63
+ - `flipper-*` - Various adapter gems (redis, active_record, mongo, etc.)
64
+
65
+ ### Key Patterns
66
+
67
+ **Configuration**: Global configuration through `Flipper.configure` with per-thread instances
68
+ **Instrumentation**: Built-in event system for monitoring and debugging
69
+ **Memoization**: Automatic caching of feature checks within request/thread scope
70
+ **Type Safety**: Strong typing system for actors, percentages, and other values
71
+
72
+ ### Testing
73
+
74
+ Uses both RSpec (currently preferred for new tests) and Minitest. Shared adapter specs ensure consistency across all storage backends. Extensive testing across multiple Rails versions (5.0-8.0).
data/Gemfile CHANGED
@@ -6,16 +6,19 @@ Dir['flipper-*.gemspec'].each do |gemspec|
6
6
  gemspec(name: "flipper-#{plugin}", development_group: plugin)
7
7
  end
8
8
 
9
+ gem 'concurrent-ruby', '1.3.4'
10
+ gem 'connection_pool'
9
11
  gem 'debug'
10
12
  gem 'rake'
11
13
  gem 'statsd-ruby', '~> 1.2.1'
12
14
  gem 'rspec', '~> 3.0'
13
15
  gem 'rack-test'
14
- gem 'rackup'
15
- gem 'sqlite3', "~> #{ENV['SQLITE3_VERSION'] || '1.4.1'}"
16
- gem 'rails', "~> #{ENV['RAILS_VERSION'] || '7.1'}"
16
+ gem 'rackup', '= 1.0.0'
17
+ gem 'sqlite3', "~> #{ENV['SQLITE3_VERSION'] || '2.1.0'}"
18
+ gem 'rails', "~> #{ENV['RAILS_VERSION'] || '8.0'}"
17
19
  gem 'minitest', '~> 5.18'
18
20
  gem 'minitest-documentation'
21
+ gem 'pstore'
19
22
  gem 'webmock'
20
23
  gem 'ice_age'
21
24
  gem 'redis-namespace'
@@ -28,6 +31,7 @@ gem 'mysql2'
28
31
  gem 'pg'
29
32
  gem 'cuprite'
30
33
  gem 'puma'
34
+ gem 'warning'
31
35
 
32
36
  group(:guard) do
33
37
  gem 'guard'
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  [![Flipper Mark](docs/images/banner.jpg)](https://www.flippercloud.io)
2
2
 
3
- [Website](https://flippercloud.io?utm_source=oss&utm_medium=readme&utm_campaign=website_link) | [Documentation](https://flippercloud.io/docs?utm_source=oss&utm_medium=readme&utm_campaign=docs_link) | [Examples](examples) | [Twitter](https://twitter.com/flipper_cloud) | [Ruby.social](https://ruby.social/@flipper)
3
+ [Website](https://flippercloud.io?utm_source=oss&utm_medium=readme&utm_campaign=website_link) | [Documentation](https://flippercloud.io/docs?utm_source=oss&utm_medium=readme&utm_campaign=docs_link) | [Examples](examples) | [Chat](https://chat.flippercloud.io/join/xjHq-aJsA-BeZH) | [Twitter](https://twitter.com/flipper_cloud) | [Ruby.social](https://ruby.social/@flipper)
4
4
 
5
5
  # Flipper
6
6
 
@@ -5,7 +5,7 @@ require 'flipper/cloud/telemetry/backoff_policy'
5
5
  intervals = []
6
6
  policy = Flipper::Cloud::Telemetry::BackoffPolicy.new
7
7
 
8
- 10.times do |n|
8
+ 5.times do |n|
9
9
  intervals << policy.next_interval
10
10
  end
11
11
 
@@ -3,7 +3,7 @@ module Flipper
3
3
  #
4
4
  # adapter = Flipper::AdapterBuilder.new do
5
5
  # use Flipper::Adapters::Strict
6
- # use Flipper::Adapters::Memoizer
6
+ # use Flipper::Adapters::Memoizable
7
7
  # store Flipper::Adapters::Memory
8
8
  # end.to_adapter
9
9
  #
@@ -20,7 +20,7 @@ module Flipper
20
20
  if more_info = data["more_info"]
21
21
  message << "\n#{data["more_info"]}"
22
22
  end
23
- rescue => exception
23
+ rescue
24
24
  # welp we tried
25
25
  end
26
26
 
@@ -59,8 +59,8 @@ module Flipper
59
59
  response = @client.get("/features?exclude_gate_names=true")
60
60
  raise Error, response unless response.is_a?(Net::HTTPOK)
61
61
 
62
- parsed_response = Typecast.from_json(response.body)
63
- parsed_features = parsed_response.fetch('features')
62
+ parsed_response = response.body.empty? ? {} : Typecast.from_json(response.body)
63
+ parsed_features = parsed_response['features'] || []
64
64
  gates_by_key = parsed_features.each_with_object({}) do |parsed_feature, hash|
65
65
  hash[parsed_feature['key']] = parsed_feature['gates']
66
66
  hash
@@ -18,6 +18,21 @@ module Flipper
18
18
  @adapter = adapter
19
19
  @poller = poller
20
20
  @last_synced_at = 0
21
+
22
+ # If the adapter is empty, we need to sync before starting the poller.
23
+ # Yes, this will block the main thread, but that's better than thinking
24
+ # nothing is enabled.
25
+ if adapter.features.empty?
26
+ begin
27
+ @poller.sync
28
+ rescue
29
+ # TODO: Warn here that it's possible that no data has been synced
30
+ # and flags are being evaluated without flag data being present
31
+ # until a sync completes. We rescue to avoid flipper being down
32
+ # causing your processes to crash.
33
+ end
34
+ end
35
+
21
36
  @poller.start
22
37
  end
23
38
 
@@ -3,7 +3,6 @@ require "socket"
3
3
  require "flipper/adapters/http"
4
4
  require "flipper/adapters/poll"
5
5
  require "flipper/poller"
6
- require "flipper/adapters/memory"
7
6
  require "flipper/adapters/dual_write"
8
7
  require "flipper/adapters/sync/synchronizer"
9
8
  require "flipper/cloud/telemetry"
@@ -135,6 +134,10 @@ module Flipper
135
134
  logger.send(level, "name=flipper_cloud #{message}")
136
135
  end
137
136
 
137
+ def instrument(name, payload = {}, &block)
138
+ instrumenter.instrument(name, payload, &block)
139
+ end
140
+
138
141
  private
139
142
 
140
143
  def app_adapter
@@ -242,7 +245,7 @@ module Flipper
242
245
  if required
243
246
  option_value = send(name)
244
247
  if option_value.nil? || option_value.empty?
245
- message = "Flipper::Cloud #{name} is missing. Please "
248
+ message = String.new("Flipper::Cloud #{name} is missing. Please ")
246
249
  message << "set #{env_var} or " if from_env
247
250
  message << "provide #{name} (e.g. Flipper::Cloud.new(#{name}: value))."
248
251
  raise ArgumentError, message
@@ -3,10 +3,10 @@ module Flipper
3
3
  class Telemetry
4
4
  class BackoffPolicy
5
5
  # Private: The default minimum timeout between intervals in milliseconds.
6
- MIN_TIMEOUT_MS = 1_000
6
+ MIN_TIMEOUT_MS = 30_000
7
7
 
8
8
  # Private: The default maximum timeout between intervals in milliseconds.
9
- MAX_TIMEOUT_MS = 30_000
9
+ MAX_TIMEOUT_MS = 120_000
10
10
 
11
11
  # Private: The value to multiply the current interval with for each
12
12
  # retry attempt.
@@ -67,7 +67,10 @@ module Flipper
67
67
 
68
68
  @attempts += 1
69
69
 
70
- [interval, @max_timeout_ms].min
70
+ # cap the interval to the max timeout
71
+ result = [interval, @max_timeout_ms].min
72
+ # jitter even when maxed out
73
+ result == @max_timeout_ms ? add_jitter(result, 0.05) : result
71
74
  end
72
75
 
73
76
  def reset
@@ -34,7 +34,7 @@ module Flipper
34
34
  return if drained.empty?
35
35
  body = to_body(drained)
36
36
  return if body.nil? || body.empty?
37
- retry_with_backoff(10) { submit(body) }
37
+ retry_with_backoff(5) { submit(body) }
38
38
  end
39
39
 
40
40
  private
@@ -51,6 +51,7 @@ module Flipper
51
51
 
52
52
  Typecast.to_gzip(json)
53
53
  rescue => exception
54
+ @cloud_configuration.instrument "telemetry_error.#{Flipper::InstrumentationNamespace}", exception: exception, request_id: request_id
54
55
  @cloud_configuration.log "action=to_body request_id=#{request_id} error=#{exception.inspect}", level: :error
55
56
  end
56
57
 
@@ -63,6 +64,7 @@ module Flipper
63
64
  result, should_retry = yield
64
65
  return [result, nil] unless should_retry
65
66
  rescue => error
67
+ @cloud_configuration.instrument "telemetry_retry.#{Flipper::InstrumentationNamespace}", attempts_remaining: attempts_remaining, exception: error
66
68
  @cloud_configuration.log "action=post_to_cloud attempts_remaining=#{attempts_remaining} error=#{error.inspect}", level: :error
67
69
  should_retry = true
68
70
  caught_exception = error
@@ -75,8 +75,8 @@ module Flipper
75
75
 
76
76
  @metric_storage = MetricStorage.new
77
77
 
78
- @pool = Concurrent::FixedThreadPool.new(2, {
79
- max_queue: 5,
78
+ @pool = Concurrent::FixedThreadPool.new(1, {
79
+ max_queue: 20, # ~ 20 minutes of data at 1 minute intervals
80
80
  fallback_policy: :discard,
81
81
  name: "flipper-telemetry-post-to-cloud-pool".freeze,
82
82
  })
@@ -1,5 +1,3 @@
1
- require "flipper/adapters/memory"
2
-
3
1
  module Flipper
4
2
  class Export
5
3
  attr_reader :contents, :format, :version
@@ -1,5 +1,3 @@
1
- require "flipper/expression"
2
-
3
1
  module Flipper
4
2
  module Expressions
5
3
  class All
@@ -100,7 +100,14 @@ module Flipper
100
100
  #
101
101
  # Returns true if enabled, false if not.
102
102
  def enabled?(*actors)
103
- actors = actors.flatten.compact.map { |actor| Types::Actor.wrap(actor) }
103
+ actors = Array(actors).
104
+ # Avoids to_ary warning that happens when passing DelegateClass of an
105
+ # ActiveRecord object and using flatten here. This is tested in
106
+ # spec/flipper/model/active_record_spec.rb.
107
+ flat_map { |actor| actor.is_a?(Array) ? actor : [actor] }.
108
+ # Allows null object pattern. See PR for more. https://github.com/flippercloud/flipper/pull/887
109
+ reject(&:nil?).
110
+ map { |actor| Types::Actor.wrap(actor) }
104
111
  actors = nil if actors.empty?
105
112
 
106
113
  # thing is left for backwards compatibility
data/lib/flipper/gate.rb CHANGED
@@ -26,7 +26,7 @@ module Flipper
26
26
  # in subclass.
27
27
  #
28
28
  # Returns true if gate open for any actor, false if not.
29
- def open?(actors, value, options = {})
29
+ def open?(context)
30
30
  false
31
31
  end
32
32
 
@@ -53,11 +53,10 @@ module Flipper
53
53
 
54
54
  feature_name = event.payload[:feature_name]
55
55
  adapter_name = event.payload[:adapter_name]
56
- gate_name = event.payload[:gate_name]
57
56
  operation = event.payload[:operation]
58
57
  result = event.payload[:result]
59
58
 
60
- description = 'Flipper '
59
+ description = String.new('Flipper ')
61
60
  description << "feature(#{feature_name}) " unless feature_name.nil?
62
61
  description << "adapter(#{adapter_name}) "
63
62
  description << "#{operation} "
@@ -2,5 +2,7 @@ require 'securerandom'
2
2
  require 'active_support/notifications'
3
3
  require 'flipper/instrumentation/statsd_subscriber'
4
4
 
5
- ActiveSupport::Notifications.subscribe /\.flipper$/,
6
- Flipper::Instrumentation::StatsdSubscriber
5
+ ActiveSupport::Notifications.subscribe(
6
+ /\.flipper$/,
7
+ Flipper::Instrumentation::StatsdSubscriber
8
+ )
@@ -42,7 +42,6 @@ module Flipper
42
42
  # Private
43
43
  def update_feature_operation_metrics
44
44
  feature_name = @payload[:feature_name]
45
- gate_name = @payload[:gate_name]
46
45
  operation = strip_trailing_question_mark(@payload[:operation])
47
46
  result = @payload[:result]
48
47
 
@@ -64,9 +63,6 @@ module Flipper
64
63
  def update_adapter_operation_metrics
65
64
  adapter_name = @payload[:adapter_name]
66
65
  operation = @payload[:operation]
67
- result = @payload[:result]
68
- value = @payload[:value]
69
- key = @payload[:key]
70
66
 
71
67
  update_timer "flipper.adapter.#{adapter_name}.#{operation}"
72
68
  end
@@ -7,5 +7,6 @@ module Flipper
7
7
  "source_code_uri" => "https://github.com/flippercloud/flipper",
8
8
  "bug_tracker_uri" => "https://github.com/flippercloud/flipper/issues",
9
9
  "changelog_uri" => "https://github.com/flippercloud/flipper/releases/tag/v#{Flipper::VERSION}",
10
+ "funding_uri" => "https://github.com/sponsors/flippercloud",
10
11
  }.freeze
11
12
  end
@@ -59,10 +59,10 @@ module Flipper
59
59
  def run
60
60
  loop do
61
61
  sleep jitter
62
- start = Concurrent.monotonic_time
62
+
63
63
  begin
64
64
  sync
65
- rescue => exception
65
+ rescue
66
66
  # you can instrument these using poller.flipper
67
67
  end
68
68
 
@@ -1,5 +1,5 @@
1
1
  module Flipper
2
- VERSION = '1.3.0'.freeze
2
+ VERSION = '1.3.6'.freeze
3
3
 
4
4
  REQUIRED_RUBY_VERSION = '2.6'.freeze
5
5
  NEXT_REQUIRED_RUBY_VERSION = '3.0'.freeze
@@ -4,10 +4,15 @@ module Flipper
4
4
  module Generators
5
5
  class SetupGenerator < ::Rails::Generators::Base
6
6
  desc 'Peform any necessary steps to install Flipper'
7
+ source_paths << File.expand_path('templates', __dir__)
7
8
 
8
9
  class_option :token, type: :string, default: nil, aliases: '-t',
9
10
  desc: "Your personal environment token for Flipper Cloud"
10
11
 
12
+ def generate_initializer
13
+ template 'initializer.rb', 'config/initializers/flipper.rb'
14
+ end
15
+
11
16
  def generate_active_record
12
17
  invoke 'flipper:active_record' if defined?(Flipper::Adapters::ActiveRecord)
13
18
  end
@@ -0,0 +1,45 @@
1
+ Rails.application.configure do
2
+ ## Memoization ensures that only one adapter call is made per feature per request.
3
+ ## For more info, see https://www.flippercloud.io/docs/optimization#memoization
4
+ # config.flipper.memoize = true
5
+
6
+ ## Flipper preloads all features before each request, which is recommended if:
7
+ ## * you have a limited number of features (< 100?)
8
+ ## * most of your requests depend on most of your features
9
+ ## * you have limited gate data combined across all features (< 1k enabled gates, like individual actors, across all features)
10
+ ##
11
+ ## For more info, see https://www.flippercloud.io/docs/optimization#preloading
12
+ # config.flipper.preload = true
13
+
14
+ ## Warn or raise an error if an unknown feature is checked
15
+ ## Can be set to `:warn`, `:raise`, or `false`
16
+ # config.flipper.strict = Rails.env.development? && :warn
17
+
18
+ ## Show Flipper checks in logs
19
+ # config.flipper.log = true
20
+
21
+ ## Reconfigure Flipper to use the Memory adapter and disable Cloud in tests
22
+ # config.flipper.test_help = true
23
+
24
+ ## The path that Flipper Cloud will use to sync features
25
+ # config.flipper.cloud_path = "_flipper"
26
+
27
+ ## The instrumenter that Flipper will use. Defaults to ActiveSupport::Notifications.
28
+ # config.flipper.instrumenter = ActiveSupport::Notifications
29
+ end
30
+
31
+ Flipper.configure do |config|
32
+ ## Configure other adapters that you want to use here:
33
+ ## See http://flippercloud.io/docs/adapters
34
+ # config.use Flipper::Adapters::ActiveSupportCacheStore, Rails.cache, expires_in: 5.minutes
35
+ end
36
+
37
+ ## Register a group that can be used for enabling features.
38
+ ##
39
+ ## Flipper.enable_group :my_feature, :admins
40
+ ##
41
+ ## See https://www.flippercloud.io/docs/features#enablement-group
42
+ #
43
+ # Flipper.register(:admins) do |actor|
44
+ # actor.respond_to?(:admin?) && actor.admin?
45
+ # end
@@ -28,6 +28,7 @@ RSpec.describe Flipper::Adapters::Http do
28
28
  end
29
29
 
30
30
  before :all do
31
+ @started = false
31
32
  dir = FlipperRoot.join('tmp').tap(&:mkpath)
32
33
  log_path = dir.join('flipper_adapters_http_spec.log')
33
34
  @pstore_file = dir.join('flipper.pstore')
@@ -0,0 +1,41 @@
1
+ require 'flipper/adapters/poll'
2
+
3
+ RSpec.describe Flipper::Adapters::Poll do
4
+ let(:remote_adapter) {
5
+ adapter = Flipper::Adapters::Memory.new(threadsafe: true)
6
+ flipper = Flipper.new(adapter)
7
+ flipper.enable(:search)
8
+ flipper.enable(:analytics)
9
+ adapter
10
+ }
11
+ let(:local_adapter) { Flipper::Adapters::Memory.new(threadsafe: true) }
12
+ let(:poller) {
13
+ Flipper::Poller.get("for_spec", {
14
+ start_automatically: false,
15
+ remote_adapter: remote_adapter,
16
+ })
17
+ }
18
+
19
+ it "syncs in main thread if local adapter is empty" do
20
+ instance = described_class.new(poller, local_adapter)
21
+ instance.features # call something to force sync
22
+ expect(local_adapter.features).to eq(remote_adapter.features)
23
+ end
24
+
25
+ it "does not sync in main thread if local adapter is not empty" do
26
+ # make local not empty by importing remote
27
+ flipper = Flipper.new(local_adapter)
28
+ flipper.import(remote_adapter)
29
+
30
+ # make a fake poller to verify calls
31
+ poller = double("Poller", last_synced_at: Concurrent::AtomicFixnum.new(0))
32
+ expect(poller).to receive(:start).twice
33
+ expect(poller).not_to receive(:sync)
34
+
35
+ # create new instance and call something to force sync
36
+ instance = described_class.new(poller, local_adapter)
37
+ instance.features # call something to force sync
38
+
39
+ expect(local_adapter.features).to eq(remote_adapter.features)
40
+ end
41
+ end