magick-feature-flags 0.8.0 → 0.8.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a5124df88b7d429811d4b0f131d01aeeef2dedabc25b598da96fa5f01eb07163
4
- data.tar.gz: 72c12e81d29990d6990f0f749effb978c7f9a4d7a316d3455e8ceb508269889d
3
+ metadata.gz: a53f3c93e974822f126ed2148d9fb61adf7de3a374ba26acbd834a2a916545a0
4
+ data.tar.gz: 31dbbacc15565a496e65113364d3eaf5a6a5385251f3654a28ea1c5eb9e9b014
5
5
  SHA512:
6
- metadata.gz: 880f57facbd900c562aa10d21708afe0100087a6160ac814cc9d3453233d62cb7b41de1bfe18a831f27ecbd14cfb2b59b238a92f91c4a8fa56d0ae30a62ba125
7
- data.tar.gz: cd4addd623241b24d2bc3d08cab687233de382d3c1dc260461484e6b750c637f19dc51368c552694c021f503a30c85e1425016f1af4a7056b8f3cf19f816a299
6
+ metadata.gz: 9b39439b771eb158d39496758362675db1134ea160311b8af9d51b59e1b40c7440ee425ee7d8f6e28bd02dc3da6498ec24d79744cd5cb7b63f54e13e86fa8cf4
7
+ data.tar.gz: 0bc2519decc3164d6b3dddd6080bf79c00b9a1ee2473d95b25550edbea8ca8c433912b8655d72c86b993762526781377df27219f1957585990b59942e3597bb8
data/README.md CHANGED
@@ -221,8 +221,15 @@ Create `config/features.rb`:
221
221
 
222
222
  ```ruby
223
223
  # Boolean features
224
- boolean_feature :new_dashboard, default: false, description: "New dashboard UI"
225
- boolean_feature :dark_mode, default: false, description: "Dark mode theme"
224
+ boolean_feature :new_dashboard,
225
+ default: false,
226
+ name: "New Dashboard",
227
+ description: "New dashboard UI"
228
+
229
+ boolean_feature :dark_mode,
230
+ default: false,
231
+ name: "Dark Mode",
232
+ description: "Dark mode theme"
226
233
 
227
234
  # String features
228
235
  string_feature :api_version, default: "v1", description: "API version"
data/lib/magick/config.rb CHANGED
@@ -44,8 +44,16 @@ module Magick
44
44
  configure_redis_adapter(url: url, namespace: namespace, **options)
45
45
  end
46
46
 
47
- def performance_metrics(enabled: true, **_options)
48
- @performance_metrics = (PerformanceMetrics.new if enabled)
47
+ def performance_metrics(enabled: true, redis_tracking: nil, batch_size: 100, flush_interval: 60, **_options)
48
+ return unless enabled
49
+
50
+ @performance_metrics = PerformanceMetrics.new(batch_size: batch_size, flush_interval: flush_interval)
51
+ # Enable Redis tracking if Redis adapter is configured (or explicitly set)
52
+ if redis_tracking.nil?
53
+ # Auto-enable if Redis adapter exists
54
+ redis_tracking = !@redis_url.nil? || configure_redis_adapter != nil
55
+ end
56
+ @performance_metrics.enable_redis_tracking(enable: redis_tracking) if @performance_metrics
49
57
  end
50
58
 
51
59
  def audit_log(enabled: true, adapter: nil)
@@ -86,6 +94,11 @@ module Magick
86
94
  Magick.audit_log = audit_log if audit_log
87
95
  Magick.versioning = versioning if versioning
88
96
  Magick.warn_on_deprecated = warn_on_deprecated
97
+
98
+ # Enable Redis tracking for performance metrics if Redis adapter is configured
99
+ if Magick.performance_metrics && adapter_registry.is_a?(Adapters::Registry) && adapter_registry.redis_adapter
100
+ Magick.performance_metrics.enable_redis_tracking(enable: true)
101
+ end
89
102
  end
90
103
 
91
104
  private
@@ -8,7 +8,7 @@ module Magick
8
8
  VALID_TYPES = %i[boolean string number].freeze
9
9
  VALID_STATUSES = %i[active inactive deprecated].freeze
10
10
 
11
- attr_reader :name, :type, :status, :default_value, :description, :adapter_registry
11
+ attr_reader :name, :type, :status, :default_value, :description, :display_name, :adapter_registry
12
12
 
13
13
  def initialize(name, adapter_registry, **options)
14
14
  @name = name.to_s
@@ -17,6 +17,7 @@ module Magick
17
17
  @status = (options[:status] || :active).to_sym
18
18
  @default_value = options.fetch(:default_value, default_for_type)
19
19
  @description = options[:description]
20
+ @display_name = options[:name] || options[:display_name]
20
21
  @targeting = {}
21
22
  @dependencies = options[:dependencies] ? Array(options[:dependencies]) : []
22
23
 
@@ -318,6 +319,7 @@ module Magick
318
319
  adapter_registry.set(name, 'status', status)
319
320
  adapter_registry.set(name, 'default_value', default_value)
320
321
  adapter_registry.set(name, 'description', description) if description
322
+ adapter_registry.set(name, 'display_name', display_name) if display_name
321
323
  @stored_value = value
322
324
 
323
325
  # Update registered feature instance if it exists
@@ -382,6 +384,9 @@ module Magick
382
384
  raise InvalidFeatureValueError, "Cannot disable feature of type #{type}"
383
385
  end
384
386
 
387
+ # Cascade disable: disable all features that depend on this one
388
+ disable_dependent_features(user_id: user_id)
389
+
385
390
  # Rails 8+ event
386
391
  if defined?(Magick::Rails::Events) && Magick::Rails::Events.rails8?
387
392
  Magick::Rails::Events.feature_disabled_globally(name, user_id: user_id)
@@ -410,6 +415,7 @@ module Magick
410
415
  def to_h
411
416
  {
412
417
  name: name,
418
+ display_name: display_name,
413
419
  type: type,
414
420
  status: status,
415
421
  value: stored_value,
@@ -437,6 +443,7 @@ module Magick
437
443
  registered = Magick.features[name]
438
444
  registered.instance_variable_set(:@stored_value, @stored_value)
439
445
  registered.instance_variable_set(:@status, @status)
446
+ registered.instance_variable_set(:@display_name, @display_name)
440
447
  registered.instance_variable_set(:@targeting, @targeting.dup)
441
448
  end
442
449
  self
@@ -448,6 +455,8 @@ module Magick
448
455
  @stored_value = load_value_from_adapter
449
456
  status_value = adapter_registry.get(name, 'status')
450
457
  @status = status_value ? status_value.to_sym : status
458
+ display_name_value = adapter_registry.get(name, 'display_name')
459
+ @display_name = display_name_value if display_name_value
451
460
  targeting_value = adapter_registry.get(name, 'targeting')
452
461
  if targeting_value.is_a?(Hash)
453
462
  # Normalize keys to symbols and handle nested structures
@@ -655,6 +664,34 @@ module Magick
655
664
  context
656
665
  end
657
666
 
667
+ def disable_dependent_features(user_id: nil)
668
+ # Find all features that depend on this feature
669
+ dependent_features = find_dependent_features
670
+
671
+ # Disable each dependent feature
672
+ dependent_features.each do |dep_feature_name|
673
+ dep_feature = Magick.features[dep_feature_name.to_s] || Magick[dep_feature_name]
674
+ next unless dep_feature
675
+
676
+ # Only disable if it's currently enabled (to avoid unnecessary operations)
677
+ if dep_feature.enabled?
678
+ dep_feature.disable(user_id: user_id)
679
+ end
680
+ end
681
+ end
682
+
683
+ def find_dependent_features
684
+ # Find all features that have this feature in their dependencies
685
+ dependent_features = []
686
+ Magick.features.each do |_name, feature|
687
+ feature_deps = feature.instance_variable_get(:@dependencies) || []
688
+ if feature_deps.include?(name.to_s) || feature_deps.include?(name.to_sym)
689
+ dependent_features << feature.name
690
+ end
691
+ end
692
+ dependent_features
693
+ end
694
+
658
695
  def enable_targeting(type, value)
659
696
  @targeting[type] ||= []
660
697
  @targeting[type] << value.to_s unless @targeting[type].include?(value.to_s)
@@ -24,17 +24,25 @@ module Magick
24
24
  end
25
25
  end
26
26
 
27
- def initialize
27
+ def initialize(batch_size: 100, flush_interval: 60)
28
28
  @metrics = []
29
29
  @mutex = Mutex.new
30
30
  @usage_count = Hash.new(0)
31
+ @pending_updates = Hash.new(0) # For Redis batching
32
+ @batch_size = batch_size
33
+ @flush_interval = flush_interval
34
+ @last_flush = Time.now
35
+ @redis_enabled = false
31
36
  end
32
37
 
33
38
  def record(feature_name, operation, duration, success: true)
34
- metric = Metric.new(feature_name, operation, duration, success: success)
39
+ feature_name_str = feature_name.to_s
40
+ metric = Metric.new(feature_name_str, operation, duration, success: success)
41
+
35
42
  @mutex.synchronize do
36
43
  @metrics << metric
37
- @usage_count[feature_name.to_s] += 1
44
+ @usage_count[feature_name_str] += 1
45
+ @pending_updates[feature_name_str] += 1
38
46
  # Keep only last 1000 metrics
39
47
  @metrics.shift if @metrics.length > 1000
40
48
  end
@@ -44,9 +52,64 @@ module Magick
44
52
  Magick::Rails::Events.usage_tracked(feature_name, operation: operation, duration: duration, success: success)
45
53
  end
46
54
 
55
+ # Batch flush to Redis if needed
56
+ flush_to_redis_if_needed
57
+
47
58
  metric
48
59
  end
49
60
 
61
+ def flush_to_redis_if_needed
62
+ return unless @redis_enabled
63
+ return if @pending_updates.empty?
64
+
65
+ should_flush = false
66
+ @mutex.synchronize do
67
+ # Flush if we have enough pending updates or enough time has passed
68
+ if @pending_updates.size >= @batch_size || (Time.now - @last_flush) >= @flush_interval
69
+ should_flush = true
70
+ end
71
+ end
72
+
73
+ flush_to_redis if should_flush
74
+ end
75
+
76
+ def flush_to_redis
77
+ updates_to_flush = nil
78
+ @mutex.synchronize do
79
+ return if @pending_updates.empty?
80
+
81
+ updates_to_flush = @pending_updates.dup
82
+ @pending_updates.clear
83
+ @last_flush = Time.now
84
+ end
85
+
86
+ return if updates_to_flush.nil? || updates_to_flush.empty?
87
+
88
+ # Update Redis in batch
89
+ begin
90
+ adapter = Magick.adapter_registry || Magick.default_adapter_registry
91
+ if adapter.is_a?(Magick::Adapters::Registry) && adapter.redis_adapter
92
+ redis = adapter.redis_adapter.instance_variable_get(:@redis)
93
+ if redis
94
+ updates_to_flush.each do |feature_name, count|
95
+ redis_key = "magick:stats:#{feature_name}"
96
+ redis.incrby(redis_key, count)
97
+ redis.expire(redis_key, 86400 * 7) # Keep stats for 7 days
98
+ end
99
+ end
100
+ end
101
+ rescue StandardError => e
102
+ # Silently fail - don't break feature checks if stats fail
103
+ warn "Magick: Failed to flush stats to Redis: #{e.message}" if defined?(Rails) && Rails.env.development?
104
+ end
105
+ end
106
+
107
+ def enable_redis_tracking(enable: true)
108
+ @redis_enabled = enable
109
+ # Flush any pending updates when enabling
110
+ flush_to_redis if enable && !@pending_updates.empty?
111
+ end
112
+
50
113
  def average_duration(feature_name: nil, operation: nil)
51
114
  filtered = @metrics.select do |m|
52
115
  (feature_name.nil? || m.feature_name == feature_name.to_s) &&
@@ -59,17 +122,60 @@ module Magick
59
122
  end
60
123
 
61
124
  def usage_count(feature_name)
62
- @usage_count[feature_name.to_s] || 0
125
+ feature_name_str = feature_name.to_s
126
+ memory_count = @usage_count[feature_name_str] || 0
127
+
128
+ # Also check Redis if enabled
129
+ redis_count = 0
130
+ if @redis_enabled
131
+ begin
132
+ adapter = Magick.adapter_registry || Magick.default_adapter_registry
133
+ if adapter.is_a?(Magick::Adapters::Registry) && adapter.redis_adapter
134
+ redis = adapter.redis_adapter.instance_variable_get(:@redis)
135
+ if redis
136
+ redis_key = "magick:stats:#{feature_name_str}"
137
+ redis_count = redis.get(redis_key).to_i
138
+ end
139
+ end
140
+ rescue StandardError
141
+ # Silently fail
142
+ end
143
+ end
144
+
145
+ memory_count + redis_count
63
146
  end
64
147
 
65
148
  def most_used_features(limit: 10)
66
- @usage_count.sort_by { |_name, count| -count }.first(limit).to_h
149
+ # Combine memory and Redis counts
150
+ combined_counts = @usage_count.dup
151
+
152
+ if @redis_enabled
153
+ begin
154
+ adapter = Magick.adapter_registry || Magick.default_adapter_registry
155
+ if adapter.is_a?(Magick::Adapters::Registry) && adapter.redis_adapter
156
+ redis = adapter.redis_adapter.instance_variable_get(:@redis)
157
+ if redis
158
+ # Get all stats keys
159
+ redis.keys("magick:stats:*").each do |key|
160
+ feature_name = key.to_s.sub('magick:stats:', '')
161
+ redis_count = redis.get(key).to_i
162
+ combined_counts[feature_name] = (combined_counts[feature_name] || 0) + redis_count
163
+ end
164
+ end
165
+ end
166
+ rescue StandardError
167
+ # Silently fail
168
+ end
169
+ end
170
+
171
+ combined_counts.sort_by { |_name, count| -count }.first(limit).to_h
67
172
  end
68
173
 
69
174
  def clear!
70
175
  @mutex.synchronize do
71
176
  @metrics.clear
72
177
  @usage_count.clear
178
+ @pending_updates.clear
73
179
  end
74
180
  end
75
181
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Magick
4
- VERSION = '0.8.0'
4
+ VERSION = '0.8.2'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: magick-feature-flags
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.8.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Lobanov