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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +19 -5
- data/.github/workflows/examples.yml +19 -5
- data/CLAUDE.md +74 -0
- data/Gemfile +7 -3
- data/README.md +1 -1
- data/examples/cloud/backoff_policy.rb +1 -1
- data/lib/flipper/adapter_builder.rb +1 -1
- data/lib/flipper/adapters/http/error.rb +1 -1
- data/lib/flipper/adapters/http.rb +2 -2
- data/lib/flipper/adapters/poll.rb +15 -0
- data/lib/flipper/cloud/configuration.rb +5 -2
- data/lib/flipper/cloud/telemetry/backoff_policy.rb +6 -3
- data/lib/flipper/cloud/telemetry/submitter.rb +3 -1
- data/lib/flipper/cloud/telemetry.rb +2 -2
- data/lib/flipper/export.rb +0 -2
- data/lib/flipper/expressions/all.rb +0 -2
- data/lib/flipper/feature.rb +8 -1
- data/lib/flipper/gate.rb +1 -1
- data/lib/flipper/instrumentation/log_subscriber.rb +1 -2
- data/lib/flipper/instrumentation/statsd.rb +4 -2
- data/lib/flipper/instrumentation/subscriber.rb +0 -4
- data/lib/flipper/metadata.rb +1 -0
- data/lib/flipper/poller.rb +2 -2
- data/lib/flipper/version.rb +1 -1
- data/lib/generators/flipper/setup_generator.rb +5 -0
- data/lib/generators/flipper/templates/initializer.rb +45 -0
- data/spec/flipper/adapters/http_spec.rb +1 -0
- data/spec/flipper/adapters/poll_spec.rb +41 -0
- data/spec/flipper/adapters/strict_spec.rb +2 -2
- data/spec/flipper/cli_spec.rb +4 -2
- data/spec/flipper/cloud/dsl_spec.rb +1 -1
- data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +3 -3
- data/spec/flipper/cloud/telemetry/submitter_spec.rb +4 -4
- data/spec/flipper/cloud/telemetry_spec.rb +6 -6
- data/spec/flipper/cloud_spec.rb +9 -4
- data/spec/flipper/dsl_spec.rb +0 -3
- data/spec/flipper/engine_spec.rb +8 -7
- data/spec/flipper/feature_spec.rb +22 -11
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +1 -0
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +1 -1
- data/spec/flipper/middleware/memoizer_spec.rb +5 -6
- data/spec/flipper/model/active_record_spec.rb +11 -0
- data/spec/flipper_spec.rb +1 -1
- data/spec/spec_helper.rb +11 -5
- data/spec/support/fail_on_output.rb +1 -1
- data/spec/support/spec_helpers.rb +13 -2
- data/test_rails/generators/flipper/setup_generator_test.rb +5 -0
- data/test_rails/generators/flipper/update_generator_test.rb +1 -1
- data/test_rails/helper.rb +3 -0
- data/test_rails/system/test_help_test.rb +1 -0
- metadata +9 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8f53e9dadf45ea1d00e7abd24358748347d516d761ea347c438057f62fdbc185
|
4
|
+
data.tar.gz: 8c2a73d793d41ce64ed26c559bf532127f73e17e20944930aa6bf14aff355a57
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: af26df5e8c10540348e2b8dd31f11198dafdc8f3bcb6ef8e5361e3a764caa5a872734c64807746f1d077cfbc95df9143959e4c3446429dce1d5d871e46861fa7
|
7
|
+
data.tar.gz: 2005a736ea665c359154bf85f56495fcf9b326e9ecb740d2e1aca0c52a698367b44334a90ae2b1277c2a3c0dcdddcc478eab3126975f210fc796a454202913f4
|
data/.github/workflows/ci.yml
CHANGED
@@ -7,7 +7,7 @@ jobs:
|
|
7
7
|
services:
|
8
8
|
redis:
|
9
9
|
image: redis
|
10
|
-
ports: [
|
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: [
|
32
|
-
rails: [
|
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@
|
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: [
|
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: [
|
20
|
-
rails: [
|
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@
|
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.
|
16
|
-
gem 'rails', "~> #{ENV['RAILS_VERSION'] || '
|
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
|
[](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
|
|
@@ -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
|
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 =
|
6
|
+
MIN_TIMEOUT_MS = 30_000
|
7
7
|
|
8
8
|
# Private: The default maximum timeout between intervals in milliseconds.
|
9
|
-
MAX_TIMEOUT_MS =
|
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
|
-
|
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(
|
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(
|
79
|
-
max_queue:
|
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
|
})
|
data/lib/flipper/export.rb
CHANGED
data/lib/flipper/feature.rb
CHANGED
@@ -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.
|
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
@@ -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
|
6
|
-
|
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
|
data/lib/flipper/metadata.rb
CHANGED
@@ -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
|
data/lib/flipper/poller.rb
CHANGED
data/lib/flipper/version.rb
CHANGED
@@ -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
|
@@ -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
|