flipper 0.16.0 → 1.4.0

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 (285) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +1 -0
  3. data/.github/FUNDING.yml +1 -0
  4. data/.github/dependabot.yml +6 -0
  5. data/.github/workflows/ci.yml +110 -0
  6. data/.github/workflows/examples.yml +105 -0
  7. data/.github/workflows/release.yml +54 -0
  8. data/.rspec +1 -0
  9. data/CLAUDE.md +87 -0
  10. data/Changelog.md +2 -215
  11. data/Dockerfile +1 -1
  12. data/Gemfile +28 -20
  13. data/README.md +72 -62
  14. data/Rakefile +13 -3
  15. data/benchmark/enabled_ips.rb +10 -0
  16. data/benchmark/enabled_multiple_actors_ips.rb +20 -0
  17. data/benchmark/enabled_profile.rb +20 -0
  18. data/benchmark/instrumentation_ips.rb +21 -0
  19. data/benchmark/typecast_ips.rb +27 -0
  20. data/docker-compose.yml +37 -34
  21. data/docs/DockerCompose.md +0 -1
  22. data/docs/README.md +1 -0
  23. data/docs/images/banner.jpg +0 -0
  24. data/docs/images/flipper_cloud.png +0 -0
  25. data/examples/api/basic.ru +18 -0
  26. data/examples/api/custom_memoized.ru +36 -0
  27. data/examples/api/memoized.ru +42 -0
  28. data/examples/basic.rb +1 -12
  29. data/examples/cloud/app.ru +12 -0
  30. data/examples/cloud/backoff_policy.rb +13 -0
  31. data/examples/cloud/basic.rb +22 -0
  32. data/examples/cloud/cloud_setup.rb +20 -0
  33. data/examples/cloud/forked.rb +36 -0
  34. data/examples/cloud/import.rb +17 -0
  35. data/examples/cloud/poll_interval/README.md +111 -0
  36. data/examples/cloud/poll_interval/client.rb +108 -0
  37. data/examples/cloud/poll_interval/server.rb +98 -0
  38. data/examples/cloud/threaded.rb +33 -0
  39. data/examples/configuring_default.rb +2 -5
  40. data/examples/dsl.rb +10 -35
  41. data/examples/enabled_for_actor.rb +10 -15
  42. data/examples/expressions.rb +237 -0
  43. data/examples/group.rb +3 -6
  44. data/examples/group_dynamic_lookup.rb +5 -19
  45. data/examples/group_with_members.rb +4 -14
  46. data/examples/importing.rb +1 -1
  47. data/examples/individual_actor.rb +2 -5
  48. data/examples/instrumentation.rb +2 -2
  49. data/examples/instrumentation_last_accessed_at.rb +38 -0
  50. data/examples/memoizing.rb +35 -0
  51. data/examples/mirroring.rb +59 -0
  52. data/examples/percentage_of_actors.rb +6 -16
  53. data/examples/percentage_of_actors_enabled_check.rb +7 -10
  54. data/examples/percentage_of_actors_group.rb +5 -18
  55. data/examples/percentage_of_time.rb +3 -6
  56. data/examples/strict.rb +18 -0
  57. data/exe/flipper +5 -0
  58. data/flipper-cloud.gemspec +19 -0
  59. data/flipper.gemspec +10 -7
  60. data/lib/flipper/actor.rb +10 -3
  61. data/lib/flipper/adapter.rb +50 -8
  62. data/lib/flipper/adapter_builder.rb +44 -0
  63. data/lib/flipper/adapters/actor_limit.rb +54 -0
  64. data/lib/flipper/adapters/cache_base.rb +161 -0
  65. data/lib/flipper/adapters/dual_write.rb +63 -0
  66. data/lib/flipper/adapters/failover.rb +85 -0
  67. data/lib/flipper/adapters/failsafe.rb +72 -0
  68. data/lib/flipper/adapters/http/client.rb +64 -7
  69. data/lib/flipper/adapters/http/error.rb +19 -1
  70. data/lib/flipper/adapters/http.rb +97 -43
  71. data/lib/flipper/adapters/instrumented.rb +47 -26
  72. data/lib/flipper/adapters/memoizable.rb +44 -40
  73. data/lib/flipper/adapters/memory.rb +75 -111
  74. data/lib/flipper/adapters/operation_logger.rb +22 -78
  75. data/lib/flipper/adapters/poll/poller.rb +2 -0
  76. data/lib/flipper/adapters/poll.rb +52 -0
  77. data/lib/flipper/adapters/pstore.rb +27 -17
  78. data/lib/flipper/adapters/read_only.rb +8 -41
  79. data/lib/flipper/adapters/strict.rb +45 -0
  80. data/lib/flipper/adapters/sync/feature_synchronizer.rb +14 -1
  81. data/lib/flipper/adapters/sync/interval_synchronizer.rb +2 -7
  82. data/lib/flipper/adapters/sync/synchronizer.rb +13 -6
  83. data/lib/flipper/adapters/sync.rb +23 -29
  84. data/lib/flipper/adapters/wrapper.rb +54 -0
  85. data/lib/flipper/cli.rb +314 -0
  86. data/lib/flipper/cloud/configuration.rb +271 -0
  87. data/lib/flipper/cloud/dsl.rb +27 -0
  88. data/lib/flipper/cloud/message_verifier.rb +95 -0
  89. data/lib/flipper/cloud/middleware.rb +63 -0
  90. data/lib/flipper/cloud/migrate.rb +71 -0
  91. data/lib/flipper/cloud/routes.rb +14 -0
  92. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  93. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  94. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  95. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  96. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  97. data/lib/flipper/cloud/telemetry.rb +191 -0
  98. data/lib/flipper/cloud.rb +54 -0
  99. data/lib/flipper/configuration.rb +54 -7
  100. data/lib/flipper/dsl.rb +58 -47
  101. data/lib/flipper/engine.rb +102 -0
  102. data/lib/flipper/errors.rb +3 -21
  103. data/lib/flipper/export.rb +24 -0
  104. data/lib/flipper/exporter.rb +17 -0
  105. data/lib/flipper/exporters/json/export.rb +32 -0
  106. data/lib/flipper/exporters/json/v1.rb +33 -0
  107. data/lib/flipper/expression/builder.rb +73 -0
  108. data/lib/flipper/expression/constant.rb +25 -0
  109. data/lib/flipper/expression.rb +71 -0
  110. data/lib/flipper/expressions/all.rb +9 -0
  111. data/lib/flipper/expressions/any.rb +9 -0
  112. data/lib/flipper/expressions/boolean.rb +9 -0
  113. data/lib/flipper/expressions/comparable.rb +13 -0
  114. data/lib/flipper/expressions/equal.rb +9 -0
  115. data/lib/flipper/expressions/feature_enabled.rb +34 -0
  116. data/lib/flipper/expressions/greater_than.rb +9 -0
  117. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  118. data/lib/flipper/expressions/less_than.rb +9 -0
  119. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  120. data/lib/flipper/expressions/not_equal.rb +9 -0
  121. data/lib/flipper/expressions/now.rb +9 -0
  122. data/lib/flipper/expressions/number.rb +9 -0
  123. data/lib/flipper/expressions/percentage.rb +9 -0
  124. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  125. data/lib/flipper/expressions/property.rb +9 -0
  126. data/lib/flipper/expressions/random.rb +9 -0
  127. data/lib/flipper/expressions/string.rb +9 -0
  128. data/lib/flipper/expressions/time.rb +16 -0
  129. data/lib/flipper/feature.rb +95 -28
  130. data/lib/flipper/feature_check_context.rb +10 -6
  131. data/lib/flipper/gate.rb +13 -11
  132. data/lib/flipper/gate_values.rb +5 -18
  133. data/lib/flipper/gates/actor.rb +10 -17
  134. data/lib/flipper/gates/boolean.rb +1 -1
  135. data/lib/flipper/gates/expression.rb +75 -0
  136. data/lib/flipper/gates/group.rb +5 -7
  137. data/lib/flipper/gates/percentage_of_actors.rb +10 -13
  138. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  139. data/lib/flipper/identifier.rb +17 -0
  140. data/lib/flipper/instrumentation/log_subscriber.rb +35 -8
  141. data/lib/flipper/instrumentation/statsd.rb +4 -2
  142. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  143. data/lib/flipper/instrumentation/subscriber.rb +8 -5
  144. data/lib/flipper/instrumenters/memory.rb +6 -2
  145. data/lib/flipper/metadata.rb +8 -1
  146. data/lib/flipper/middleware/memoizer.rb +46 -27
  147. data/lib/flipper/middleware/setup_env.rb +13 -3
  148. data/lib/flipper/model/active_record.rb +23 -0
  149. data/lib/flipper/poller.rb +157 -0
  150. data/lib/flipper/serializers/gzip.rb +22 -0
  151. data/lib/flipper/serializers/json.rb +17 -0
  152. data/lib/flipper/spec/shared_adapter_specs.rb +122 -56
  153. data/lib/flipper/test/shared_adapter_test.rb +120 -52
  154. data/lib/flipper/test_help.rb +43 -0
  155. data/lib/flipper/typecast.rb +59 -18
  156. data/lib/flipper/types/actor.rb +19 -13
  157. data/lib/flipper/types/group.rb +12 -5
  158. data/lib/flipper/types/percentage.rb +1 -1
  159. data/lib/flipper/version.rb +11 -1
  160. data/lib/flipper.rb +71 -12
  161. data/lib/generators/flipper/setup_generator.rb +68 -0
  162. data/lib/generators/flipper/templates/initializer.rb +45 -0
  163. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  164. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  165. data/lib/generators/flipper/update_generator.rb +35 -0
  166. data/package-lock.json +41 -0
  167. data/package.json +10 -0
  168. data/spec/fixtures/environment.rb +1 -0
  169. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  170. data/spec/flipper/actor_spec.rb +10 -2
  171. data/spec/flipper/adapter_builder_spec.rb +72 -0
  172. data/spec/flipper/adapter_spec.rb +52 -6
  173. data/spec/flipper/adapters/actor_limit_spec.rb +75 -0
  174. data/spec/flipper/adapters/dual_write_spec.rb +82 -0
  175. data/spec/flipper/adapters/failover_spec.rb +141 -0
  176. data/spec/flipper/adapters/failsafe_spec.rb +58 -0
  177. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  178. data/spec/flipper/adapters/http_spec.rb +402 -65
  179. data/spec/flipper/adapters/instrumented_spec.rb +31 -13
  180. data/spec/flipper/adapters/memoizable_spec.rb +51 -33
  181. data/spec/flipper/adapters/memory_spec.rb +33 -5
  182. data/spec/flipper/adapters/operation_logger_spec.rb +38 -12
  183. data/spec/flipper/adapters/poll_spec.rb +41 -0
  184. data/spec/flipper/adapters/pstore_spec.rb +0 -2
  185. data/spec/flipper/adapters/read_only_spec.rb +32 -18
  186. data/spec/flipper/adapters/strict_spec.rb +64 -0
  187. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +39 -1
  188. data/spec/flipper/adapters/sync/interval_synchronizer_spec.rb +4 -5
  189. data/spec/flipper/adapters/sync/synchronizer_spec.rb +87 -1
  190. data/spec/flipper/adapters/sync_spec.rb +17 -6
  191. data/spec/flipper/cli_spec.rb +217 -0
  192. data/spec/flipper/cloud/configuration_spec.rb +257 -0
  193. data/spec/flipper/cloud/dsl_spec.rb +90 -0
  194. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  195. data/spec/flipper/cloud/middleware_spec.rb +307 -0
  196. data/spec/flipper/cloud/migrate_spec.rb +160 -0
  197. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  198. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  199. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  200. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  201. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  202. data/spec/flipper/cloud_spec.rb +186 -0
  203. data/spec/flipper/configuration_spec.rb +37 -3
  204. data/spec/flipper/dsl_spec.rb +67 -80
  205. data/spec/flipper/engine_spec.rb +374 -0
  206. data/spec/flipper/export_spec.rb +13 -0
  207. data/spec/flipper/exporter_spec.rb +16 -0
  208. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  209. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  210. data/spec/flipper/expression/builder_spec.rb +248 -0
  211. data/spec/flipper/expression_spec.rb +188 -0
  212. data/spec/flipper/expressions/all_spec.rb +15 -0
  213. data/spec/flipper/expressions/any_spec.rb +15 -0
  214. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  215. data/spec/flipper/expressions/equal_spec.rb +24 -0
  216. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  217. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  218. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  219. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  220. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  221. data/spec/flipper/expressions/now_spec.rb +11 -0
  222. data/spec/flipper/expressions/number_spec.rb +21 -0
  223. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  224. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  225. data/spec/flipper/expressions/property_spec.rb +13 -0
  226. data/spec/flipper/expressions/random_spec.rb +9 -0
  227. data/spec/flipper/expressions/string_spec.rb +11 -0
  228. data/spec/flipper/expressions/time_spec.rb +29 -0
  229. data/spec/flipper/feature_check_context_spec.rb +18 -20
  230. data/spec/flipper/feature_spec.rb +461 -48
  231. data/spec/flipper/gate_spec.rb +0 -2
  232. data/spec/flipper/gate_values_spec.rb +2 -34
  233. data/spec/flipper/gates/actor_spec.rb +0 -2
  234. data/spec/flipper/gates/boolean_spec.rb +1 -3
  235. data/spec/flipper/gates/expression_spec.rb +190 -0
  236. data/spec/flipper/gates/group_spec.rb +2 -5
  237. data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -7
  238. data/spec/flipper/gates/percentage_of_time_spec.rb +2 -4
  239. data/spec/flipper/identifier_spec.rb +12 -0
  240. data/spec/flipper/instrumentation/log_subscriber_spec.rb +24 -7
  241. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +26 -3
  242. data/spec/flipper/instrumenters/memory_spec.rb +18 -1
  243. data/spec/flipper/instrumenters/noop_spec.rb +14 -8
  244. data/spec/flipper/middleware/memoizer_spec.rb +199 -62
  245. data/spec/flipper/middleware/setup_env_spec.rb +23 -5
  246. data/spec/flipper/model/active_record_spec.rb +72 -0
  247. data/spec/flipper/poller_spec.rb +390 -0
  248. data/spec/flipper/registry_spec.rb +0 -1
  249. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  250. data/spec/flipper/serializers/json_spec.rb +13 -0
  251. data/spec/flipper/typecast_spec.rb +121 -7
  252. data/spec/flipper/types/actor_spec.rb +63 -47
  253. data/spec/flipper/types/boolean_spec.rb +0 -1
  254. data/spec/flipper/types/group_spec.rb +24 -3
  255. data/spec/flipper/types/percentage_of_actors_spec.rb +0 -1
  256. data/spec/flipper/types/percentage_of_time_spec.rb +0 -1
  257. data/spec/flipper/types/percentage_spec.rb +0 -1
  258. data/spec/{integration_spec.rb → flipper_integration_spec.rb} +301 -59
  259. data/spec/flipper_spec.rb +123 -29
  260. data/spec/{helper.rb → spec_helper.rb} +23 -21
  261. data/spec/support/actor_names.yml +1 -0
  262. data/spec/support/descriptions.yml +1 -0
  263. data/spec/support/fail_on_output.rb +8 -0
  264. data/spec/support/fake_backoff_policy.rb +15 -0
  265. data/spec/support/skippable.rb +18 -0
  266. data/spec/support/spec_helpers.rb +53 -6
  267. data/test/adapters/actor_limit_test.rb +20 -0
  268. data/test/test_helper.rb +2 -1
  269. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  270. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  271. data/test_rails/helper.rb +31 -0
  272. data/test_rails/system/test_help_test.rb +52 -0
  273. metadata +200 -82
  274. data/.rubocop.yml +0 -54
  275. data/.rubocop_todo.yml +0 -199
  276. data/docs/Adapters.md +0 -124
  277. data/docs/Caveats.md +0 -4
  278. data/docs/Gates.md +0 -167
  279. data/docs/Instrumentation.md +0 -27
  280. data/docs/Optimization.md +0 -114
  281. data/docs/api/README.md +0 -849
  282. data/docs/http/README.md +0 -35
  283. data/docs/read-only/README.md +0 -21
  284. data/examples/example_setup.rb +0 -8
  285. data/test/helper.rb +0 -11
@@ -1,120 +1,455 @@
1
- require 'helper'
2
1
  require 'flipper/adapters/http'
3
2
  require 'flipper/adapters/pstore'
4
- require 'flipper/spec/shared_adapter_specs'
5
- require 'rack/handler/webrick'
3
+
4
+ rack_handler = begin
5
+ # Rack 3+
6
+ require 'rackup/handler/webrick'
7
+ Rackup::Handler::WEBrick
8
+ rescue LoadError
9
+ require 'rack/handler/webrick'
10
+ Rack::Handler::WEBrick
11
+ end
12
+
6
13
 
7
14
  FLIPPER_SPEC_API_PORT = ENV.fetch('FLIPPER_SPEC_API_PORT', 9001).to_i
8
15
 
9
16
  RSpec.describe Flipper::Adapters::Http do
10
- context 'adapter' do
11
- subject do
12
- described_class.new(url: "http://localhost:#{FLIPPER_SPEC_API_PORT}")
13
- end
14
-
15
- before :all do
16
- dir = FlipperRoot.join('tmp').tap(&:mkpath)
17
- log_path = dir.join('flipper_adapters_http_spec.log')
18
- @pstore_file = dir.join('flipper.pstore')
19
- @pstore_file.unlink if @pstore_file.exist?
20
-
21
- api_adapter = Flipper::Adapters::PStore.new(@pstore_file)
22
- flipper_api = Flipper.new(api_adapter)
23
- app = Flipper::Api.app(flipper_api)
24
- server_options = {
25
- Port: FLIPPER_SPEC_API_PORT,
26
- StartCallback: -> { @started = true },
27
- Logger: WEBrick::Log.new(log_path.to_s, WEBrick::Log::INFO),
28
- AccessLog: [
29
- [log_path.open('w'), WEBrick::AccessLog::COMBINED_LOG_FORMAT],
30
- ],
31
- }
32
- @server = WEBrick::HTTPServer.new(server_options)
33
- @server.mount '/', Rack::Handler::WEBrick, app
17
+ default_options = {
18
+ url: "http://localhost:#{FLIPPER_SPEC_API_PORT}",
19
+ }
34
20
 
35
- Thread.new { @server.start }
36
- Timeout.timeout(1) { :wait until @started }
37
- end
21
+ {
22
+ basic: default_options.dup,
23
+ gzip: default_options.dup.merge(headers: { 'accept-encoding': 'gzip' }),
24
+ }.each do |name, options|
25
+ context "adapter (#{name} #{options.inspect})" do
26
+ subject do
27
+ described_class.new(options)
28
+ end
38
29
 
39
- after :all do
40
- @server.shutdown if @server
41
- end
30
+ before :all do
31
+ @started = false
32
+ dir = FlipperRoot.join('tmp').tap(&:mkpath)
33
+ log_path = dir.join('flipper_adapters_http_spec.log')
34
+ @pstore_file = dir.join('flipper.pstore')
35
+ @pstore_file.unlink if @pstore_file.exist?
42
36
 
43
- before(:each) do
44
- @pstore_file.unlink if @pstore_file.exist?
45
- end
37
+ api_adapter = Flipper::Adapters::PStore.new(@pstore_file)
38
+ flipper_api = Flipper.new(api_adapter)
39
+ app = Flipper::Api.app(flipper_api)
40
+ server_options = {
41
+ Port: FLIPPER_SPEC_API_PORT,
42
+ StartCallback: -> { @started = true },
43
+ Logger: WEBrick::Log.new(log_path.to_s, WEBrick::Log::INFO),
44
+ AccessLog: [
45
+ [log_path.open('w'), WEBrick::AccessLog::COMBINED_LOG_FORMAT],
46
+ ],
47
+ }
48
+ @server = WEBrick::HTTPServer.new(server_options)
49
+ @server.mount '/', rack_handler, app
50
+
51
+ Thread.new { @server.start }
52
+ Timeout.timeout(1) { :wait until @started }
53
+ end
46
54
 
47
- it_should_behave_like 'a flipper adapter'
55
+ after :all do
56
+ @server.shutdown if @server
57
+ end
48
58
 
49
- it "can enable and disable unregistered group" do
50
- flipper = Flipper.new(subject)
51
- expect(flipper[:search].enable_group(:some_made_up_group)).to be(true)
52
- expect(flipper[:search].groups_value).to eq(Set["some_made_up_group"])
59
+ before(:each) do
60
+ @pstore_file.unlink if @pstore_file.exist?
61
+ end
53
62
 
54
- expect(flipper[:search].disable_group(:some_made_up_group)).to be(true)
55
- expect(flipper[:search].groups_value).to eq(Set.new)
63
+ it_should_behave_like 'a flipper adapter'
64
+
65
+ it "can enable and disable unregistered group" do
66
+ flipper = Flipper.new(subject)
67
+ expect(flipper[:search].enable_group(:some_made_up_group)).to be(true)
68
+ expect(flipper[:search].groups_value).to eq(Set["some_made_up_group"])
69
+
70
+ expect(flipper[:search].disable_group(:some_made_up_group)).to be(true)
71
+ expect(flipper[:search].groups_value).to eq(Set.new)
72
+ end
73
+
74
+ it "can import" do
75
+ adapter = Flipper::Adapters::Memory.new
76
+ source_flipper = Flipper.new(adapter)
77
+ source_flipper.enable_percentage_of_actors :search, 10
78
+ source_flipper.enable_percentage_of_time :search, 15
79
+ source_flipper.enable_actor :search, Flipper::Actor.new('User;1')
80
+ source_flipper.enable_actor :search, Flipper::Actor.new('User;100')
81
+ source_flipper.enable_group :search, :admins
82
+ source_flipper.enable_group :search, :employees
83
+ source_flipper.enable :plausible
84
+ source_flipper.disable :google_analytics
85
+
86
+ flipper = Flipper.new(subject)
87
+ flipper.import(source_flipper)
88
+ expect(flipper[:search].percentage_of_actors_value).to be(10)
89
+ expect(flipper[:search].percentage_of_time_value).to be(15)
90
+ expect(flipper[:search].actors_value).to eq(Set["User;1", "User;100"])
91
+ expect(flipper[:search].groups_value).to eq(Set["admins", "employees"])
92
+ expect(flipper[:plausible].boolean_value).to be(true)
93
+ expect(flipper[:google_analytics].boolean_value).to be(false)
94
+ end
56
95
  end
57
96
  end
58
97
 
59
98
  it "sends default headers" do
60
99
  headers = {
61
- 'Accept' => 'application/json',
62
- 'Content-Type' => 'application/json',
63
- 'User-Agent' => "Flipper HTTP Adapter v#{Flipper::VERSION}",
100
+ 'accept' => 'application/json',
101
+ 'content-type' => 'application/json',
102
+ 'user-agent' => "Flipper HTTP Adapter v#{Flipper::VERSION}",
103
+ }
104
+ stub_request(:get, "http://app.com/flipper/features/feature_panel")
105
+ .with(headers: headers)
106
+ .to_return(status: 404)
107
+
108
+ adapter = described_class.new(url: 'http://app.com/flipper')
109
+ adapter.get(flipper[:feature_panel])
110
+ end
111
+
112
+ it "sends framework versions" do
113
+ stub_const("Rails", double(version: "7.1.0"))
114
+ stub_const("Sinatra::VERSION", "3.1.0")
115
+ stub_const("Hanami::VERSION", "0.7.2")
116
+ stub_const("GoodJob::VERSION", "3.21.5")
117
+ stub_const("Sidekiq::VERSION", "7.2.0")
118
+
119
+ headers = {
120
+ "client-framework" => [
121
+ "rails=7.1.0",
122
+ "sinatra=3.1.0",
123
+ "hanami=0.7.2",
124
+ "good_job=3.21.5",
125
+ "sidekiq=7.2.0",
126
+ ]
64
127
  }
128
+
65
129
  stub_request(:get, "http://app.com/flipper/features/feature_panel")
66
130
  .with(headers: headers)
67
- .to_return(status: 404, body: "", headers: {})
131
+ .to_return(status: 404)
68
132
 
69
133
  adapter = described_class.new(url: 'http://app.com/flipper')
70
134
  adapter.get(flipper[:feature_panel])
71
135
  end
72
136
 
137
+ it "does not send undefined framework versions" do
138
+ stub_const("Rails", double(version: "7.1.0"))
139
+ stub_const("Sinatra::VERSION", "3.1.0")
140
+
141
+ headers = {
142
+ "client-framework" => ["rails=7.1.0", "sinatra=3.1.0"]
143
+ }
144
+
145
+ stub_request(:get, "http://app.com/flipper/features/feature_panel")
146
+ .with(headers: headers)
147
+ .to_return(status: 404)
148
+
149
+ adapter = described_class.new(url: 'http://app.com/flipper')
150
+ adapter.get(flipper[:feature_panel])
151
+ end
152
+
153
+
73
154
  describe "#get" do
74
155
  it "raises error when not successful response" do
75
156
  stub_request(:get, "http://app.com/flipper/features/feature_panel")
76
- .to_return(status: 503, body: "", headers: {})
157
+ .to_return(status: 503)
77
158
 
78
159
  adapter = described_class.new(url: 'http://app.com/flipper')
79
- expect do
160
+ expect {
80
161
  adapter.get(flipper[:feature_panel])
81
- end.to raise_error(Flipper::Adapters::Http::Error)
162
+ }.to raise_error(Flipper::Adapters::Http::Error)
82
163
  end
83
164
  end
84
165
 
85
166
  describe "#get_multi" do
86
167
  it "raises error when not successful response" do
87
- stub_request(:get, "http://app.com/flipper/features?keys=feature_panel")
88
- .to_return(status: 503, body: "", headers: {})
168
+ stub_request(:get, "http://app.com/flipper/features?keys=feature_panel&exclude_gate_names=true")
169
+ .to_return(status: 503)
89
170
 
90
171
  adapter = described_class.new(url: 'http://app.com/flipper')
91
- expect do
172
+ expect {
92
173
  adapter.get_multi([flipper[:feature_panel]])
93
- end.to raise_error(Flipper::Adapters::Http::Error)
174
+ }.to raise_error(Flipper::Adapters::Http::Error)
94
175
  end
95
176
  end
96
177
 
97
178
  describe "#get_all" do
98
179
  it "raises error when not successful response" do
99
- stub_request(:get, "http://app.com/flipper/features")
100
- .to_return(status: 503, body: "", headers: {})
180
+ stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
181
+ .to_return(status: 503)
101
182
 
102
183
  adapter = described_class.new(url: 'http://app.com/flipper')
103
- expect do
184
+ expect {
104
185
  adapter.get_all
105
- end.to raise_error(Flipper::Adapters::Http::Error)
186
+ }.to raise_error(Flipper::Adapters::Http::Error)
187
+ end
188
+
189
+ it "stores ETag and sends If-None-Match on subsequent requests" do
190
+ features_response = {
191
+ "features" => [
192
+ {
193
+ "key" => "search",
194
+ "gates" => [
195
+ {"key" => "boolean", "value" => true}
196
+ ]
197
+ }
198
+ ]
199
+ }
200
+
201
+ # First request - server returns ETag
202
+ stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
203
+ .to_return(
204
+ status: 200,
205
+ body: JSON.generate(features_response),
206
+ headers: { 'ETag' => '"abc123"' }
207
+ )
208
+
209
+ adapter = described_class.new(url: 'http://app.com/flipper')
210
+ result = adapter.get_all
211
+
212
+ expect(result).to have_key("search")
213
+
214
+ # Second request - should send If-None-Match header
215
+ stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
216
+ .with(headers: { 'If-None-Match' => '"abc123"' })
217
+ .to_return(
218
+ status: 200,
219
+ body: JSON.generate(features_response),
220
+ headers: { 'ETag' => '"abc123"' }
221
+ )
222
+
223
+ etag_result = adapter.get_all
224
+
225
+ expect(etag_result).to eq(result)
226
+ expect(etag_result).to have_key("search")
227
+ expect(
228
+ a_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
229
+ .with(headers: { 'If-None-Match' => '"abc123"' })
230
+ ).to have_been_made.once
231
+ end
232
+
233
+ it "returns cached result when server returns 304 Not Modified" do
234
+ features_response = {
235
+ "features" => [
236
+ {
237
+ "key" => "search",
238
+ "gates" => [
239
+ {"key" => "boolean", "value" => true}
240
+ ]
241
+ }
242
+ ]
243
+ }
244
+
245
+ # First request - server returns ETag
246
+ stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
247
+ .to_return(
248
+ status: 200,
249
+ body: JSON.generate(features_response),
250
+ headers: { 'ETag' => '"abc123"' }
251
+ )
252
+
253
+ adapter = described_class.new(url: 'http://app.com/flipper')
254
+ first_result = adapter.get_all
255
+
256
+ expect(first_result).to have_key("search")
257
+
258
+ # Second request - server returns 304
259
+ stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
260
+ .with(headers: { 'If-None-Match' => '"abc123"' })
261
+ .to_return(status: 304, headers: { 'ETag' => '"abc123"' })
262
+
263
+ second_result = adapter.get_all
264
+
265
+ # Should return the cached result
266
+ expect(second_result).to eq(first_result)
267
+ expect(second_result).to have_key("search")
268
+ end
269
+
270
+ it "raises error when 304 received without cached result" do
271
+ # Server returns 304 without any prior request
272
+ stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
273
+ .to_return(status: 304)
274
+
275
+ adapter = described_class.new(url: 'http://app.com/flipper')
276
+ expect {
277
+ adapter.get_all
278
+ }.to raise_error(Flipper::Adapters::Http::Error)
279
+ end
280
+
281
+ it "does not send If-None-Match for other endpoints" do
282
+ stub_request(:get, "http://app.com/flipper/features/search")
283
+ .to_return(status: 404)
284
+
285
+ adapter = described_class.new(url: 'http://app.com/flipper')
286
+ adapter.get(flipper[:search])
287
+
288
+ # Verify no If-None-Match header was sent
289
+ expect(
290
+ a_request(:get, "http://app.com/flipper/features/search")
291
+ .with { |req| req.headers['If-None-Match'].nil? }
292
+ ).to have_been_made.once
293
+ end
294
+
295
+ it "stores last get_all response for poll-shutdown header checking" do
296
+ features_response = {
297
+ "features" => [
298
+ {
299
+ "key" => "search",
300
+ "gates" => [
301
+ {"key" => "boolean", "value" => true}
302
+ ]
303
+ }
304
+ ]
305
+ }
306
+
307
+ stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
308
+ .to_return(
309
+ status: 200,
310
+ body: JSON.generate(features_response),
311
+ headers: { 'poll-shutdown' => 'true' }
312
+ )
313
+
314
+ adapter = described_class.new(url: 'http://app.com/flipper')
315
+ adapter.get_all
316
+
317
+ expect(adapter.last_get_all_response).not_to be_nil
318
+ expect(adapter.last_get_all_response['poll-shutdown']).to eq('true')
319
+ end
320
+
321
+ it "stores last get_all response even for error responses" do
322
+ stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
323
+ .to_return(
324
+ status: 404,
325
+ body: JSON.generate({ error: "Not found" }),
326
+ headers: { 'poll-shutdown' => 'true' }
327
+ )
328
+
329
+ adapter = described_class.new(url: 'http://app.com/flipper')
330
+
331
+ expect {
332
+ adapter.get_all
333
+ }.to raise_error(Flipper::Adapters::Http::Error)
334
+
335
+ # Even though it raised an error, response should be stored
336
+ expect(adapter.last_get_all_response).not_to be_nil
337
+ expect(adapter.last_get_all_response['poll-shutdown']).to eq('true')
106
338
  end
107
339
  end
108
340
 
109
341
  describe "#features" do
110
342
  it "raises error when not successful response" do
111
- stub_request(:get, "http://app.com/flipper/features")
112
- .to_return(status: 503, body: "", headers: {})
343
+ stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
344
+ .to_return(status: 503)
113
345
 
114
346
  adapter = described_class.new(url: 'http://app.com/flipper')
115
- expect do
347
+ expect {
116
348
  adapter.features
117
- end.to raise_error(Flipper::Adapters::Http::Error)
349
+ }.to raise_error(Flipper::Adapters::Http::Error)
350
+ end
351
+ end
352
+
353
+ describe "#add" do
354
+ it "raises error when not successful" do
355
+ stub_request(:post, /app.com/)
356
+ .to_return(status: 503, body: "{}", headers: {})
357
+
358
+ adapter = described_class.new(url: 'http://app.com/flipper')
359
+ expect {
360
+ adapter.add(Flipper::Feature.new(:search, adapter))
361
+ }.to raise_error(Flipper::Adapters::Http::Error)
362
+ end
363
+ end
364
+
365
+ describe "#remove" do
366
+ it "raises error when not successful" do
367
+ stub_request(:delete, /app.com/)
368
+ .to_return(status: 503, body: "{}", headers: {})
369
+
370
+ adapter = described_class.new(url: 'http://app.com/flipper')
371
+ expect {
372
+ adapter.remove(Flipper::Feature.new(:search, adapter))
373
+ }.to raise_error(Flipper::Adapters::Http::Error)
374
+ end
375
+ end
376
+
377
+ describe "#clear" do
378
+ it "raises error when not successful" do
379
+ stub_request(:delete, /app.com/)
380
+ .to_return(status: 503, body: "{}", headers: {})
381
+
382
+ adapter = described_class.new(url: 'http://app.com/flipper')
383
+ expect {
384
+ adapter.clear(Flipper::Feature.new(:search, adapter))
385
+ }.to raise_error(Flipper::Adapters::Http::Error)
386
+ end
387
+ end
388
+
389
+ describe "#enable" do
390
+ it "raises error when not successful" do
391
+ stub_request(:post, /app.com/)
392
+ .to_return(status: 503, body: "{}", headers: {})
393
+
394
+ adapter = described_class.new(url: 'http://app.com/flipper')
395
+ feature = Flipper::Feature.new(:search, adapter)
396
+ gate = feature.gate(:boolean)
397
+ thing = gate.wrap(true)
398
+ expect {
399
+ adapter.enable(feature, gate, thing)
400
+ }.to raise_error(Flipper::Adapters::Http::Error, "Failed with status: 503")
401
+ end
402
+
403
+ it "doesn't raise json error if body cannot be parsed" do
404
+ stub_request(:post, /app.com/)
405
+ .to_return(status: 503, body: "barf", headers: {})
406
+
407
+ adapter = described_class.new(url: 'http://app.com/flipper')
408
+ feature = Flipper::Feature.new(:search, adapter)
409
+ gate = feature.gate(:boolean)
410
+ thing = gate.wrap(true)
411
+ expect {
412
+ adapter.enable(feature, gate, thing)
413
+ }.to raise_error(Flipper::Adapters::Http::Error)
414
+ end
415
+
416
+ it "includes response information if available when raising error" do
417
+ api_response = {
418
+ "code" => "error",
419
+ "message" => "This feature has reached the limit to the number of " +
420
+ "actors per feature. Check out groups as a more flexible " +
421
+ "way to enable many actors.",
422
+ "more_info" => "https://www.flippercloud.io/docs",
423
+ }
424
+ stub_request(:post, /app.com/)
425
+ .to_return(status: 503, body: JSON.dump(api_response), headers: {})
426
+
427
+ adapter = described_class.new(url: 'http://app.com/flipper')
428
+ feature = Flipper::Feature.new(:search, adapter)
429
+ gate = feature.gate(:boolean)
430
+ thing = gate.wrap(true)
431
+ error_message = "Failed with status: 503\n\nThis feature has reached the " +
432
+ "limit to the number of actors per feature. Check out " +
433
+ "groups as a more flexible way to enable many actors.\n" +
434
+ "https://www.flippercloud.io/docs"
435
+ expect {
436
+ adapter.enable(feature, gate, thing)
437
+ }.to raise_error(Flipper::Adapters::Http::Error, error_message)
438
+ end
439
+ end
440
+
441
+ describe "#disable" do
442
+ it "raises error when not successful" do
443
+ stub_request(:delete, /app.com/)
444
+ .to_return(status: 503, body: "{}", headers: {})
445
+
446
+ adapter = described_class.new(url: 'http://app.com/flipper')
447
+ feature = Flipper::Feature.new(:search, adapter)
448
+ gate = feature.gate(:boolean)
449
+ thing = gate.wrap(false)
450
+ expect {
451
+ adapter.disable(feature, gate, thing)
452
+ }.to raise_error(Flipper::Adapters::Http::Error)
118
453
  end
119
454
  end
120
455
 
@@ -123,11 +458,12 @@ RSpec.describe Flipper::Adapters::Http do
123
458
  let(:options) do
124
459
  {
125
460
  url: 'http://app.com/mount-point',
126
- headers: { 'X-Custom-Header' => 'foo' },
461
+ headers: { 'x-custom-header' => 'foo' },
127
462
  basic_auth_username: 'username',
128
463
  basic_auth_password: 'password',
129
464
  read_timeout: 100,
130
465
  open_timeout: 40,
466
+ write_timeout: 40,
131
467
  debug_output: debug_output,
132
468
  }
133
469
  end
@@ -135,14 +471,15 @@ RSpec.describe Flipper::Adapters::Http do
135
471
  let(:feature) { flipper[:feature_panel] }
136
472
 
137
473
  before do
138
- stub_request(:get, %r{\Ahttp://app.com*}).to_return(body: fixture_file('feature.json'))
474
+ stub_request(:get, %r{\Ahttp://app.com*}).
475
+ to_return(body: fixture_file('feature.json'))
139
476
  end
140
477
 
141
478
  it 'allows client to set request headers' do
142
479
  subject.get(feature)
143
480
  expect(
144
481
  a_request(:get, 'http://app.com/mount-point/features/feature_panel')
145
- .with(headers: { 'X-Custom-Header' => 'foo' })
482
+ .with(headers: { 'x-custom-header' => 'foo' })
146
483
  ).to have_been_made.once
147
484
  end
148
485
 
@@ -1,7 +1,5 @@
1
- require 'helper'
2
1
  require 'flipper/adapters/instrumented'
3
2
  require 'flipper/instrumenters/memory'
4
- require 'flipper/spec/shared_adapter_specs'
5
3
 
6
4
  RSpec.describe Flipper::Adapters::Instrumented do
7
5
  let(:instrumenter) { Flipper::Instrumenters::Memory.new }
@@ -10,7 +8,7 @@ RSpec.describe Flipper::Adapters::Instrumented do
10
8
 
11
9
  let(:feature) { flipper[:stats] }
12
10
  let(:gate) { feature.gate(:percentage_of_actors) }
13
- let(:thing) { flipper.actors(22) }
11
+ let(:thing) { Flipper::Types::PercentageOfActors.new(22) }
14
12
 
15
13
  subject do
16
14
  described_class.new(adapter, instrumenter: instrumenter)
@@ -18,16 +16,6 @@ RSpec.describe Flipper::Adapters::Instrumented do
18
16
 
19
17
  it_should_behave_like 'a flipper adapter'
20
18
 
21
- it 'forwards missing methods to underlying adapter' do
22
- adapter = Class.new do
23
- def foo
24
- :foo
25
- end
26
- end.new
27
- instrumented = described_class.new(adapter)
28
- expect(instrumented.foo).to eq(:foo)
29
- end
30
-
31
19
  describe '#name' do
32
20
  it 'is instrumented' do
33
21
  expect(subject.name).to be(:instrumented)
@@ -73,6 +61,7 @@ RSpec.describe Flipper::Adapters::Instrumented do
73
61
  expect(event.payload[:adapter_name]).to eq(:memory)
74
62
  expect(event.payload[:feature_name]).to eq(:stats)
75
63
  expect(event.payload[:gate_name]).to eq(:percentage_of_actors)
64
+ expect(event.payload[:thing_value]).to eq(22)
76
65
  expect(event.payload[:result]).to be(result)
77
66
  end
78
67
  end
@@ -88,6 +77,7 @@ RSpec.describe Flipper::Adapters::Instrumented do
88
77
  expect(event.payload[:adapter_name]).to eq(:memory)
89
78
  expect(event.payload[:feature_name]).to eq(:stats)
90
79
  expect(event.payload[:gate_name]).to eq(:percentage_of_actors)
80
+ expect(event.payload[:thing_value]).to eq(22)
91
81
  expect(event.payload[:result]).to be(result)
92
82
  end
93
83
  end
@@ -146,4 +136,32 @@ RSpec.describe Flipper::Adapters::Instrumented do
146
136
  expect(event.payload[:result]).to be(result)
147
137
  end
148
138
  end
139
+
140
+ describe '#import' do
141
+ it 'records instrumentation' do
142
+ result = subject.import(Flipper::Adapters::Memory.new)
143
+
144
+ event = instrumenter.events.last
145
+ expect(event).not_to be_nil
146
+ expect(event.name).to eq('adapter_operation.flipper')
147
+ expect(event.payload[:operation]).to eq(:import)
148
+ expect(event.payload[:adapter_name]).to eq(:memory)
149
+ expect(event.payload[:result]).to be(result)
150
+ end
151
+ end
152
+
153
+ describe '#export' do
154
+ it 'records instrumentation' do
155
+ result = subject.export(format: :json, version: 1)
156
+
157
+ event = instrumenter.events.last
158
+ expect(event).not_to be_nil
159
+ expect(event.name).to eq('adapter_operation.flipper')
160
+ expect(event.payload[:operation]).to eq(:export)
161
+ expect(event.payload[:adapter_name]).to eq(:memory)
162
+ expect(event.payload[:format]).to be(:json)
163
+ expect(event.payload[:version]).to be(1)
164
+ expect(event.payload[:result]).to be(result)
165
+ end
166
+ end
149
167
  end