magick-feature-flags 1.4.3 → 1.5.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.
- checksums.yaml +4 -4
- data/README.md +63 -3
- data/app/controllers/magick/adminui/features_controller.rb +19 -0
- data/config/magick.rb.example +5 -3
- data/lib/generators/magick/install/templates/magick.rb +5 -3
- data/lib/magick/adapters/registry.rb +8 -2
- data/lib/magick/admin_ui.rb +5 -1
- data/lib/magick/config.rb +10 -4
- data/lib/magick/feature.rb +304 -166
- data/lib/magick/rails/railtie.rb +5 -2
- data/lib/magick/version.rb +1 -1
- data/lib/magick/versioning.rb +205 -51
- data/lib/magick.rb +61 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 958764166d59a55b2243c5ce0f009164c26c75ae07740b200808866f3d6ad543
|
|
4
|
+
data.tar.gz: a96a1b187259c225a0b55ec243dbdfc9d9771621b420a4519c2fc7c4e9ee6a5c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1affc66448028865d889535959d7970e8b54d1b3e3367d0bd4fb19ec8b18451c9bbc4714735e5f98f26da4a124e52b56a00c345d161660bfb944a9adb2bcbb12
|
|
7
|
+
data.tar.gz: 3ef2b6d8341517ef3bc8099a582e550877242d50f3fb67eab645b7d13f7adbdcf999f9f7a1d39e86ed3f5d1c13a98e8a69200dc5ab3b8c666cd14e924c462d0f
|
data/README.md
CHANGED
|
@@ -531,12 +531,58 @@ Magick.import(File.read('features.json'))
|
|
|
531
531
|
|
|
532
532
|
#### Versioning and Rollback
|
|
533
533
|
|
|
534
|
+
Every state-changing operation (value, status, group, targeting, exclusions,
|
|
535
|
+
variants, dependencies, delete) automatically records a version snapshot and
|
|
536
|
+
an audit entry — one per logical operation, under its real action name
|
|
537
|
+
(`enable`, `exclude_user`, `set_status`, …). Nested internals never
|
|
538
|
+
double-record.
|
|
539
|
+
|
|
534
540
|
```ruby
|
|
535
|
-
#
|
|
541
|
+
# History accumulates automatically:
|
|
542
|
+
Magick[:my_feature].enable # => version 1 (action: "enable")
|
|
543
|
+
Magick[:my_feature].enable_for_user(42) # => version 2 (action: "enable_for_user")
|
|
544
|
+
|
|
545
|
+
# Inspect history (hot window: last 50 versions by default)
|
|
546
|
+
Magick.versioning.get_versions(:my_feature)
|
|
547
|
+
|
|
548
|
+
# Include the unlimited ActiveRecord archive (when AR adapter is configured)
|
|
549
|
+
Magick.versioning.get_versions(:my_feature, all: true)
|
|
550
|
+
|
|
551
|
+
# Rollback fully restores a snapshot: value (including false/empty), status,
|
|
552
|
+
# group, and the entire targeting hash — and records the rollback itself as a
|
|
553
|
+
# new version, so history only ever rolls forward.
|
|
554
|
+
Magick.versioning.rollback(:my_feature, 2)
|
|
555
|
+
|
|
556
|
+
# Manual snapshots still work (action: "manual")
|
|
536
557
|
Magick.versioning.save_version(:my_feature, created_by: current_user.id)
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
**Retention is tiered:** memory/Redis keep the last `max_versions` snapshots
|
|
561
|
+
(default 50) for fast access; the ActiveRecord adapter keeps an unlimited
|
|
562
|
+
archive that also survives feature deletion.
|
|
537
563
|
|
|
538
|
-
|
|
539
|
-
Magick.
|
|
564
|
+
```ruby
|
|
565
|
+
Magick.configure do
|
|
566
|
+
versioning enabled: true, max_versions: 50
|
|
567
|
+
end
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
**Attribution:** wrap changes in `Magick.with_actor` to stamp audit entries
|
|
571
|
+
(`user_id`) and versions (`created_by`):
|
|
572
|
+
|
|
573
|
+
```ruby
|
|
574
|
+
Magick.with_actor(current_user.id) do
|
|
575
|
+
Magick[:my_feature].enable_for_user(42)
|
|
576
|
+
end
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
**Boot replay is not recorded:** the Rails railtie loads `config/features.rb`
|
|
580
|
+
inside `Magick.definition_mode`, so re-applying declarative definitions on
|
|
581
|
+
every process boot does not flood history. Non-Rails apps should wrap their
|
|
582
|
+
own definition file load the same way:
|
|
583
|
+
|
|
584
|
+
```ruby
|
|
585
|
+
Magick.definition_mode { load 'config/features.rb' }
|
|
540
586
|
```
|
|
541
587
|
|
|
542
588
|
#### Performance Metrics
|
|
@@ -595,6 +641,11 @@ end
|
|
|
595
641
|
|
|
596
642
|
#### Audit Logging
|
|
597
643
|
|
|
644
|
+
Every mutation is logged under its real action name (`enable`, `disable`,
|
|
645
|
+
`set_value`, `enable_for_user`, `exclude_role`, `set_status`, `set_group`,
|
|
646
|
+
`delete`, `rollback`, …). One logical operation produces exactly one entry:
|
|
647
|
+
`enable` no longer surfaces as a bare `set_value`.
|
|
648
|
+
|
|
598
649
|
```ruby
|
|
599
650
|
# View audit log entries
|
|
600
651
|
entries = Magick.audit_log.entries(feature_name: :my_feature, limit: 100)
|
|
@@ -603,6 +654,15 @@ entries.each do |entry|
|
|
|
603
654
|
end
|
|
604
655
|
```
|
|
605
656
|
|
|
657
|
+
In the Admin UI, configure a `current_actor` hook so every change made
|
|
658
|
+
through the UI is attributed:
|
|
659
|
+
|
|
660
|
+
```ruby
|
|
661
|
+
Magick::AdminUI.configure do |config|
|
|
662
|
+
config.current_actor = ->(controller) { controller.session[:admin_id] }
|
|
663
|
+
end
|
|
664
|
+
```
|
|
665
|
+
|
|
606
666
|
## Architecture
|
|
607
667
|
|
|
608
668
|
### Adapters
|
|
@@ -15,6 +15,9 @@ module Magick
|
|
|
15
15
|
layout 'application'
|
|
16
16
|
before_action :authenticate_admin!
|
|
17
17
|
before_action :set_feature, only: %i[show edit update enable disable enable_for_user enable_for_role disable_for_role update_targeting update_variants]
|
|
18
|
+
# Attribute every change made during the request to the configured
|
|
19
|
+
# actor, so audit entries and version snapshots record who did it.
|
|
20
|
+
around_action :with_magick_actor
|
|
18
21
|
# Render the TRUE current state, not this process's local cache. In a
|
|
19
22
|
# multi-process / multi-container deployment the enable/disable POST and
|
|
20
23
|
# the redirected GET are load-balanced to different processes, so the
|
|
@@ -321,6 +324,22 @@ module Magick
|
|
|
321
324
|
|
|
322
325
|
private
|
|
323
326
|
|
|
327
|
+
# Resolve the acting admin via the configurable AdminUI hook and run the
|
|
328
|
+
# action inside Magick.with_actor. A failing resolver only costs
|
|
329
|
+
# attribution — it must never 500 the admin UI, and it is rescued
|
|
330
|
+
# separately so an action error is never swallowed or re-run.
|
|
331
|
+
def with_magick_actor(&block)
|
|
332
|
+
actor = begin
|
|
333
|
+
resolver = Magick::AdminUI.config.current_actor
|
|
334
|
+
resolver.respond_to?(:call) ? resolver.call(self) : nil
|
|
335
|
+
rescue StandardError => e
|
|
336
|
+
Rails.logger.warn "Magick: current_actor hook failed: #{e.class}: #{e.message}" if defined?(Rails)
|
|
337
|
+
nil
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
actor ? Magick.with_actor(actor, &block) : yield
|
|
341
|
+
end
|
|
342
|
+
|
|
324
343
|
def authenticate_admin!
|
|
325
344
|
return unless Magick::AdminUI.config.require_role
|
|
326
345
|
|
data/config/magick.rb.example
CHANGED
|
@@ -20,11 +20,13 @@ Magick.configure do
|
|
|
20
20
|
# Enable performance metrics tracking
|
|
21
21
|
performance_metrics enabled: true
|
|
22
22
|
|
|
23
|
-
# Enable audit logging
|
|
23
|
+
# Enable audit logging (every mutation is logged under its real action name)
|
|
24
24
|
audit_log enabled: true
|
|
25
25
|
|
|
26
|
-
# Enable versioning
|
|
27
|
-
|
|
26
|
+
# Enable versioning (every save creates a version snapshot; allows rollback)
|
|
27
|
+
# max_versions caps the hot window kept in memory/Redis; the ActiveRecord
|
|
28
|
+
# adapter keeps an unlimited archive.
|
|
29
|
+
versioning enabled: true, max_versions: 50
|
|
28
30
|
|
|
29
31
|
# Enable deprecation warnings
|
|
30
32
|
warn_on_deprecated true
|
|
@@ -23,11 +23,13 @@ Magick.configure do
|
|
|
23
23
|
# Enable performance metrics tracking
|
|
24
24
|
performance_metrics enabled: true
|
|
25
25
|
|
|
26
|
-
# Enable audit logging (
|
|
26
|
+
# Enable audit logging (every mutation is logged under its real action name)
|
|
27
27
|
audit_log enabled: true
|
|
28
28
|
|
|
29
|
-
# Enable versioning
|
|
30
|
-
|
|
29
|
+
# Enable versioning (every save creates a version snapshot; allows rollback)
|
|
30
|
+
# max_versions caps the hot window kept in memory/Redis; the ActiveRecord
|
|
31
|
+
# adapter keeps an unlimited archive.
|
|
32
|
+
versioning enabled: true, max_versions: 50
|
|
31
33
|
|
|
32
34
|
# Enable deprecation warnings in logs
|
|
33
35
|
warn_on_deprecated enabled: true
|
|
@@ -176,7 +176,9 @@ module Magick
|
|
|
176
176
|
features += memory_adapter.all_features if memory_adapter
|
|
177
177
|
features += redis_adapter.all_features if redis_adapter
|
|
178
178
|
features += active_record_adapter.all_features if active_record_adapter
|
|
179
|
-
|
|
179
|
+
# Version history is stored under a reserved pseudo-feature namespace;
|
|
180
|
+
# it is bookkeeping, not a feature.
|
|
181
|
+
features.uniq.reject { |f| f.to_s.start_with?(Versioning::STORE_PREFIX) }
|
|
180
182
|
end
|
|
181
183
|
|
|
182
184
|
# Load all keys for a single feature in one call instead of N separate get() calls
|
|
@@ -357,9 +359,13 @@ module Magick
|
|
|
357
359
|
end
|
|
358
360
|
end
|
|
359
361
|
|
|
362
|
+
# Public so Versioning can apply tiered retention: hot window written to
|
|
363
|
+
# memory/Redis, unlimited archive written to ActiveRecord only.
|
|
364
|
+
attr_reader :memory_adapter, :redis_adapter, :active_record_adapter
|
|
365
|
+
|
|
360
366
|
private
|
|
361
367
|
|
|
362
|
-
attr_reader :
|
|
368
|
+
attr_reader :circuit_breaker
|
|
363
369
|
|
|
364
370
|
# Signal the subscribe loop to return, then close the connection so any
|
|
365
371
|
# retry/reconnect attempt fails fast instead of sleeping for 5s.
|
data/lib/magick/admin_ui.rb
CHANGED
|
@@ -17,7 +17,7 @@ module Magick
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
class Configuration
|
|
20
|
-
attr_accessor :theme, :brand_name, :require_role, :available_roles, :available_tags
|
|
20
|
+
attr_accessor :theme, :brand_name, :require_role, :available_roles, :available_tags, :current_actor
|
|
21
21
|
|
|
22
22
|
def initialize
|
|
23
23
|
@theme = :light
|
|
@@ -25,6 +25,10 @@ module Magick
|
|
|
25
25
|
@require_role = nil
|
|
26
26
|
@available_roles = [] # Can be populated via DSL: admin_ui { roles ['admin', 'user', 'manager'] }
|
|
27
27
|
@available_tags = nil # Can be array or lambda: -> { Tag.all }
|
|
28
|
+
# Lambda receiving the controller, returning who is making the
|
|
29
|
+
# change; stamped onto audit entries (user_id) and versions
|
|
30
|
+
# (created_by): -> (controller) { controller.current_user&.id }
|
|
31
|
+
@current_actor = nil
|
|
28
32
|
end
|
|
29
33
|
|
|
30
34
|
# Get available tags, calling lambda if needed
|
data/lib/magick/config.rb
CHANGED
|
@@ -135,8 +135,11 @@ module Magick
|
|
|
135
135
|
end
|
|
136
136
|
end
|
|
137
137
|
|
|
138
|
-
def versioning(enabled: true)
|
|
139
|
-
@
|
|
138
|
+
def versioning(enabled: true, max_versions: Versioning::DEFAULT_MAX_VERSIONS)
|
|
139
|
+
@versioning_enabled = enabled
|
|
140
|
+
@versioning = if enabled
|
|
141
|
+
Versioning.new(adapter_registry || default_adapter_registry, max_versions: max_versions)
|
|
142
|
+
end
|
|
140
143
|
end
|
|
141
144
|
|
|
142
145
|
def circuit_breaker(threshold: nil, timeout: nil)
|
|
@@ -186,8 +189,11 @@ module Magick
|
|
|
186
189
|
end
|
|
187
190
|
end
|
|
188
191
|
|
|
189
|
-
|
|
190
|
-
|
|
192
|
+
# Read the ivars directly: calling the DSL methods here would re-run
|
|
193
|
+
# them with their defaults and stomp explicit `enabled: false` settings.
|
|
194
|
+
Magick.audit_log = @audit_log if @audit_log
|
|
195
|
+
Magick.versioning = @versioning if @versioning
|
|
196
|
+
Magick.versioning_enabled = @versioning_enabled unless @versioning_enabled.nil?
|
|
191
197
|
Magick.warn_on_deprecated = warn_on_deprecated
|
|
192
198
|
end
|
|
193
199
|
|