flipper 0.24.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (226) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/dependabot.yml +6 -0
  4. data/.github/workflows/ci.yml +45 -14
  5. data/.github/workflows/examples.yml +39 -16
  6. data/Changelog.md +2 -443
  7. data/Gemfile +19 -11
  8. data/README.md +31 -27
  9. data/Rakefile +6 -4
  10. data/benchmark/enabled_ips.rb +10 -0
  11. data/benchmark/enabled_multiple_actors_ips.rb +20 -0
  12. data/benchmark/enabled_profile.rb +20 -0
  13. data/benchmark/instrumentation_ips.rb +21 -0
  14. data/benchmark/typecast_ips.rb +27 -0
  15. data/docs/images/banner.jpg +0 -0
  16. data/docs/images/flipper_cloud.png +0 -0
  17. data/examples/api/basic.ru +3 -4
  18. data/examples/api/custom_memoized.ru +3 -4
  19. data/examples/api/memoized.ru +3 -4
  20. data/examples/cloud/app.ru +12 -0
  21. data/examples/cloud/backoff_policy.rb +13 -0
  22. data/examples/cloud/basic.rb +22 -0
  23. data/examples/cloud/cloud_setup.rb +20 -0
  24. data/examples/cloud/forked.rb +36 -0
  25. data/examples/cloud/import.rb +17 -0
  26. data/examples/cloud/threaded.rb +33 -0
  27. data/examples/dsl.rb +1 -15
  28. data/examples/enabled_for_actor.rb +4 -2
  29. data/examples/expressions.rb +213 -0
  30. data/examples/instrumentation.rb +1 -0
  31. data/examples/instrumentation_last_accessed_at.rb +1 -0
  32. data/examples/mirroring.rb +59 -0
  33. data/examples/strict.rb +18 -0
  34. data/exe/flipper +5 -0
  35. data/flipper-cloud.gemspec +19 -0
  36. data/flipper.gemspec +10 -6
  37. data/lib/flipper/actor.rb +6 -3
  38. data/lib/flipper/adapter.rb +33 -7
  39. data/lib/flipper/adapter_builder.rb +44 -0
  40. data/lib/flipper/adapters/actor_limit.rb +28 -0
  41. data/lib/flipper/adapters/cache_base.rb +143 -0
  42. data/lib/flipper/adapters/dual_write.rb +1 -3
  43. data/lib/flipper/adapters/failover.rb +0 -4
  44. data/lib/flipper/adapters/failsafe.rb +72 -0
  45. data/lib/flipper/adapters/http/client.rb +44 -20
  46. data/lib/flipper/adapters/http/error.rb +1 -1
  47. data/lib/flipper/adapters/http.rb +31 -16
  48. data/lib/flipper/adapters/instrumented.rb +25 -6
  49. data/lib/flipper/adapters/memoizable.rb +33 -21
  50. data/lib/flipper/adapters/memory.rb +81 -46
  51. data/lib/flipper/adapters/operation_logger.rb +17 -78
  52. data/lib/flipper/adapters/poll/poller.rb +2 -0
  53. data/lib/flipper/adapters/poll.rb +37 -0
  54. data/lib/flipper/adapters/pstore.rb +17 -11
  55. data/lib/flipper/adapters/read_only.rb +8 -41
  56. data/lib/flipper/adapters/strict.rb +45 -0
  57. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  58. data/lib/flipper/adapters/sync.rb +0 -4
  59. data/lib/flipper/adapters/wrapper.rb +54 -0
  60. data/lib/flipper/cli.rb +263 -0
  61. data/lib/flipper/cloud/configuration.rb +263 -0
  62. data/lib/flipper/cloud/dsl.rb +27 -0
  63. data/lib/flipper/cloud/message_verifier.rb +95 -0
  64. data/lib/flipper/cloud/middleware.rb +63 -0
  65. data/lib/flipper/cloud/routes.rb +14 -0
  66. data/lib/flipper/cloud/telemetry/backoff_policy.rb +93 -0
  67. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  68. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  69. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  70. data/lib/flipper/cloud/telemetry/submitter.rb +98 -0
  71. data/lib/flipper/cloud/telemetry.rb +191 -0
  72. data/lib/flipper/cloud.rb +53 -0
  73. data/lib/flipper/configuration.rb +25 -4
  74. data/lib/flipper/dsl.rb +46 -45
  75. data/lib/flipper/engine.rb +102 -0
  76. data/lib/flipper/errors.rb +3 -20
  77. data/lib/flipper/export.rb +26 -0
  78. data/lib/flipper/exporter.rb +17 -0
  79. data/lib/flipper/exporters/json/export.rb +32 -0
  80. data/lib/flipper/exporters/json/v1.rb +33 -0
  81. data/lib/flipper/expression/builder.rb +73 -0
  82. data/lib/flipper/expression/constant.rb +25 -0
  83. data/lib/flipper/expression.rb +71 -0
  84. data/lib/flipper/expressions/all.rb +11 -0
  85. data/lib/flipper/expressions/any.rb +9 -0
  86. data/lib/flipper/expressions/boolean.rb +9 -0
  87. data/lib/flipper/expressions/comparable.rb +13 -0
  88. data/lib/flipper/expressions/duration.rb +28 -0
  89. data/lib/flipper/expressions/equal.rb +9 -0
  90. data/lib/flipper/expressions/greater_than.rb +9 -0
  91. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  92. data/lib/flipper/expressions/less_than.rb +9 -0
  93. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  94. data/lib/flipper/expressions/not_equal.rb +9 -0
  95. data/lib/flipper/expressions/now.rb +9 -0
  96. data/lib/flipper/expressions/number.rb +9 -0
  97. data/lib/flipper/expressions/percentage.rb +9 -0
  98. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  99. data/lib/flipper/expressions/property.rb +9 -0
  100. data/lib/flipper/expressions/random.rb +9 -0
  101. data/lib/flipper/expressions/string.rb +9 -0
  102. data/lib/flipper/expressions/time.rb +9 -0
  103. data/lib/flipper/feature.rb +87 -26
  104. data/lib/flipper/feature_check_context.rb +10 -6
  105. data/lib/flipper/gate.rb +13 -11
  106. data/lib/flipper/gate_values.rb +5 -18
  107. data/lib/flipper/gates/actor.rb +10 -17
  108. data/lib/flipper/gates/boolean.rb +1 -1
  109. data/lib/flipper/gates/expression.rb +75 -0
  110. data/lib/flipper/gates/group.rb +5 -7
  111. data/lib/flipper/gates/percentage_of_actors.rb +10 -13
  112. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  113. data/lib/flipper/identifier.rb +2 -2
  114. data/lib/flipper/instrumentation/log_subscriber.rb +34 -6
  115. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  116. data/lib/flipper/instrumentation/subscriber.rb +8 -1
  117. data/lib/flipper/metadata.rb +7 -1
  118. data/lib/flipper/middleware/memoizer.rb +28 -22
  119. data/lib/flipper/model/active_record.rb +23 -0
  120. data/lib/flipper/poller.rb +118 -0
  121. data/lib/flipper/serializers/gzip.rb +22 -0
  122. data/lib/flipper/serializers/json.rb +17 -0
  123. data/lib/flipper/spec/shared_adapter_specs.rb +105 -63
  124. data/lib/flipper/test/shared_adapter_test.rb +101 -58
  125. data/lib/flipper/test_help.rb +43 -0
  126. data/lib/flipper/typecast.rb +59 -18
  127. data/lib/flipper/types/actor.rb +13 -13
  128. data/lib/flipper/types/group.rb +4 -4
  129. data/lib/flipper/types/percentage.rb +1 -1
  130. data/lib/flipper/version.rb +11 -1
  131. data/lib/flipper.rb +50 -11
  132. data/lib/generators/flipper/setup_generator.rb +63 -0
  133. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  134. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  135. data/lib/generators/flipper/update_generator.rb +35 -0
  136. data/package-lock.json +41 -0
  137. data/package.json +10 -0
  138. data/spec/fixtures/environment.rb +1 -0
  139. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  140. data/spec/flipper/adapter_builder_spec.rb +72 -0
  141. data/spec/flipper/adapter_spec.rb +30 -2
  142. data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
  143. data/spec/flipper/adapters/dual_write_spec.rb +2 -2
  144. data/spec/flipper/adapters/failsafe_spec.rb +58 -0
  145. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  146. data/spec/flipper/adapters/http_spec.rb +137 -55
  147. data/spec/flipper/adapters/instrumented_spec.rb +29 -11
  148. data/spec/flipper/adapters/memoizable_spec.rb +51 -31
  149. data/spec/flipper/adapters/memory_spec.rb +14 -3
  150. data/spec/flipper/adapters/operation_logger_spec.rb +31 -12
  151. data/spec/flipper/adapters/read_only_spec.rb +32 -17
  152. data/spec/flipper/adapters/strict_spec.rb +64 -0
  153. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  154. data/spec/flipper/cli_spec.rb +164 -0
  155. data/spec/flipper/cloud/configuration_spec.rb +251 -0
  156. data/spec/flipper/cloud/dsl_spec.rb +82 -0
  157. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  158. data/spec/flipper/cloud/middleware_spec.rb +289 -0
  159. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  160. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  161. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  162. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  163. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  164. data/spec/flipper/cloud_spec.rb +181 -0
  165. data/spec/flipper/configuration_spec.rb +17 -0
  166. data/spec/flipper/dsl_spec.rb +54 -73
  167. data/spec/flipper/engine_spec.rb +373 -0
  168. data/spec/flipper/export_spec.rb +13 -0
  169. data/spec/flipper/exporter_spec.rb +16 -0
  170. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  171. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  172. data/spec/flipper/expression/builder_spec.rb +248 -0
  173. data/spec/flipper/expression_spec.rb +188 -0
  174. data/spec/flipper/expressions/all_spec.rb +15 -0
  175. data/spec/flipper/expressions/any_spec.rb +15 -0
  176. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  177. data/spec/flipper/expressions/duration_spec.rb +43 -0
  178. data/spec/flipper/expressions/equal_spec.rb +24 -0
  179. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  180. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  181. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  182. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  183. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  184. data/spec/flipper/expressions/now_spec.rb +11 -0
  185. data/spec/flipper/expressions/number_spec.rb +21 -0
  186. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  187. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  188. data/spec/flipper/expressions/property_spec.rb +13 -0
  189. data/spec/flipper/expressions/random_spec.rb +9 -0
  190. data/spec/flipper/expressions/string_spec.rb +11 -0
  191. data/spec/flipper/expressions/time_spec.rb +13 -0
  192. data/spec/flipper/feature_check_context_spec.rb +17 -17
  193. data/spec/flipper/feature_spec.rb +436 -33
  194. data/spec/flipper/gate_values_spec.rb +2 -33
  195. data/spec/flipper/gates/boolean_spec.rb +1 -1
  196. data/spec/flipper/gates/expression_spec.rb +108 -0
  197. data/spec/flipper/gates/group_spec.rb +2 -3
  198. data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -5
  199. data/spec/flipper/gates/percentage_of_time_spec.rb +2 -2
  200. data/spec/flipper/identifier_spec.rb +4 -5
  201. data/spec/flipper/instrumentation/log_subscriber_spec.rb +23 -6
  202. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +25 -1
  203. data/spec/flipper/middleware/memoizer_spec.rb +74 -24
  204. data/spec/flipper/model/active_record_spec.rb +61 -0
  205. data/spec/flipper/poller_spec.rb +47 -0
  206. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  207. data/spec/flipper/serializers/json_spec.rb +13 -0
  208. data/spec/flipper/typecast_spec.rb +121 -6
  209. data/spec/flipper/types/actor_spec.rb +63 -46
  210. data/spec/flipper/types/group_spec.rb +2 -2
  211. data/spec/flipper_integration_spec.rb +168 -58
  212. data/spec/flipper_spec.rb +93 -29
  213. data/spec/spec_helper.rb +8 -14
  214. data/spec/support/actor_names.yml +1 -0
  215. data/spec/support/fail_on_output.rb +8 -0
  216. data/spec/support/fake_backoff_policy.rb +15 -0
  217. data/spec/support/skippable.rb +18 -0
  218. data/spec/support/spec_helpers.rb +23 -8
  219. data/test/adapters/actor_limit_test.rb +20 -0
  220. data/test_rails/generators/flipper/setup_generator_test.rb +64 -0
  221. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  222. data/test_rails/helper.rb +19 -2
  223. data/test_rails/system/test_help_test.rb +51 -0
  224. metadata +223 -19
  225. data/lib/flipper/railtie.rb +0 -47
  226. data/spec/flipper/railtie_spec.rb +0 -73
@@ -0,0 +1,98 @@
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(10) { 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.log "action=to_body request_id=#{request_id} error=#{exception.inspect}", level: :error
55
+ end
56
+
57
+ def retry_with_backoff(attempts, &block)
58
+ result, caught_exception = nil
59
+ should_retry = false
60
+ attempts_remaining = attempts - 1
61
+
62
+ begin
63
+ result, should_retry = yield
64
+ return [result, nil] unless should_retry
65
+ rescue => error
66
+ @cloud_configuration.log "action=post_to_cloud attempts_remaining=#{attempts_remaining} error=#{error.inspect}", level: :error
67
+ should_retry = true
68
+ caught_exception = error
69
+ end
70
+
71
+ if should_retry && attempts_remaining > 0
72
+ sleep @backoff_policy.next_interval.to_f / 1000
73
+ retry_with_backoff attempts_remaining, &block
74
+ else
75
+ [result, caught_exception]
76
+ end
77
+ end
78
+
79
+ def submit(body)
80
+ client = @cloud_configuration.http_client
81
+ client.add_header "schema-version", SCHEMA_VERSION
82
+ client.add_header "content-encoding", GZIP_ENCODING
83
+
84
+ response = client.post PATH, body
85
+ code = response.code.to_i
86
+
87
+ # Raise error and retry for retriable status codes.
88
+ # FIXME: what about redirects?
89
+ if code < 200 || code == 429 || code >= 500
90
+ raise Error.new(request_id, response)
91
+ end
92
+
93
+ response
94
+ end
95
+ end
96
+ end
97
+ end
98
+ 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(2, {
79
+ max_queue: 5,
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
@@ -0,0 +1,53 @@
1
+ require "flipper"
2
+ require "flipper/middleware/setup_env"
3
+ require "flipper/middleware/memoizer"
4
+ require "flipper/cloud/configuration"
5
+ require "flipper/cloud/dsl"
6
+ require "flipper/cloud/middleware"
7
+
8
+ module Flipper
9
+ module Cloud
10
+ # Public: Returns a new Flipper instance with an http adapter correctly
11
+ # configured for flipper cloud.
12
+ #
13
+ # token - The String token for the environment from the website.
14
+ # options - The Hash of options. See Flipper::Cloud::Configuration.
15
+ # block - The block that configuration will be yielded to allowing you to
16
+ # customize this cloud instance and its adapter.
17
+ def self.new(options = {})
18
+ configuration = Configuration.new(options)
19
+ yield configuration if block_given?
20
+ DSL.new(configuration)
21
+ end
22
+
23
+ def self.app(flipper = nil, options = {})
24
+ env_key = options.fetch(:env_key, 'flipper')
25
+ memoizer_options = options.fetch(:memoizer_options, {})
26
+
27
+ app = ->(_) { [404, { Rack::CONTENT_TYPE => 'application/json'.freeze }, ['{}'.freeze]] }
28
+ builder = Rack::Builder.new
29
+ yield builder if block_given?
30
+ builder.use Flipper::Middleware::SetupEnv, flipper, env_key: env_key
31
+ builder.use Flipper::Middleware::Memoizer, memoizer_options.merge(env_key: env_key)
32
+ builder.use Flipper::Cloud::Middleware, env_key: env_key
33
+ builder.run app
34
+ klass = self
35
+ app = builder.to_app
36
+ app.define_singleton_method(:inspect) { klass.inspect } # pretty rake routes output
37
+ app
38
+ end
39
+
40
+ # Private: Configure Flipper to use Cloud by default
41
+ def self.set_default
42
+ if ENV["FLIPPER_CLOUD_TOKEN"]
43
+ Flipper.configure do |config|
44
+ config.default do
45
+ self.new(local_adapter: config.adapter)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ Flipper::Cloud.set_default
@@ -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
@@ -10,7 +10,7 @@ module Flipper
10
10
  # Private: What is being used to instrument all the things.
11
11
  attr_reader :instrumenter
12
12
 
13
- def_delegators :@adapter, :memoize=, :memoizing?
13
+ def_delegators :@adapter, :memoize=, :memoizing?, :import, :export
14
14
 
15
15
  # Public: Returns a new instance of the DSL.
16
16
  #
@@ -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.
@@ -210,22 +247,6 @@ module Flipper
210
247
  # Returns an instance of Flipper::Feature.
211
248
  alias_method :[], :feature
212
249
 
213
- # Public: Shortcut for getting a boolean type instance.
214
- #
215
- # value - The true or false value for the boolean.
216
- #
217
- # Returns a Flipper::Types::Boolean instance.
218
- def boolean(value = true)
219
- Types::Boolean.new(value)
220
- end
221
-
222
- # Public: Even shorter shortcut for getting a boolean type instance.
223
- #
224
- # value - The true or false value for the boolean.
225
- #
226
- # Returns a Flipper::Types::Boolean instance.
227
- alias_method :bool, :boolean
228
-
229
250
  # Public: Access a flipper group by name.
230
251
  #
231
252
  # name - The String or Symbol name of the feature.
@@ -235,35 +256,14 @@ module Flipper
235
256
  Flipper.group(name)
236
257
  end
237
258
 
238
- # Public: Wraps an object as a flipper actor.
239
- #
240
- # thing - The object that you would like to wrap.
259
+ # Public: Gets the expression for the feature.
241
260
  #
242
- # Returns an instance of Flipper::Types::Actor.
243
- # Raises ArgumentError if thing does not respond to `flipper_id`.
244
- def actor(thing)
245
- Types::Actor.new(thing)
246
- end
247
-
248
- # Public: Shortcut for getting a percentage of time instance.
249
- #
250
- # number - The percentage of time that should be enabled.
251
- #
252
- # Returns Flipper::Types::PercentageOfTime.
253
- def time(number)
254
- Types::PercentageOfTime.new(number)
255
- end
256
- alias_method :percentage_of_time, :time
257
-
258
- # Public: Shortcut for getting a percentage of actors instance.
259
- #
260
- # number - The percentage of actors that should be enabled.
261
+ # name - The String or Symbol name of the feature.
261
262
  #
262
- # Returns Flipper::Types::PercentageOfActors.
263
- def actors(number)
264
- Types::PercentageOfActors.new(number)
263
+ # Returns an instance of Flipper::Expression.
264
+ def expression(name)
265
+ feature(name).expression
265
266
  end
266
- alias_method :percentage_of_actors, :actors
267
267
 
268
268
  # Public: Returns a Set of the known features for this adapter.
269
269
  #
@@ -272,8 +272,9 @@ module Flipper
272
272
  adapter.features.map { |name| feature(name) }.to_set
273
273
  end
274
274
 
275
- def import(flipper)
276
- adapter.import(flipper.adapter)
275
+ # Public: Does this adapter support writes or not.
276
+ def read_only?
277
+ adapter.read_only?
277
278
  end
278
279
 
279
280
  # Cloud DSL method that does nothing for open source version.
@@ -0,0 +1,102 @@
1
+ module Flipper
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
+
17
+ paths["config/routes.rb"] = ["lib/flipper/cloud/routes.rb"]
18
+
19
+ config.before_configuration do
20
+ config.flipper = ActiveSupport::OrderedOptions.new.update(
21
+ env_key: ENV.fetch('FLIPPER_ENV_KEY', 'flipper'),
22
+ memoize: ENV.fetch('FLIPPER_MEMOIZE', 'true').casecmp('true').zero?,
23
+ preload: ENV.fetch('FLIPPER_PRELOAD', 'true').casecmp('true').zero?,
24
+ instrumenter: ENV.fetch('FLIPPER_INSTRUMENTER', 'ActiveSupport::Notifications').constantize,
25
+ log: ENV.fetch('FLIPPER_LOG', 'true').casecmp('true').zero?,
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?),
30
+ )
31
+ end
32
+
33
+ initializer "flipper.properties" do
34
+ ActiveSupport.on_load(:active_record) do
35
+ require "flipper/model/active_record"
36
+ ActiveRecord::Base.include Flipper::Model::ActiveRecord
37
+ end
38
+ end
39
+
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
+
45
+ require 'flipper/cloud' if cloud?
46
+
47
+ Flipper.configure do |config|
48
+ config.default do
49
+ if cloud?
50
+ Flipper::Cloud.new(
51
+ local_adapter: config.adapter,
52
+ instrumenter: app.config.flipper.instrumenter
53
+ )
54
+ else
55
+ Flipper.new(config.adapter, instrumenter: app.config.flipper.instrumenter)
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ initializer "flipper.log", after: :load_config_initializers do |app|
62
+ flipper = app.config.flipper
63
+
64
+ if flipper.log && flipper.instrumenter == ActiveSupport::Notifications
65
+ require "flipper/instrumentation/log_subscriber"
66
+ end
67
+ end
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
+
78
+ initializer "flipper.memoizer", after: :load_config_initializers do |app|
79
+ flipper = app.config.flipper
80
+
81
+ if flipper.memoize
82
+ app.middleware.use Flipper::Middleware::Memoizer, {
83
+ env_key: flipper.env_key,
84
+ preload: flipper.preload,
85
+ if: flipper.memoize.respond_to?(:call) ? flipper.memoize : nil
86
+ }
87
+ end
88
+ end
89
+
90
+ initializer "flipper.test" do |app|
91
+ require "flipper/test_help" if app.config.flipper.test_help
92
+ end
93
+
94
+ def cloud?
95
+ !!ENV["FLIPPER_CLOUD_TOKEN"]
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
101
+ end
102
+ end
@@ -2,25 +2,16 @@ module Flipper
2
2
  # Top level error that all other errors inherit from.
3
3
  class Error < StandardError; end
4
4
 
5
- # Raised when gate can not be found for a thing.
5
+ # Raised when gate can not be found for an actor.
6
6
  class GateNotFound < Error
7
- def initialize(thing)
8
- super "Could not find gate for #{thing.inspect}"
7
+ def initialize(actor)
8
+ super "Could not find gate for #{actor.inspect}"
9
9
  end
10
10
  end
11
11
 
12
12
  # Raised when attempting to declare a group name that has already been used.
13
13
  class DuplicateGroup < Error; end
14
14
 
15
- # Raised when default instance not configured but there is an attempt to
16
- # use it.
17
- class DefaultNotSet < Flipper::Error
18
- def initialize(message = nil)
19
- warn "Flipper::DefaultNotSet is deprecated and will be removed in 1.0"
20
- super
21
- end
22
- end
23
-
24
15
  # Raised when an invalid value is set to a configuration property
25
16
  class InvalidConfigurationValue < Flipper::Error
26
17
  def initialize(message = nil)
@@ -28,12 +19,4 @@ module Flipper
28
19
  super(message || default)
29
20
  end
30
21
  end
31
-
32
- # Raised when accessing a configuration property that has been deprecated
33
- class ConfigurationDeprecated < Flipper::Error
34
- def initialize(message = nil)
35
- default = "The configuration property has been deprecated"
36
- super(message || default)
37
- end
38
- end
39
22
  end
@@ -0,0 +1,26 @@
1
+ require "flipper/adapters/memory"
2
+
3
+ module Flipper
4
+ class Export
5
+ attr_reader :contents, :format, :version
6
+
7
+ def initialize(contents:, format: :json, version: 1)
8
+ @contents = contents
9
+ @format = format
10
+ @version = version
11
+ end
12
+
13
+ def features
14
+ raise NotImplementedError
15
+ end
16
+
17
+ def adapter
18
+ @adapter ||= Flipper::Adapters::Memory.new(features)
19
+ end
20
+
21
+ def eql?(other)
22
+ self.class.eql?(other.class) && @contents == other.contents && @format == other.format && @version == other.version
23
+ end
24
+ alias_method :==, :eql?
25
+ end
26
+ end