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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3e6f3b4ee81b2521e7f6975c0bd3c7a0a886ea2e23b5792b37e48cd28b08cb8d
4
- data.tar.gz: 3aeb62560e637d844af0ea4e320a08a48eb72a33ebfd0653482899f42e8e2079
3
+ metadata.gz: 958764166d59a55b2243c5ce0f009164c26c75ae07740b200808866f3d6ad543
4
+ data.tar.gz: a96a1b187259c225a0b55ec243dbdfc9d9771621b420a4519c2fc7c4e9ee6a5c
5
5
  SHA512:
6
- metadata.gz: 1135ba02b878e0d772259545231aa89f950e11199a0ee8d982440b9040f9c575a077567f3cd404727b27eb6eb962d811c6b301f92ad2227817289851392cf6c9
7
- data.tar.gz: fb95eca4f8aa3b013298c087209818c91338ef4f9fe2a843951777df4c17fbe9bf83fdc5b1b45447503d5e718c6059d8a57b888746a36f463c929d8965526b3a
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
- # Save current state as version
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
- # Rollback to previous version
539
- Magick.versioning.rollback(:my_feature, version: 2)
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
 
@@ -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 support
27
- versioning enabled: true
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 (tracks who changed what, when)
26
+ # Enable audit logging (every mutation is logged under its real action name)
27
27
  audit_log enabled: true
28
28
 
29
- # Enable versioning support (allows rollback)
30
- versioning enabled: true
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
- features.uniq
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 :memory_adapter, :redis_adapter, :active_record_adapter, :circuit_breaker
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.
@@ -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
- @versioning = (Versioning.new(adapter_registry || default_adapter_registry) if enabled)
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
- Magick.audit_log = audit_log if audit_log
190
- Magick.versioning = versioning if versioning
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