flipper 0.28.3 → 1.4.2

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 (204) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/ci.yml +56 -10
  4. data/.github/workflows/examples.yml +54 -11
  5. data/.github/workflows/release.yml +54 -0
  6. data/.superset/config.json +4 -0
  7. data/CLAUDE.md +93 -0
  8. data/Changelog.md +1 -559
  9. data/Gemfile +17 -8
  10. data/README.md +34 -29
  11. data/Rakefile +2 -2
  12. data/benchmark/typecast_ips.rb +8 -0
  13. data/docs/images/banner.jpg +0 -0
  14. data/docs/images/flipper_cloud.png +0 -0
  15. data/examples/cloud/app.ru +12 -0
  16. data/examples/cloud/backoff_policy.rb +13 -0
  17. data/examples/cloud/basic.rb +22 -0
  18. data/examples/cloud/cloud_setup.rb +20 -0
  19. data/examples/cloud/forked.rb +36 -0
  20. data/examples/cloud/import.rb +17 -0
  21. data/examples/cloud/poll_interval/README.md +111 -0
  22. data/examples/cloud/poll_interval/client.rb +108 -0
  23. data/examples/cloud/poll_interval/server.rb +98 -0
  24. data/examples/cloud/threaded.rb +33 -0
  25. data/examples/dsl.rb +0 -14
  26. data/examples/expressions.rb +237 -0
  27. data/examples/strict.rb +18 -0
  28. data/exe/flipper +5 -0
  29. data/flipper-cloud.gemspec +19 -0
  30. data/flipper.gemspec +8 -4
  31. data/lib/flipper/actor.rb +6 -3
  32. data/lib/flipper/adapter.rb +27 -1
  33. data/lib/flipper/adapter_builder.rb +44 -0
  34. data/lib/flipper/adapters/actor_limit.rb +54 -0
  35. data/lib/flipper/adapters/cache_base.rb +161 -0
  36. data/lib/flipper/adapters/dual_write.rb +7 -5
  37. data/lib/flipper/adapters/failover.rb +9 -7
  38. data/lib/flipper/adapters/failsafe.rb +2 -6
  39. data/lib/flipper/adapters/http/client.rb +53 -14
  40. data/lib/flipper/adapters/http/error.rb +2 -2
  41. data/lib/flipper/adapters/http.rb +56 -16
  42. data/lib/flipper/adapters/instrumented.rb +2 -6
  43. data/lib/flipper/adapters/memoizable.rb +17 -22
  44. data/lib/flipper/adapters/memory.rb +5 -7
  45. data/lib/flipper/adapters/operation_logger.rb +18 -92
  46. data/lib/flipper/adapters/poll.rb +16 -3
  47. data/lib/flipper/adapters/pstore.rb +18 -12
  48. data/lib/flipper/adapters/read_only.rb +8 -41
  49. data/lib/flipper/adapters/strict.rb +75 -0
  50. data/lib/flipper/adapters/sync/feature_synchronizer.rb +14 -1
  51. data/lib/flipper/adapters/sync/synchronizer.rb +13 -5
  52. data/lib/flipper/adapters/sync.rb +7 -7
  53. data/lib/flipper/adapters/wrapper.rb +54 -0
  54. data/lib/flipper/cli.rb +314 -0
  55. data/lib/flipper/cloud/configuration.rb +267 -0
  56. data/lib/flipper/cloud/dsl.rb +27 -0
  57. data/lib/flipper/cloud/message_verifier.rb +95 -0
  58. data/lib/flipper/cloud/middleware.rb +63 -0
  59. data/lib/flipper/cloud/migrate.rb +71 -0
  60. data/lib/flipper/cloud/routes.rb +14 -0
  61. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  62. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  63. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  64. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  65. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  66. data/lib/flipper/cloud/telemetry.rb +191 -0
  67. data/lib/flipper/cloud.rb +54 -0
  68. data/lib/flipper/configuration.rb +25 -4
  69. data/lib/flipper/dsl.rb +48 -43
  70. data/lib/flipper/engine.rb +102 -0
  71. data/lib/flipper/export.rb +0 -2
  72. data/lib/flipper/exporters/json/export.rb +1 -1
  73. data/lib/flipper/exporters/json/v1.rb +1 -1
  74. data/lib/flipper/expression/builder.rb +73 -0
  75. data/lib/flipper/expression/constant.rb +25 -0
  76. data/lib/flipper/expression.rb +71 -0
  77. data/lib/flipper/expressions/all.rb +9 -0
  78. data/lib/flipper/expressions/any.rb +9 -0
  79. data/lib/flipper/expressions/boolean.rb +9 -0
  80. data/lib/flipper/expressions/comparable.rb +13 -0
  81. data/lib/flipper/expressions/equal.rb +9 -0
  82. data/lib/flipper/expressions/feature_enabled.rb +34 -0
  83. data/lib/flipper/expressions/greater_than.rb +9 -0
  84. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  85. data/lib/flipper/expressions/less_than.rb +9 -0
  86. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  87. data/lib/flipper/expressions/not_equal.rb +9 -0
  88. data/lib/flipper/expressions/now.rb +9 -0
  89. data/lib/flipper/expressions/number.rb +9 -0
  90. data/lib/flipper/expressions/percentage.rb +9 -0
  91. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  92. data/lib/flipper/expressions/property.rb +9 -0
  93. data/lib/flipper/expressions/random.rb +9 -0
  94. data/lib/flipper/expressions/string.rb +9 -0
  95. data/lib/flipper/expressions/time.rb +16 -0
  96. data/lib/flipper/feature.rb +63 -1
  97. data/lib/flipper/gate.rb +2 -1
  98. data/lib/flipper/gate_values.rb +5 -2
  99. data/lib/flipper/gates/expression.rb +75 -0
  100. data/lib/flipper/instrumentation/log_subscriber.rb +13 -5
  101. data/lib/flipper/instrumentation/statsd.rb +4 -2
  102. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  103. data/lib/flipper/instrumentation/subscriber.rb +0 -4
  104. data/lib/flipper/metadata.rb +8 -1
  105. data/lib/flipper/middleware/memoizer.rb +30 -14
  106. data/lib/flipper/model/active_record.rb +23 -0
  107. data/lib/flipper/poller.rb +54 -12
  108. data/lib/flipper/serializers/gzip.rb +22 -0
  109. data/lib/flipper/serializers/json.rb +17 -0
  110. data/lib/flipper/spec/shared_adapter_specs.rb +82 -63
  111. data/lib/flipper/test/shared_adapter_test.rb +77 -58
  112. data/lib/flipper/test_help.rb +43 -0
  113. data/lib/flipper/typecast.rb +37 -9
  114. data/lib/flipper/types/percentage.rb +1 -1
  115. data/lib/flipper/version.rb +11 -1
  116. data/lib/flipper.rb +61 -8
  117. data/lib/generators/flipper/setup_generator.rb +68 -0
  118. data/lib/generators/flipper/templates/initializer.rb +45 -0
  119. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  120. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  121. data/lib/generators/flipper/update_generator.rb +35 -0
  122. data/package-lock.json +41 -0
  123. data/package.json +10 -0
  124. data/spec/fixtures/environment.rb +1 -0
  125. data/spec/flipper/adapter_builder_spec.rb +72 -0
  126. data/spec/flipper/adapter_spec.rb +21 -0
  127. data/spec/flipper/adapters/actor_limit_spec.rb +75 -0
  128. data/spec/flipper/adapters/dual_write_spec.rb +15 -2
  129. data/spec/flipper/adapters/failover_spec.rb +12 -0
  130. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  131. data/spec/flipper/adapters/http_spec.rb +375 -74
  132. data/spec/flipper/adapters/instrumented_spec.rb +1 -1
  133. data/spec/flipper/adapters/memoizable_spec.rb +21 -21
  134. data/spec/flipper/adapters/operation_logger_spec.rb +2 -2
  135. data/spec/flipper/adapters/poll_spec.rb +41 -0
  136. data/spec/flipper/adapters/read_only_spec.rb +32 -17
  137. data/spec/flipper/adapters/strict_spec.rb +122 -0
  138. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +39 -0
  139. data/spec/flipper/adapters/sync/synchronizer_spec.rb +87 -0
  140. data/spec/flipper/adapters/sync_spec.rb +13 -0
  141. data/spec/flipper/cli_spec.rb +217 -0
  142. data/spec/flipper/cloud/configuration_spec.rb +251 -0
  143. data/spec/flipper/cloud/dsl_spec.rb +82 -0
  144. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  145. data/spec/flipper/cloud/middleware_spec.rb +289 -0
  146. data/spec/flipper/cloud/migrate_spec.rb +160 -0
  147. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  148. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  149. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  150. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  151. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  152. data/spec/flipper/cloud_spec.rb +186 -0
  153. data/spec/flipper/configuration_spec.rb +17 -0
  154. data/spec/flipper/dsl_spec.rb +34 -73
  155. data/spec/flipper/engine_spec.rb +374 -0
  156. data/spec/flipper/exporters/json/v1_spec.rb +3 -3
  157. data/spec/flipper/expression/builder_spec.rb +248 -0
  158. data/spec/flipper/expression_spec.rb +188 -0
  159. data/spec/flipper/expressions/all_spec.rb +15 -0
  160. data/spec/flipper/expressions/any_spec.rb +15 -0
  161. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  162. data/spec/flipper/expressions/equal_spec.rb +24 -0
  163. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  164. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  165. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  166. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  167. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  168. data/spec/flipper/expressions/now_spec.rb +11 -0
  169. data/spec/flipper/expressions/number_spec.rb +21 -0
  170. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  171. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  172. data/spec/flipper/expressions/property_spec.rb +13 -0
  173. data/spec/flipper/expressions/random_spec.rb +9 -0
  174. data/spec/flipper/expressions/string_spec.rb +11 -0
  175. data/spec/flipper/expressions/time_spec.rb +29 -0
  176. data/spec/flipper/feature_spec.rb +380 -10
  177. data/spec/flipper/gate_values_spec.rb +2 -2
  178. data/spec/flipper/gates/expression_spec.rb +190 -0
  179. data/spec/flipper/identifier_spec.rb +4 -5
  180. data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -2
  181. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +16 -2
  182. data/spec/flipper/middleware/memoizer_spec.rb +107 -7
  183. data/spec/flipper/model/active_record_spec.rb +72 -0
  184. data/spec/flipper/poller_spec.rb +347 -4
  185. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  186. data/spec/flipper/serializers/json_spec.rb +13 -0
  187. data/spec/flipper/typecast_spec.rb +43 -7
  188. data/spec/flipper/types/actor_spec.rb +18 -1
  189. data/spec/flipper_integration_spec.rb +247 -16
  190. data/spec/flipper_spec.rb +92 -29
  191. data/spec/spec_helper.rb +24 -17
  192. data/spec/support/actor_names.yml +1 -0
  193. data/spec/support/fail_on_output.rb +8 -0
  194. data/spec/support/fake_backoff_policy.rb +15 -0
  195. data/spec/support/spec_helpers.rb +34 -8
  196. data/test/adapters/actor_limit_test.rb +20 -0
  197. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  198. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  199. data/test_rails/helper.rb +22 -2
  200. data/test_rails/system/test_help_test.rb +52 -0
  201. metadata +139 -75
  202. data/.tool-versions +0 -1
  203. data/lib/flipper/railtie.rb +0 -47
  204. data/spec/flipper/railtie_spec.rb +0 -109
@@ -0,0 +1,9 @@
1
+ module Flipper
2
+ module Expressions
3
+ class Random
4
+ def self.call(max = 0)
5
+ rand max
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Flipper
2
+ module Expressions
3
+ class String
4
+ def self.call(value)
5
+ value.to_s
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ require "time"
2
+
3
+ module Flipper
4
+ module Expressions
5
+ class Time
6
+ def self.call(value)
7
+ case value
8
+ when Numeric
9
+ ::Time.at(value).utc
10
+ else
11
+ ::Time.parse(value).utc
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -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
@@ -120,6 +127,28 @@ module Flipper
120
127
  end
121
128
  end
122
129
 
130
+ # Public: Enables an expression_to_add for a feature.
131
+ #
132
+ # expression - an Expression or Hash that can be converted to an expression.
133
+ #
134
+ # Returns result of enable.
135
+ def enable_expression(expression)
136
+ enable Expression.build(expression)
137
+ end
138
+
139
+ # Public: Add an expression for a feature.
140
+ #
141
+ # expression_to_add - an expression or Hash that can be converted to an expression.
142
+ #
143
+ # Returns result of enable.
144
+ def add_expression(expression_to_add)
145
+ if (current_expression = expression)
146
+ enable current_expression.add(expression_to_add)
147
+ else
148
+ enable expression_to_add
149
+ end
150
+ end
151
+
123
152
  # Public: Enables a feature for an actor.
124
153
  #
125
154
  # actor - a Flipper::Types::Actor instance or an object that responds
@@ -160,6 +189,27 @@ module Flipper
160
189
  enable Types::PercentageOfActors.wrap(percentage)
161
190
  end
162
191
 
192
+ # Public: Disables an expression for a feature.
193
+ #
194
+ # expression - an expression or Hash that can be converted to an expression.
195
+ #
196
+ # Returns result of disable.
197
+ def disable_expression
198
+ disable Flipper.all # just need an expression to clear
199
+ end
200
+
201
+ # Public: Remove an expression from a feature. Does nothing if no expression is
202
+ # currently enabled.
203
+ #
204
+ # expression - an Expression or Hash that can be converted to an expression.
205
+ #
206
+ # Returns result of enable or nil (if no expression enabled).
207
+ def remove_expression(expression_to_remove)
208
+ if (current_expression = expression)
209
+ enable current_expression.remove(expression_to_remove)
210
+ end
211
+ end
212
+
163
213
  # Public: Disables a feature for an actor.
164
214
  #
165
215
  # actor - a Flipper::Types::Actor instance or an object that responds
@@ -252,6 +302,10 @@ module Flipper
252
302
  Flipper.groups - enabled_groups
253
303
  end
254
304
 
305
+ def expression
306
+ Flipper::Expression.build(expression_value) if expression_value
307
+ end
308
+
255
309
  # Public: Get the adapter value for the groups gate.
256
310
  #
257
311
  # Returns Set of String group names.
@@ -259,6 +313,13 @@ module Flipper
259
313
  gate_values.groups
260
314
  end
261
315
 
316
+ # Public: Get the adapter value for the expression gate.
317
+ #
318
+ # Returns expression.
319
+ def expression_value
320
+ gate_values.expression
321
+ end
322
+
262
323
  # Public: Get the adapter value for the actors gate.
263
324
  #
264
325
  # Returns Set of String flipper_id's.
@@ -347,6 +408,7 @@ module Flipper
347
408
  def gates_hash
348
409
  @gates_hash ||= {
349
410
  boolean: Gates::Boolean.new,
411
+ expression: Gates::Expression.new,
350
412
  actor: Gates::Actor.new,
351
413
  percentage_of_actors: Gates::PercentageOfActors.new,
352
414
  percentage_of_time: Gates::PercentageOfTime.new,
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
 
@@ -60,3 +60,4 @@ require 'flipper/gates/boolean'
60
60
  require 'flipper/gates/group'
61
61
  require 'flipper/gates/percentage_of_actors'
62
62
  require 'flipper/gates/percentage_of_time'
63
+ require 'flipper/gates/expression'
@@ -6,6 +6,7 @@ module Flipper
6
6
  attr_reader :boolean
7
7
  attr_reader :actors
8
8
  attr_reader :groups
9
+ attr_reader :expression
9
10
  attr_reader :percentage_of_actors
10
11
  attr_reader :percentage_of_time
11
12
 
@@ -13,8 +14,9 @@ module Flipper
13
14
  @boolean = Typecast.to_boolean(adapter_values[:boolean])
14
15
  @actors = Typecast.to_set(adapter_values[:actors])
15
16
  @groups = Typecast.to_set(adapter_values[:groups])
16
- @percentage_of_actors = Typecast.to_percentage(adapter_values[:percentage_of_actors])
17
- @percentage_of_time = Typecast.to_percentage(adapter_values[:percentage_of_time])
17
+ @expression = adapter_values[:expression]
18
+ @percentage_of_actors = Typecast.to_number(adapter_values[:percentage_of_actors])
19
+ @percentage_of_time = Typecast.to_number(adapter_values[:percentage_of_time])
18
20
  end
19
21
 
20
22
  def eql?(other)
@@ -22,6 +24,7 @@ module Flipper
22
24
  boolean == other.boolean &&
23
25
  actors == other.actors &&
24
26
  groups == other.groups &&
27
+ expression == other.expression &&
25
28
  percentage_of_actors == other.percentage_of_actors &&
26
29
  percentage_of_time == other.percentage_of_time
27
30
  end
@@ -0,0 +1,75 @@
1
+ require "flipper/expression"
2
+
3
+ module Flipper
4
+ module Gates
5
+ class Expression < Gate
6
+ # Internal: The name of the gate. Used for instrumentation, etc.
7
+ def name
8
+ :expression
9
+ end
10
+
11
+ # Internal: Name converted to value safe for adapter.
12
+ def key
13
+ :expression
14
+ end
15
+
16
+ def data_type
17
+ :json
18
+ end
19
+
20
+ def enabled?(value)
21
+ !value.nil? && !value.empty?
22
+ end
23
+
24
+ # Internal: Checks if the gate is open for a thing.
25
+ #
26
+ # Returns true if gate open for thing, false if not.
27
+ def open?(context)
28
+ data = context.values.expression
29
+ return false if data.nil? || data.empty?
30
+ expression = Flipper::Expression.build(data)
31
+
32
+ if context.actors.nil? || context.actors.empty?
33
+ !!expression.evaluate(feature_name: context.feature_name, properties: DEFAULT_PROPERTIES, actor: nil)
34
+ else
35
+ context.actors.any? do |actor|
36
+ !!expression.evaluate(feature_name: context.feature_name, properties: properties(actor), actor: actor)
37
+ end
38
+ end
39
+ end
40
+
41
+ def protects?(thing)
42
+ thing.is_a?(Flipper::Expression) || thing.is_a?(Hash)
43
+ end
44
+
45
+ def wrap(thing)
46
+ Flipper::Expression.build(thing)
47
+ end
48
+
49
+ private
50
+
51
+ # Internal
52
+ DEFAULT_PROPERTIES = {}.freeze
53
+
54
+ def properties(actor)
55
+ return DEFAULT_PROPERTIES if actor.nil?
56
+
57
+ properties = {}
58
+
59
+ if actor.respond_to?(:flipper_properties)
60
+ properties.update(actor.flipper_properties)
61
+ else
62
+ warn "#{actor.inspect} does not respond to `flipper_properties` but should."
63
+ end
64
+
65
+ properties.transform_keys!(&:to_s)
66
+
67
+ if actor.respond_to?(:flipper_id)
68
+ properties["flipper_id".freeze] = actor.flipper_id
69
+ end
70
+
71
+ properties
72
+ end
73
+ end
74
+ end
75
+ end
@@ -1,4 +1,5 @@
1
1
  require 'securerandom'
2
+ require 'active_support/gem_version'
2
3
  require 'active_support/notifications'
3
4
  require 'active_support/log_subscriber'
4
5
 
@@ -52,11 +53,10 @@ module Flipper
52
53
 
53
54
  feature_name = event.payload[:feature_name]
54
55
  adapter_name = event.payload[:adapter_name]
55
- gate_name = event.payload[:gate_name]
56
56
  operation = event.payload[:operation]
57
57
  result = event.payload[:result]
58
58
 
59
- description = 'Flipper '
59
+ description = String.new('Flipper ')
60
60
  description << "feature(#{feature_name}) " unless feature_name.nil?
61
61
  description << "adapter(#{adapter_name}) "
62
62
  description << "#{operation} "
@@ -71,11 +71,19 @@ module Flipper
71
71
  self.class.logger
72
72
  end
73
73
 
74
+ def self.attach
75
+ attach_to InstrumentationNamespace
76
+ end
77
+
78
+ def self.detach
79
+ # Rails 5.2 doesn't support this, that's fine
80
+ detach_from InstrumentationNamespace if respond_to?(:detach_from)
81
+ end
82
+
74
83
  private
75
84
 
76
85
  # Rails 7.1 changed the signature of this function.
77
- # Checking if > 7.0.99 rather than >= 7.1 so that 7.1 pre-release versions are included.
78
- COLOR_OPTIONS = if Rails.gem_version > Gem::Version.new('7.0.99')
86
+ COLOR_OPTIONS = if Gem::Requirement.new(">=7.1").satisfied_by?(ActiveSupport.gem_version)
79
87
  { bold: true }.freeze
80
88
  else
81
89
  true
@@ -88,5 +96,5 @@ module Flipper
88
96
  end
89
97
  end
90
98
 
91
- Instrumentation::LogSubscriber.attach_to InstrumentationNamespace
99
+ Instrumentation::LogSubscriber.attach
92
100
  end
@@ -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
+ )
@@ -12,13 +12,11 @@ module Flipper
12
12
  end
13
13
 
14
14
  def update_timer(metric)
15
- if self.class.client
16
- self.class.client.timing metric, (@duration * 1_000).round
17
- end
15
+ self.class.client&.timing metric, (@duration * 1_000).round
18
16
  end
19
17
 
20
18
  def update_counter(metric)
21
- self.class.client.increment metric if self.class.client
19
+ self.class.client&.increment metric
22
20
  end
23
21
  end
24
22
  end
@@ -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
@@ -1,5 +1,12 @@
1
+ require_relative './version'
2
+
1
3
  module Flipper
2
4
  METADATA = {
3
- 'changelog_uri' => 'https://github.com/jnunemaker/flipper/blob/main/Changelog.md',
5
+ "documentation_uri" => "https://www.flippercloud.io/docs",
6
+ "homepage_uri" => "https://www.flippercloud.io",
7
+ "source_code_uri" => "https://github.com/flippercloud/flipper",
8
+ "bug_tracker_uri" => "https://github.com/flippercloud/flipper/issues",
9
+ "changelog_uri" => "https://github.com/flippercloud/flipper/releases/tag/v#{Flipper::VERSION}",
10
+ "funding_uri" => "https://github.com/sponsors/flippercloud",
4
11
  }.freeze
5
12
  end
@@ -20,6 +20,14 @@ module Flipper
20
20
  # # using with preload specific features
21
21
  # use Flipper::Middleware::Memoizer, preload: [:stats, :search, :some_feature]
22
22
  #
23
+ # # using with preload block that returns true/false
24
+ # use Flipper::Middleware::Memoizer, preload: ->(request) { !request.path.start_with?('/assets') }
25
+ #
26
+ # # using with preload block that returns specific features
27
+ # use Flipper::Middleware::Memoizer, preload: ->(request) {
28
+ # request.path.starts_with?('/admin') ? [:stats, :search] : false
29
+ # }
30
+ #
23
31
  def initialize(app, opts = {})
24
32
  if opts.is_a?(Flipper::DSL) || opts.is_a?(Proc)
25
33
  raise 'Flipper::Middleware::Memoizer no longer initializes with a flipper instance or block. Read more at: https://git.io/vSo31.'
@@ -34,7 +42,7 @@ module Flipper
34
42
  request = Rack::Request.new(env)
35
43
 
36
44
  if memoize?(request)
37
- memoized_call(env)
45
+ memoized_call(request)
38
46
  else
39
47
  @app.call(env)
40
48
  end
@@ -52,26 +60,34 @@ module Flipper
52
60
  end
53
61
  end
54
62
 
55
- def memoized_call(env)
56
- reset_on_body_close = false
57
- flipper = env.fetch(@env_key) { Flipper }
63
+ def memoized_call(request)
64
+ flipper = request.env.fetch(@env_key) { Flipper }
58
65
 
59
66
  # Already memoizing. This instance does not need to do anything.
60
67
  if flipper.memoizing?
61
- warn "Flipper::Middleware::Memoizer appears to be running twice. Read how to resolve this at https://github.com/jnunemaker/flipper/pull/523"
62
- return @app.call(env)
68
+ warn "Flipper::Middleware::Memoizer appears to be running twice. Read how to resolve this at https://github.com/flippercloud/flipper/pull/523"
69
+ return @app.call(request.env)
63
70
  end
64
71
 
65
- flipper.memoize = true
72
+ begin
73
+ flipper.memoize = true
66
74
 
67
- case @opts[:preload]
68
- when true then flipper.preload_all
69
- when Array then flipper.preload(@opts[:preload])
70
- end
75
+ # Preloading is pointless without memoizing.
76
+ preload = if @opts[:preload].respond_to?(:call)
77
+ @opts[:preload].call(request)
78
+ else
79
+ @opts[:preload]
80
+ end
71
81
 
72
- @app.call(env)
73
- ensure
74
- flipper.memoize = false if flipper
82
+ case preload
83
+ when true then flipper.preload_all
84
+ when Array then flipper.preload(preload)
85
+ end
86
+
87
+ @app.call(request.env)
88
+ ensure
89
+ flipper.memoize = false
90
+ end
75
91
  end
76
92
  end
77
93
  end
@@ -0,0 +1,23 @@
1
+ module Flipper
2
+ module Model
3
+ module ActiveRecord
4
+ # The id of the record when used as an actor.
5
+ #
6
+ # class User < ActiveRecord::Base
7
+ # end
8
+ #
9
+ # user = User.first
10
+ # Flipper.enable :some_feature, user
11
+ # Flipper.enabled? :some_feature, user #=> true
12
+ #
13
+ def flipper_id
14
+ "#{self.class.base_class.name};#{id}"
15
+ end
16
+
17
+ # Properties used to evaluate expressions
18
+ def flipper_properties
19
+ {"type" => self.class.name}.merge(attributes)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -2,6 +2,7 @@ require 'logger'
2
2
  require 'concurrent/utility/monotonic_time'
3
3
  require 'concurrent/map'
4
4
  require 'concurrent/atomic/atomic_fixnum'
5
+ require 'concurrent/atomic/atomic_boolean'
5
6
 
6
7
  module Flipper
7
8
  class Poller
@@ -17,23 +18,26 @@ module Flipper
17
18
  end
18
19
 
19
20
  def self.reset
20
- instances.each {|_,poller| poller.stop }.clear
21
+ instances.each do |_, instance|
22
+ instance.stop
23
+ instance.thread&.join(1)
24
+ end.clear
21
25
  end
22
26
 
27
+ MINIMUM_POLL_INTERVAL = 10
28
+
23
29
  def initialize(options = {})
24
30
  @thread = nil
25
31
  @pid = Process.pid
26
32
  @mutex = Mutex.new
27
33
  @instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
28
34
  @remote_adapter = options.fetch(:remote_adapter)
29
- @interval = options.fetch(:interval, 10).to_f
30
35
  @last_synced_at = Concurrent::AtomicFixnum.new(0)
31
36
  @adapter = Adapters::Memory.new(nil, threadsafe: true)
37
+ @shutdown_requested = Concurrent::AtomicBoolean.new(false)
32
38
 
33
- if @interval < 1
34
- warn "Flipper::Cloud poll interval must be greater than or equal to 1 but was #{@interval}. Setting @interval to 1."
35
- @interval = 1
36
- end
39
+ self.interval = options.fetch(:interval, 10)
40
+ @initial_interval = @interval
37
41
 
38
42
  @start_automatically = options.fetch(:start_automatically, true)
39
43
 
@@ -44,6 +48,7 @@ module Flipper
44
48
 
45
49
  def start
46
50
  reset if forked?
51
+ return if @shutdown_requested.true?
47
52
  ensure_worker_running
48
53
  end
49
54
 
@@ -57,23 +62,38 @@ module Flipper
57
62
  def run
58
63
  loop do
59
64
  sleep jitter
60
- start = Concurrent.monotonic_time
65
+
61
66
  begin
62
67
  sync
63
- rescue => exception
68
+ rescue
64
69
  # you can instrument these using poller.flipper
65
70
  end
66
71
 
67
- sleep_interval = interval - (Concurrent.monotonic_time - start)
68
- sleep sleep_interval if sleep_interval.positive?
72
+ sleep interval
69
73
  end
70
74
  end
71
75
 
72
76
  def sync
73
77
  @instrumenter.instrument("poller.#{InstrumentationNamespace}", operation: :poll) do
74
- @adapter.import @remote_adapter
75
- @last_synced_at.update { |time| Concurrent.monotonic_time }
78
+ begin
79
+ @adapter.import @remote_adapter
80
+ @last_synced_at.update { |time| Concurrent.monotonic_time }
81
+ ensure
82
+ apply_response_headers
83
+ end
84
+ end
85
+ end
86
+
87
+ # Internal: Sets the interval in seconds for how often to poll.
88
+ def interval=(value)
89
+ requested_interval = Flipper::Typecast.to_float(value)
90
+ new_interval = [requested_interval, MINIMUM_POLL_INTERVAL].max
91
+
92
+ if requested_interval < MINIMUM_POLL_INTERVAL
93
+ warn "Flipper::Cloud poll interval must be greater than or equal to #{MINIMUM_POLL_INTERVAL} but was #{requested_interval}. Setting interval to #{MINIMUM_POLL_INTERVAL}."
76
94
  end
95
+
96
+ @interval = new_interval
77
97
  end
78
98
 
79
99
  private
@@ -97,6 +117,7 @@ module Flipper
97
117
  begin
98
118
  return if thread_alive?
99
119
  @thread = Thread.new { run }
120
+ @thread&.report_on_exception = false
100
121
  @instrumenter.instrument("poller.#{InstrumentationNamespace}", {
101
122
  operation: :thread_start,
102
123
  })
@@ -111,7 +132,28 @@ module Flipper
111
132
 
112
133
  def reset
113
134
  @pid = Process.pid
135
+ @shutdown_requested.make_false
114
136
  mutex.unlock if mutex.locked?
115
137
  end
138
+
139
+ def apply_response_headers
140
+ return unless @remote_adapter.respond_to?(:last_get_all_response)
141
+
142
+ if response = @remote_adapter.last_get_all_response
143
+ # shutdown based on response header
144
+ if Flipper::Typecast.to_boolean(response["poll-shutdown"])
145
+ @shutdown_requested.make_true
146
+ @instrumenter.instrument("poller.#{InstrumentationNamespace}", {
147
+ operation: :shutdown_requested,
148
+ })
149
+ stop
150
+ end
151
+
152
+ # update interval based on response header
153
+ if interval = response["poll-interval"]
154
+ self.interval = [Flipper::Typecast.to_float(interval), @initial_interval].max
155
+ end
156
+ end
157
+ end
116
158
  end
117
159
  end
@@ -0,0 +1,22 @@
1
+ require "zlib"
2
+ require "stringio"
3
+
4
+ module Flipper
5
+ module Serializers
6
+ class Gzip
7
+ def self.serialize(source)
8
+ return if source.nil?
9
+ output = StringIO.new
10
+ gz = Zlib::GzipWriter.new(output)
11
+ gz.write(source)
12
+ gz.close
13
+ output.string
14
+ end
15
+
16
+ def self.deserialize(source)
17
+ return if source.nil?
18
+ Zlib::GzipReader.wrap(StringIO.new(source), &:read)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ require "json"
2
+
3
+ module Flipper
4
+ module Serializers
5
+ class Json
6
+ def self.serialize(source)
7
+ return if source.nil?
8
+ JSON.generate(source)
9
+ end
10
+
11
+ def self.deserialize(source)
12
+ return if source.nil?
13
+ JSON.parse(source)
14
+ end
15
+ end
16
+ end
17
+ end