flipper 1.0.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 (180) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/ci.yml +50 -7
  4. data/.github/workflows/examples.yml +50 -8
  5. data/CLAUDE.md +74 -0
  6. data/Changelog.md +1 -584
  7. data/Gemfile +15 -8
  8. data/README.md +31 -27
  9. data/Rakefile +2 -2
  10. data/benchmark/typecast_ips.rb +8 -0
  11. data/docs/images/banner.jpg +0 -0
  12. data/docs/images/flipper_cloud.png +0 -0
  13. data/examples/cloud/backoff_policy.rb +13 -0
  14. data/examples/cloud/cloud_setup.rb +16 -0
  15. data/examples/cloud/forked.rb +7 -2
  16. data/examples/cloud/threaded.rb +15 -18
  17. data/examples/expressions.rb +213 -0
  18. data/examples/strict.rb +18 -0
  19. data/exe/flipper +5 -0
  20. data/flipper.gemspec +6 -3
  21. data/lib/flipper/actor.rb +6 -3
  22. data/lib/flipper/adapter.rb +10 -0
  23. data/lib/flipper/adapter_builder.rb +44 -0
  24. data/lib/flipper/adapters/actor_limit.rb +28 -0
  25. data/lib/flipper/adapters/cache_base.rb +143 -0
  26. data/lib/flipper/adapters/dual_write.rb +1 -3
  27. data/lib/flipper/adapters/failover.rb +0 -4
  28. data/lib/flipper/adapters/failsafe.rb +0 -4
  29. data/lib/flipper/adapters/http/client.rb +40 -12
  30. data/lib/flipper/adapters/http/error.rb +2 -2
  31. data/lib/flipper/adapters/http.rb +19 -14
  32. data/lib/flipper/adapters/instrumented.rb +0 -4
  33. data/lib/flipper/adapters/memoizable.rb +14 -19
  34. data/lib/flipper/adapters/memory.rb +4 -6
  35. data/lib/flipper/adapters/operation_logger.rb +18 -92
  36. data/lib/flipper/adapters/poll.rb +16 -3
  37. data/lib/flipper/adapters/pstore.rb +17 -11
  38. data/lib/flipper/adapters/read_only.rb +8 -41
  39. data/lib/flipper/adapters/strict.rb +45 -0
  40. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  41. data/lib/flipper/adapters/sync.rb +0 -4
  42. data/lib/flipper/adapters/wrapper.rb +54 -0
  43. data/lib/flipper/cli.rb +263 -0
  44. data/lib/flipper/cloud/configuration.rb +131 -54
  45. data/lib/flipper/cloud/middleware.rb +5 -5
  46. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  47. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  48. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  49. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  50. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  51. data/lib/flipper/cloud/telemetry.rb +191 -0
  52. data/lib/flipper/cloud.rb +1 -1
  53. data/lib/flipper/configuration.rb +25 -4
  54. data/lib/flipper/dsl.rb +51 -0
  55. data/lib/flipper/engine.rb +42 -3
  56. data/lib/flipper/export.rb +0 -2
  57. data/lib/flipper/exporters/json/export.rb +1 -1
  58. data/lib/flipper/exporters/json/v1.rb +1 -1
  59. data/lib/flipper/expression/builder.rb +73 -0
  60. data/lib/flipper/expression/constant.rb +25 -0
  61. data/lib/flipper/expression.rb +71 -0
  62. data/lib/flipper/expressions/all.rb +9 -0
  63. data/lib/flipper/expressions/any.rb +9 -0
  64. data/lib/flipper/expressions/boolean.rb +9 -0
  65. data/lib/flipper/expressions/comparable.rb +13 -0
  66. data/lib/flipper/expressions/duration.rb +28 -0
  67. data/lib/flipper/expressions/equal.rb +9 -0
  68. data/lib/flipper/expressions/greater_than.rb +9 -0
  69. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  70. data/lib/flipper/expressions/less_than.rb +9 -0
  71. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  72. data/lib/flipper/expressions/not_equal.rb +9 -0
  73. data/lib/flipper/expressions/now.rb +9 -0
  74. data/lib/flipper/expressions/number.rb +9 -0
  75. data/lib/flipper/expressions/percentage.rb +9 -0
  76. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  77. data/lib/flipper/expressions/property.rb +9 -0
  78. data/lib/flipper/expressions/random.rb +9 -0
  79. data/lib/flipper/expressions/string.rb +9 -0
  80. data/lib/flipper/expressions/time.rb +9 -0
  81. data/lib/flipper/feature.rb +63 -1
  82. data/lib/flipper/gate.rb +2 -1
  83. data/lib/flipper/gate_values.rb +5 -2
  84. data/lib/flipper/gates/expression.rb +75 -0
  85. data/lib/flipper/instrumentation/log_subscriber.rb +13 -5
  86. data/lib/flipper/instrumentation/statsd.rb +4 -2
  87. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  88. data/lib/flipper/instrumentation/subscriber.rb +0 -4
  89. data/lib/flipper/metadata.rb +4 -1
  90. data/lib/flipper/middleware/memoizer.rb +29 -13
  91. data/lib/flipper/model/active_record.rb +23 -0
  92. data/lib/flipper/poller.rb +9 -8
  93. data/lib/flipper/serializers/gzip.rb +22 -0
  94. data/lib/flipper/serializers/json.rb +17 -0
  95. data/lib/flipper/spec/shared_adapter_specs.rb +46 -27
  96. data/lib/flipper/test/shared_adapter_test.rb +41 -22
  97. data/lib/flipper/test_help.rb +43 -0
  98. data/lib/flipper/typecast.rb +37 -9
  99. data/lib/flipper/types/percentage.rb +1 -1
  100. data/lib/flipper/version.rb +11 -1
  101. data/lib/flipper.rb +41 -2
  102. data/lib/generators/flipper/setup_generator.rb +68 -0
  103. data/lib/generators/flipper/templates/initializer.rb +45 -0
  104. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  105. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  106. data/lib/generators/flipper/update_generator.rb +35 -0
  107. data/package-lock.json +41 -0
  108. data/package.json +10 -0
  109. data/spec/fixtures/environment.rb +1 -0
  110. data/spec/flipper/adapter_builder_spec.rb +72 -0
  111. data/spec/flipper/adapter_spec.rb +1 -0
  112. data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
  113. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  114. data/spec/flipper/adapters/http_spec.rb +135 -74
  115. data/spec/flipper/adapters/memoizable_spec.rb +15 -15
  116. data/spec/flipper/adapters/poll_spec.rb +41 -0
  117. data/spec/flipper/adapters/read_only_spec.rb +26 -11
  118. data/spec/flipper/adapters/strict_spec.rb +64 -0
  119. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  120. data/spec/flipper/cli_spec.rb +166 -0
  121. data/spec/flipper/cloud/configuration_spec.rb +39 -57
  122. data/spec/flipper/cloud/dsl_spec.rb +6 -6
  123. data/spec/flipper/cloud/middleware_spec.rb +8 -8
  124. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  125. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  126. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  127. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  128. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  129. data/spec/flipper/cloud_spec.rb +31 -25
  130. data/spec/flipper/configuration_spec.rb +17 -0
  131. data/spec/flipper/dsl_spec.rb +39 -3
  132. data/spec/flipper/engine_spec.rb +226 -42
  133. data/spec/flipper/exporters/json/v1_spec.rb +3 -3
  134. data/spec/flipper/expression/builder_spec.rb +248 -0
  135. data/spec/flipper/expression_spec.rb +188 -0
  136. data/spec/flipper/expressions/all_spec.rb +15 -0
  137. data/spec/flipper/expressions/any_spec.rb +15 -0
  138. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  139. data/spec/flipper/expressions/duration_spec.rb +43 -0
  140. data/spec/flipper/expressions/equal_spec.rb +24 -0
  141. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  142. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  143. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  144. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  145. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  146. data/spec/flipper/expressions/now_spec.rb +11 -0
  147. data/spec/flipper/expressions/number_spec.rb +21 -0
  148. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  149. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  150. data/spec/flipper/expressions/property_spec.rb +13 -0
  151. data/spec/flipper/expressions/random_spec.rb +9 -0
  152. data/spec/flipper/expressions/string_spec.rb +11 -0
  153. data/spec/flipper/expressions/time_spec.rb +13 -0
  154. data/spec/flipper/feature_spec.rb +380 -10
  155. data/spec/flipper/gate_values_spec.rb +2 -2
  156. data/spec/flipper/gates/expression_spec.rb +108 -0
  157. data/spec/flipper/identifier_spec.rb +4 -5
  158. data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -2
  159. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +16 -2
  160. data/spec/flipper/middleware/memoizer_spec.rb +79 -10
  161. data/spec/flipper/model/active_record_spec.rb +72 -0
  162. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  163. data/spec/flipper/serializers/json_spec.rb +13 -0
  164. data/spec/flipper/typecast_spec.rb +43 -7
  165. data/spec/flipper/types/actor_spec.rb +18 -1
  166. data/spec/flipper_integration_spec.rb +102 -4
  167. data/spec/flipper_spec.rb +91 -3
  168. data/spec/spec_helper.rb +17 -5
  169. data/spec/support/actor_names.yml +1 -0
  170. data/spec/support/fail_on_output.rb +8 -0
  171. data/spec/support/fake_backoff_policy.rb +15 -0
  172. data/spec/support/spec_helpers.rb +34 -8
  173. data/test/adapters/actor_limit_test.rb +20 -0
  174. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  175. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  176. data/test_rails/helper.rb +22 -2
  177. data/test_rails/system/test_help_test.rb +52 -0
  178. metadata +145 -29
  179. data/lib/flipper/cloud/instrumenter.rb +0 -48
  180. data/spec/support/climate_control.rb +0 -7
@@ -0,0 +1,30 @@
1
+ require 'concurrent/map'
2
+ require 'concurrent/atomic/atomic_fixnum'
3
+
4
+ module Flipper
5
+ module Cloud
6
+ class Telemetry
7
+ class MetricStorage
8
+ def initialize
9
+ @storage = Concurrent::Map.new { |h, k| h[k] = Concurrent::AtomicFixnum.new(0) }
10
+ end
11
+
12
+ def increment(metric)
13
+ @storage[metric].increment
14
+ end
15
+
16
+ def drain
17
+ metrics = {}
18
+ @storage.keys.each do |metric|
19
+ metrics[metric] = @storage.delete(metric).value
20
+ end
21
+ metrics.freeze
22
+ end
23
+
24
+ def empty?
25
+ @storage.empty?
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,100 @@
1
+ require "securerandom"
2
+ require "flipper/typecast"
3
+ require "flipper/cloud/telemetry/backoff_policy"
4
+
5
+ module Flipper
6
+ module Cloud
7
+ class Telemetry
8
+ class Submitter
9
+ PATH = "/telemetry".freeze
10
+ SCHEMA_VERSION = "V1".freeze
11
+ GZIP_ENCODING = "gzip".freeze
12
+
13
+ Error = Class.new(StandardError) do
14
+ attr_reader :request_id, :response
15
+
16
+ def initialize(request_id, response)
17
+ @request_id = request_id
18
+ @response = response
19
+ super "Unexpected response code=#{response.code} request_id=#{request_id}"
20
+ end
21
+ end
22
+
23
+ attr_reader :cloud_configuration, :request_id, :backoff_policy
24
+
25
+ def initialize(cloud_configuration, backoff_policy: nil)
26
+ @cloud_configuration = cloud_configuration
27
+ @backoff_policy = backoff_policy || BackoffPolicy.new
28
+ @request_id = SecureRandom.uuid
29
+ end
30
+
31
+ # Returns Array of [response, error]. response and error could be nil
32
+ # but usually one or the other will be present.
33
+ def call(drained)
34
+ return if drained.empty?
35
+ body = to_body(drained)
36
+ return if body.nil? || body.empty?
37
+ retry_with_backoff(5) { submit(body) }
38
+ end
39
+
40
+ private
41
+
42
+ def to_body(drained)
43
+ enabled_metrics = drained.map { |metric, value|
44
+ metric.as_json(with: {"value" => value})
45
+ }
46
+
47
+ json = Typecast.to_json({
48
+ request_id: request_id,
49
+ enabled_metrics: enabled_metrics,
50
+ })
51
+
52
+ Typecast.to_gzip(json)
53
+ rescue => exception
54
+ @cloud_configuration.instrument "telemetry_error.#{Flipper::InstrumentationNamespace}", exception: exception, request_id: request_id
55
+ @cloud_configuration.log "action=to_body request_id=#{request_id} error=#{exception.inspect}", level: :error
56
+ end
57
+
58
+ def retry_with_backoff(attempts, &block)
59
+ result, caught_exception = nil
60
+ should_retry = false
61
+ attempts_remaining = attempts - 1
62
+
63
+ begin
64
+ result, should_retry = yield
65
+ return [result, nil] unless should_retry
66
+ rescue => error
67
+ @cloud_configuration.instrument "telemetry_retry.#{Flipper::InstrumentationNamespace}", attempts_remaining: attempts_remaining, exception: error
68
+ @cloud_configuration.log "action=post_to_cloud attempts_remaining=#{attempts_remaining} error=#{error.inspect}", level: :error
69
+ should_retry = true
70
+ caught_exception = error
71
+ end
72
+
73
+ if should_retry && attempts_remaining > 0
74
+ sleep @backoff_policy.next_interval.to_f / 1000
75
+ retry_with_backoff attempts_remaining, &block
76
+ else
77
+ [result, caught_exception]
78
+ end
79
+ end
80
+
81
+ def submit(body)
82
+ client = @cloud_configuration.http_client
83
+ client.add_header "schema-version", SCHEMA_VERSION
84
+ client.add_header "content-encoding", GZIP_ENCODING
85
+
86
+ response = client.post PATH, body
87
+ code = response.code.to_i
88
+
89
+ # Raise error and retry for retriable status codes.
90
+ # FIXME: what about redirects?
91
+ if code < 200 || code == 429 || code >= 500
92
+ raise Error.new(request_id, response)
93
+ end
94
+
95
+ response
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,191 @@
1
+ require "concurrent/map"
2
+ require "concurrent/timer_task"
3
+ require "concurrent/executor/fixed_thread_pool"
4
+ require "flipper/typecast"
5
+ require "flipper/cloud/telemetry/metric"
6
+ require "flipper/cloud/telemetry/metric_storage"
7
+ require "flipper/cloud/telemetry/submitter"
8
+
9
+ module Flipper
10
+ module Cloud
11
+ class Telemetry
12
+ # Internal: Map of instances of telemetry.
13
+ def self.instances
14
+ @instances ||= Concurrent::Map.new
15
+ end
16
+ private_class_method :instances
17
+
18
+ def self.reset
19
+ instances.each { |_, instance| instance.stop }.clear
20
+ end
21
+
22
+ # Internal: Fetch an instance of telemetry once per process per url +
23
+ # token (aka cloud endpoint). Should only ever be one instance unless you
24
+ # are doing some funky stuff.
25
+ def self.instance_for(cloud_configuration)
26
+ instances.compute_if_absent(cloud_configuration.url + cloud_configuration.token) do
27
+ new(cloud_configuration)
28
+ end
29
+ end
30
+
31
+ # Public: The cloud configuration to use for this telemetry instance.
32
+ attr_reader :cloud_configuration
33
+
34
+ # Internal: Where the metrics are stored between cloud submissions.
35
+ attr_reader :metric_storage
36
+
37
+ # Internal: The pool of background threads that submits metrics to cloud.
38
+ attr_reader :pool
39
+
40
+ # Internal: The timer that triggers draining the metrics to the pool.
41
+ attr_reader :timer
42
+
43
+ # Internal: The interval in seconds for how often telemetry should be sent to cloud.
44
+ attr_reader :interval
45
+
46
+ # Internal: The timeout in seconds for how long to wait for the pool to shutdown.
47
+ attr_reader :shutdown_timeout
48
+
49
+ # Internal: The proc that is called to submit metrics to cloud.
50
+ attr_accessor :submitter
51
+
52
+ def initialize(cloud_configuration)
53
+ @pid = $$
54
+ @cloud_configuration = cloud_configuration
55
+ self.interval = ENV.fetch("FLIPPER_TELEMETRY_INTERVAL", 60).to_f
56
+ self.shutdown_timeout = ENV.fetch("FLIPPER_TELEMETRY_SHUTDOWN_TIMEOUT", 5).to_f
57
+ self.submitter = ->(drained) { Submitter.new(@cloud_configuration).call(drained) }
58
+ start
59
+ at_exit { stop }
60
+ end
61
+
62
+ # Public: Records telemetry events based on active support notifications.
63
+ def record(name, payload)
64
+ return unless name == Flipper::Feature::InstrumentationName
65
+ return unless payload[:operation] == :enabled?
66
+ detect_forking
67
+
68
+ metric = Metric.new(payload[:feature_name].to_s.freeze, payload[:result])
69
+ @metric_storage.increment metric
70
+ end
71
+
72
+ # Public: Start all the tasks and setup new metric storage.
73
+ def start
74
+ info "action=start"
75
+
76
+ @metric_storage = MetricStorage.new
77
+
78
+ @pool = Concurrent::FixedThreadPool.new(1, {
79
+ max_queue: 20, # ~ 20 minutes of data at 1 minute intervals
80
+ fallback_policy: :discard,
81
+ name: "flipper-telemetry-post-to-cloud-pool".freeze,
82
+ })
83
+
84
+ @timer = Concurrent::TimerTask.execute({
85
+ execution_interval: interval,
86
+ name: "flipper-telemetry-post-to-pool-timer".freeze,
87
+ }) { post_to_pool }
88
+ end
89
+
90
+ # Public: Shuts down all the tasks and tries to flush any remaining info to Cloud.
91
+ def stop
92
+ info "action=stop"
93
+
94
+ if @timer
95
+ debug "action=timer_shutdown_start"
96
+ @timer.shutdown
97
+ # no need to wait long for timer, all it does is drain in memory metric
98
+ # storage and post to the pool of background workers
99
+ timer_termination_result = @timer.wait_for_termination(1)
100
+ @timer.kill unless timer_termination_result
101
+ debug "action=timer_shutdown_end result=#{timer_termination_result}"
102
+ end
103
+
104
+ if @pool
105
+ post_to_pool # one last drain
106
+ debug "action=pool_shutdown_start"
107
+ @pool.shutdown
108
+ pool_termination_result = @pool.wait_for_termination(@shutdown_timeout)
109
+ @pool.kill unless pool_termination_result
110
+ debug "action=pool_shutdown_end result=#{pool_termination_result}"
111
+ end
112
+ end
113
+
114
+ # Public: Restart all the tasks and reset the storage.
115
+ def restart
116
+ stop
117
+ start
118
+ end
119
+
120
+ # Internal: Sets the interval in seconds for how often telemetry should be sent to cloud.
121
+ def interval=(value)
122
+ new_interval = [Typecast.to_float(value), 10].max
123
+ @timer&.execution_interval = new_interval
124
+ @interval = new_interval
125
+ end
126
+
127
+ # Internal: Sets the timeout in seconds for how long to wait for the pool to shutdown.
128
+ def shutdown_timeout=(value)
129
+ new_shutdown_timeout = [Typecast.to_float(value), 0.1].max
130
+ @shutdown_timeout = new_shutdown_timeout
131
+ end
132
+
133
+ private
134
+
135
+ def detect_forking
136
+ if @pid != $$
137
+ info "action=fork_detected pid_was#{@pid} pid_is=#{$$}"
138
+ restart
139
+ @pid = $$
140
+ end
141
+ end
142
+
143
+ # Drains the metric storage and enqueues the metrics to be posted to cloud.
144
+ def post_to_pool
145
+ drained = @metric_storage.drain
146
+ return if drained.empty?
147
+ debug "action=post_to_pool metrics=#{drained.size}"
148
+ @pool.post { post_to_cloud(drained) }
149
+ rescue => error
150
+ error "action=post_to_pool error=#{error.inspect}"
151
+ end
152
+
153
+ # Posts the drained metrics to cloud.
154
+ def post_to_cloud(drained)
155
+ debug "action=post_to_cloud metrics=#{drained.size}"
156
+ response, error = submitter.call(drained)
157
+ debug "action=post_to_cloud response=#{response.inspect} body=#{response&.body.inspect} error=#{error.inspect}"
158
+
159
+ # Some of the errors are response code errors which have a response and
160
+ # thus may have a telemetry-interval header for us to respect.
161
+ response ||= error.response if error && error.respond_to?(:response)
162
+
163
+ if response
164
+ if Flipper::Typecast.to_boolean(response["telemetry-shutdown"])
165
+ debug "action=telemetry_shutdown message=The server has requested that telemetry be shut down."
166
+ stop
167
+ return
168
+ end
169
+
170
+ if interval = response["telemetry-interval"]
171
+ self.interval = interval.to_f
172
+ end
173
+ end
174
+ rescue => error
175
+ error "action=post_to_cloud error=#{error.inspect}"
176
+ end
177
+
178
+ def error(message)
179
+ @cloud_configuration.log message, level: :error
180
+ end
181
+
182
+ def info(message)
183
+ @cloud_configuration.log message, level: :info
184
+ end
185
+
186
+ def debug(message)
187
+ @cloud_configuration.log message
188
+ end
189
+ end
190
+ end
191
+ end
data/lib/flipper/cloud.rb CHANGED
@@ -24,7 +24,7 @@ module Flipper
24
24
  env_key = options.fetch(:env_key, 'flipper')
25
25
  memoizer_options = options.fetch(:memoizer_options, {})
26
26
 
27
- app = ->(_) { [404, { 'content-type'.freeze => 'application/json'.freeze }, ['{}'.freeze]] }
27
+ app = ->(_) { [404, { Rack::CONTENT_TYPE => 'application/json'.freeze }, ['{}'.freeze]] }
28
28
  builder = Rack::Builder.new
29
29
  yield builder if block_given?
30
30
  builder.use Flipper::Middleware::SetupEnv, flipper, env_key: env_key
@@ -1,8 +1,8 @@
1
1
  module Flipper
2
2
  class Configuration
3
3
  def initialize(options = {})
4
- @default = -> { Flipper.new(adapter) }
5
- @adapter = -> { Flipper::Adapters::Memory.new }
4
+ @builder = AdapterBuilder.new { store Flipper::Adapters::Memory }
5
+ @default = -> { Flipper.new(@builder.to_adapter) }
6
6
  end
7
7
 
8
8
  # The default adapter to use.
@@ -24,9 +24,20 @@ module Flipper
24
24
  #
25
25
  def adapter(&block)
26
26
  if block_given?
27
- @adapter = block
27
+ @builder.store(block)
28
28
  else
29
- @adapter.call
29
+ @builder.to_adapter
30
+ end
31
+ end
32
+
33
+ # An adapter to use to augment the primary storage adapter. See `AdapterBuilder#use`
34
+ if RUBY_VERSION >= '3.0'
35
+ def use(klass, *args, **kwargs, &block)
36
+ @builder.use(klass, *args, **kwargs, &block)
37
+ end
38
+ else
39
+ def use(klass, *args, &block)
40
+ @builder.use(klass, *args, &block)
30
41
  end
31
42
  end
32
43
 
@@ -54,5 +65,15 @@ module Flipper
54
65
  @default.call
55
66
  end
56
67
  end
68
+
69
+ def statsd
70
+ require 'flipper/instrumentation/statsd_subscriber'
71
+ Flipper::Instrumentation::StatsdSubscriber.client
72
+ end
73
+
74
+ def statsd=(client)
75
+ require "flipper/instrumentation/statsd"
76
+ Flipper::Instrumentation::StatsdSubscriber.client = client
77
+ end
57
78
  end
58
79
  end
data/lib/flipper/dsl.rb CHANGED
@@ -46,6 +46,25 @@ module Flipper
46
46
  feature(name).enable(*args)
47
47
  end
48
48
 
49
+ # Public: Enable a feature for an expression.
50
+ #
51
+ # name - The String or Symbol name of the feature.
52
+ # expression - a Flipper::Expression instance or a Hash.
53
+ #
54
+ # Returns result of Feature#enable.
55
+ def enable_expression(name, expression)
56
+ feature(name).enable_expression(expression)
57
+ end
58
+
59
+ # Public: Add an expression to a feature.
60
+ #
61
+ # expression - an expression or Hash that can be converted to an expression.
62
+ #
63
+ # Returns result of enable.
64
+ def add_expression(name, expression)
65
+ feature(name).add_expression(expression)
66
+ end
67
+
49
68
  # Public: Enable a feature for an actor.
50
69
  #
51
70
  # name - The String or Symbol name of the feature.
@@ -100,6 +119,24 @@ module Flipper
100
119
  feature(name).disable(*args)
101
120
  end
102
121
 
122
+ # Public: Disable expression for feature.
123
+ #
124
+ # name - The String or Symbol name of the feature.
125
+ #
126
+ # Returns result of Feature#disable.
127
+ def disable_expression(name)
128
+ feature(name).disable_expression
129
+ end
130
+
131
+ # Public: Remove an expression from a feature.
132
+ #
133
+ # expression - an Expression or Hash that can be converted to an expression.
134
+ #
135
+ # Returns result of enable.
136
+ def remove_expression(name, expression)
137
+ feature(name).remove_expression(expression)
138
+ end
139
+
103
140
  # Public: Disable a feature for an actor.
104
141
  #
105
142
  # name - The String or Symbol name of the feature.
@@ -219,6 +256,15 @@ module Flipper
219
256
  Flipper.group(name)
220
257
  end
221
258
 
259
+ # Public: Gets the expression for the feature.
260
+ #
261
+ # name - The String or Symbol name of the feature.
262
+ #
263
+ # Returns an instance of Flipper::Expression.
264
+ def expression(name)
265
+ feature(name).expression
266
+ end
267
+
222
268
  # Public: Returns a Set of the known features for this adapter.
223
269
  #
224
270
  # Returns Set of Flipper::Feature instances.
@@ -226,6 +272,11 @@ module Flipper
226
272
  adapter.features.map { |name| feature(name) }.to_set
227
273
  end
228
274
 
275
+ # Public: Does this adapter support writes or not.
276
+ def read_only?
277
+ adapter.read_only?
278
+ end
279
+
229
280
  # Cloud DSL method that does nothing for open source version.
230
281
  def sync
231
282
  end
@@ -1,5 +1,19 @@
1
1
  module Flipper
2
2
  class Engine < Rails::Engine
3
+ def self.default_strict_value
4
+ value = ENV["FLIPPER_STRICT"]
5
+ if value.in?(["warn", "raise", "noop"])
6
+ value.to_sym
7
+ elsif value
8
+ Typecast.to_boolean(value) ? :raise : false
9
+ elsif Rails.env.production?
10
+ false
11
+ else
12
+ # Warn in development for now. Future versions may default to :raise in development and test
13
+ Rails.env.development? && :warn
14
+ end
15
+ end
16
+
3
17
  paths["config/routes.rb"] = ["lib/flipper/cloud/routes.rb"]
4
18
 
5
19
  config.before_configuration do
@@ -9,17 +23,25 @@ module Flipper
9
23
  preload: ENV.fetch('FLIPPER_PRELOAD', 'true').casecmp('true').zero?,
10
24
  instrumenter: ENV.fetch('FLIPPER_INSTRUMENTER', 'ActiveSupport::Notifications').constantize,
11
25
  log: ENV.fetch('FLIPPER_LOG', 'true').casecmp('true').zero?,
12
- cloud_path: "_flipper"
26
+ cloud_path: "_flipper",
27
+ strict: default_strict_value,
28
+ actor_limit: ENV["FLIPPER_ACTOR_LIMIT"]&.to_i || 100,
29
+ test_help: Flipper::Typecast.to_boolean(ENV["FLIPPER_TEST_HELP"] || Rails.env.test?),
13
30
  )
14
31
  end
15
32
 
16
- initializer "flipper.identifier" do
33
+ initializer "flipper.properties" do
17
34
  ActiveSupport.on_load(:active_record) do
18
- ActiveRecord::Base.include Flipper::Identifier
35
+ require "flipper/model/active_record"
36
+ ActiveRecord::Base.include Flipper::Model::ActiveRecord
19
37
  end
20
38
  end
21
39
 
22
40
  initializer "flipper.default", before: :load_config_initializers do |app|
41
+ # Load cloud secrets from Rails credentials
42
+ ENV["FLIPPER_CLOUD_TOKEN"] ||= app.credentials.dig(:flipper, :cloud_token)
43
+ ENV["FLIPPER_CLOUD_SYNC_SECRET"] ||= app.credentials.dig(:flipper, :cloud_sync_secret)
44
+
23
45
  require 'flipper/cloud' if cloud?
24
46
 
25
47
  Flipper.configure do |config|
@@ -44,6 +66,15 @@ module Flipper
44
66
  end
45
67
  end
46
68
 
69
+ initializer "flipper.adapters", after: :load_config_initializers do |app|
70
+ flipper = app.config.flipper
71
+
72
+ Flipper.configure do |config|
73
+ config.use Flipper::Adapters::Strict, flipper.strict if flipper.strict
74
+ config.use Flipper::Adapters::ActorLimit, flipper.actor_limit if flipper.actor_limit
75
+ end
76
+ end
77
+
47
78
  initializer "flipper.memoizer", after: :load_config_initializers do |app|
48
79
  flipper = app.config.flipper
49
80
 
@@ -56,8 +87,16 @@ module Flipper
56
87
  end
57
88
  end
58
89
 
90
+ initializer "flipper.test" do |app|
91
+ require "flipper/test_help" if app.config.flipper.test_help
92
+ end
93
+
59
94
  def cloud?
60
95
  !!ENV["FLIPPER_CLOUD_TOKEN"]
61
96
  end
97
+
98
+ def self.deprecated_rails_version?
99
+ Gem::Version.new(Rails.version) < Gem::Version.new(Flipper::NEXT_REQUIRED_RAILS_VERSION)
100
+ end
62
101
  end
63
102
  end
@@ -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
@@ -18,7 +18,7 @@ module Flipper
18
18
  # Public: The features hash identical to calling get_all on adapter.
19
19
  def features
20
20
  @features ||= begin
21
- features = JSON.parse(contents).fetch("features")
21
+ features = Typecast.from_json(contents).fetch("features")
22
22
  Typecast.features_hash(features)
23
23
  rescue JSON::ParserError
24
24
  raise JsonError
@@ -20,7 +20,7 @@ module Flipper
20
20
  end
21
21
  end
22
22
 
23
- json = JSON.dump({
23
+ json = Typecast.to_json({
24
24
  version: VERSION,
25
25
  features: features,
26
26
  })
@@ -0,0 +1,73 @@
1
+ module Flipper
2
+ class Expression
3
+ module Builder
4
+ def build(object)
5
+ Expression.build(object)
6
+ end
7
+
8
+ def add(*expressions)
9
+ group? ? build(name => args + expressions.flatten) : any.add(*expressions)
10
+ end
11
+
12
+ def remove(*expressions)
13
+ group? ? build(name => args - expressions.flatten) : any.remove(*expressions)
14
+ end
15
+
16
+ def any
17
+ any? ? self : build({ Any: [self] })
18
+ end
19
+
20
+ def all
21
+ all? ? self : build({ All: [self] })
22
+ end
23
+
24
+ def equal(object)
25
+ build({ Equal: [self, object] })
26
+ end
27
+ alias eq equal
28
+
29
+ def not_equal(object)
30
+ build({ NotEqual: [self, object] })
31
+ end
32
+ alias neq not_equal
33
+
34
+ def greater_than(object)
35
+ build({ GreaterThan: [self, object] })
36
+ end
37
+ alias gt greater_than
38
+
39
+ def greater_than_or_equal_to(object)
40
+ build({ GreaterThanOrEqualTo: [self, object] })
41
+ end
42
+ alias gte greater_than_or_equal_to
43
+ alias greater_than_or_equal greater_than_or_equal_to
44
+
45
+ def less_than(object)
46
+ build({ LessThan: [self, object] })
47
+ end
48
+ alias lt less_than
49
+
50
+ def less_than_or_equal_to(object)
51
+ build({ LessThanOrEqualTo: [self, object] })
52
+ end
53
+ alias lte less_than_or_equal_to
54
+ alias less_than_or_equal less_than_or_equal_to
55
+
56
+ def percentage_of_actors(object)
57
+ build({ PercentageOfActors: [self, build(object)] })
58
+ end
59
+
60
+ def any?
61
+ is_a?(Expression) && function == Expressions::Any
62
+ end
63
+
64
+ def all?
65
+ is_a?(Expression) && function == Expressions::All
66
+ end
67
+
68
+ def group?
69
+ any? || all?
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,25 @@
1
+ module Flipper
2
+ class Expression
3
+ # Public: A constant value like a "string", Number (1, 3.5), Boolean (true, false).
4
+ #
5
+ # Implements the same interface as Expression
6
+ class Constant
7
+ include Expression::Builder
8
+
9
+ attr_reader :value
10
+
11
+ def initialize(value)
12
+ @value = value
13
+ end
14
+
15
+ def evaluate(_ = nil)
16
+ value
17
+ end
18
+
19
+ def eql?(other)
20
+ other.is_a?(self.class) && other.value == value
21
+ end
22
+ alias_method :==, :eql?
23
+ end
24
+ end
25
+ end