Module: Magick

Extended by:
Rails::RequestStoreIntegration
Defined in:
lib/magick/rails/railtie.rb,
lib/magick.rb,
lib/magick/dsl.rb,
lib/magick/config.rb,
lib/magick/errors.rb,
lib/magick/feature.rb,
lib/magick/version.rb,
lib/magick/admin_ui.rb,
lib/magick/log_safe.rb,
lib/magick/audit_log.rb,
lib/magick/versioning.rb,
lib/magick/rails/events.rb,
lib/magick/adapters/base.rb,
lib/magick/documentation.rb,
lib/magick/export_import.rb,
lib/magick/adapters/redis.rb,
lib/magick/targeting/base.rb,
lib/magick/targeting/role.rb,
lib/magick/targeting/user.rb,
lib/magick/adapters/memory.rb,
lib/magick/admin_ui/engine.rb,
lib/magick/circuit_breaker.rb,
lib/magick/feature_variant.rb,
lib/magick/targeting/group.rb,
lib/magick/testing_helpers.rb,
lib/magick/admin_ui/helpers.rb,
lib/magick/adapters/registry.rb,
lib/magick/targeting/complex.rb,
lib/magick/targeting_payload.rb,
lib/magick/performance_metrics.rb,
lib/magick/targeting/date_range.rb,
lib/magick/targeting/ip_address.rb,
lib/magick/targeting/percentage.rb,
lib/magick/adapters/active_record.rb,
lib/magick/rails/event_subscriber.rb,
lib/magick/targeting/custom_attribute.rb,
lib/magick/targeting/request_percentage.rb,
app/controllers/magick/adminui/stats_controller.rb,
lib/generators/magick/install/install_generator.rb,
app/controllers/magick/adminui/features_controller.rb,
lib/generators/magick/active_record/active_record_generator.rb

Overview

Admin UI engine is now loaded in magick.rb when Rails is detected

Defined Under Namespace

Modules: Adapters, AdminUI, ConfigDSL, DSL, Generators, LogSafe, Rails, RequestStoreIntegration, Targeting, TargetingPayload, TestingHelpers Classes: AdapterError, AuditLog, CircuitBreaker, Config, Documentation, Error, ExportImport, Feature, FeatureNotFoundError, FeatureVariant, InvalidFeatureTypeError, InvalidFeatureValueError, InvalidTargetingError, PerformanceMetrics, SubscriberMiddleware, Versioning

Constant Summary collapse

VERSION =
'1.6.0'

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.adapter_registryObject

Returns the value of attribute adapter_registry.



52
53
54
# File 'lib/magick.rb', line 52

def adapter_registry
  @adapter_registry
end

.audit_logObject

Returns the value of attribute audit_log.



52
53
54
# File 'lib/magick.rb', line 52

def audit_log
  @audit_log
end

.default_adapterObject

Returns the value of attribute default_adapter.



52
53
54
# File 'lib/magick.rb', line 52

def default_adapter
  @default_adapter
end

.versioningObject

Returns the value of attribute versioning.



52
53
54
# File 'lib/magick.rb', line 52

def versioning
  @versioning
end

.versioning_enabled=(value) ⇒ Object (writeonly)

When false, Feature#record_change skips version snapshots (audit log entries are still written). Set via versioning enabled: false in the configuration DSL.



223
224
225
# File 'lib/magick.rb', line 223

def versioning_enabled=(value)
  @versioning_enabled = value
end

.warn_on_deprecatedObject

Returns the value of attribute warn_on_deprecated.



52
53
54
# File 'lib/magick.rb', line 52

def warn_on_deprecated
  @warn_on_deprecated
end

Class Method Details

.[](feature_name) ⇒ Object



117
118
119
120
# File 'lib/magick.rb', line 117

def [](feature_name)
  # Return registered feature if it exists, otherwise create new instance
  features[feature_name.to_s] || Feature.new(feature_name, adapter_registry || default_adapter_registry)
end

.bulk_disable(feature_names, _context = {}) ⇒ Object



189
190
191
192
193
194
195
# File 'lib/magick.rb', line 189

def bulk_disable(feature_names, _context = {})
  feature_names.map do |name|
    feature = features[name.to_s] || self[name]
    feature.set_value(false) if feature.type == :boolean
    feature
  end
end

.bulk_enable(feature_names, _context = {}) ⇒ Object



181
182
183
184
185
186
187
# File 'lib/magick.rb', line 181

def bulk_enable(feature_names, _context = {})
  feature_names.map do |name|
    feature = features[name.to_s] || self[name]
    feature.set_value(true) if feature.type == :boolean
    feature
  end
end

.change_recording_suppressed?Boolean

Returns:

  • (Boolean)


274
275
276
# File 'lib/magick.rb', line 274

def change_recording_suppressed?
  Thread.current[:magick_change_recording] == true || definition_mode?
end

.configure(&block) ⇒ Object



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/magick.rb', line 86

def configure(&block)
  @performance_metrics ||= PerformanceMetrics.new
  @audit_log ||= AuditLog.new
  @warn_on_deprecated ||= false
  # Ensure adapter_registry is set (fallback to default if not configured)
  @adapter_registry ||= default_adapter_registry

  # Support both old style and new DSL style
  return unless block_given?

  if block.arity.zero?
    # New DSL style - calls apply! automatically
    Magick::ConfigDSL.configure(&block)
  else
    # Old style - need to manually reapply Redis tracking after configuration
    yield self
    # Ensure adapter_registry is still set after configuration
    @adapter_registry ||= default_adapter_registry
    # Enable Redis tracking if adapter is available and performance_metrics exists
    # Only enable if not already enabled (to avoid overriding explicit false setting)
    if @performance_metrics && @adapter_registry.is_a?(Adapters::Registry) && @adapter_registry.redis_available?
      unless @performance_metrics.instance_variable_get(:@redis_enabled)
        @performance_metrics.enable_redis_tracking(enable: true)
      end
    end
  end

  # Final check: ensure adapter_registry is set
  @adapter_registry ||= default_adapter_registry
end

.current_actorObject



242
243
244
# File 'lib/magick.rb', line 242

def current_actor
  Thread.current[:magick_actor]
end

.default_adapter_registryObject

Get default adapter registry (public method for use by other classes)



358
359
360
361
362
363
364
365
366
367
368
# File 'lib/magick.rb', line 358

def default_adapter_registry
  @default_adapter_registry ||= begin
    memory_adapter = Adapters::Memory.new
    redis_adapter = begin
      Adapters::Redis.new if defined?(Redis)
    rescue AdapterError
      nil
    end
    Adapters::Registry.new(memory_adapter, redis_adapter)
  end
end

.definition_modeObject

Suppress audit/version recording while declarative feature definitions are (re)applied. Process boot replays config/features.rb in every container; recording those replays would flood history with identical snapshots. The Rails railtie wraps the features file load in this; non-Rails apps should do the same around their definition file.



251
252
253
254
255
256
257
# File 'lib/magick.rb', line 251

def definition_mode
  previous = Thread.current[:magick_definition_mode]
  Thread.current[:magick_definition_mode] = true
  yield
ensure
  Thread.current[:magick_definition_mode] = previous
end

.definition_mode?Boolean

Returns:

  • (Boolean)


259
260
261
# File 'lib/magick.rb', line 259

def definition_mode?
  Thread.current[:magick_definition_mode] == true
end

.disabled?(feature_name, context = {}) ⇒ Boolean

Returns:

  • (Boolean)


163
164
165
# File 'lib/magick.rb', line 163

def disabled?(feature_name, context = {})
  !enabled?(feature_name, context)
end

.disabled_for?(feature_name, object, **additional_context) ⇒ Boolean

Returns:

  • (Boolean)


153
154
155
# File 'lib/magick.rb', line 153

def disabled_for?(feature_name, object, **additional_context)
  !enabled_for?(feature_name, object, **additional_context)
end

.enable_redis_tracking(enable: true) ⇒ Object

Manually enable Redis tracking for performance metrics Useful if Redis adapter becomes available after initial configuration



280
281
282
283
284
# File 'lib/magick.rb', line 280

def enable_redis_tracking(enable: true)
  return unless performance_metrics

  performance_metrics.enable_redis_tracking(enable: enable)
end

.enabled?(feature_name, context = {}) ⇒ Boolean

Returns:

  • (Boolean)


141
142
143
144
145
146
# File 'lib/magick.rb', line 141

def enabled?(feature_name, context = {})
  # Fast path: use string key directly (avoid repeated to_s conversion)
  feature_name_str = feature_name.to_s
  feature = features[feature_name_str] || self[feature_name]
  feature.enabled?(context)
end

.enabled_for?(feature_name, object, **additional_context) ⇒ Boolean

Returns:

  • (Boolean)


148
149
150
151
# File 'lib/magick.rb', line 148

def enabled_for?(feature_name, object, **additional_context)
  feature = features[feature_name.to_s] || self[feature_name]
  feature.enabled_for?(object, **additional_context)
end

.exists?(feature_name) ⇒ Boolean

Returns:

  • (Boolean)


177
178
179
# File 'lib/magick.rb', line 177

def exists?(feature_name)
  features.key?(feature_name.to_s) || (adapter_registry || default_adapter_registry).exists?(feature_name)
end

.export(format: :json) ⇒ Object



197
198
199
200
201
202
203
204
205
206
# File 'lib/magick.rb', line 197

def export(format: :json)
  case format
  when :json
    ExportImport.export_json(features)
  when :hash
    ExportImport.export(features)
  else
    ExportImport.export(features)
  end
end

.feature_average_duration(feature_name, operation: nil) ⇒ Object

Get average duration for a feature (optionally filtered by operation)



307
308
309
310
311
# File 'lib/magick.rb', line 307

def feature_average_duration(feature_name, operation: nil)
  return 0.0 unless performance_metrics

  performance_metrics.average_duration(feature_name: feature_name, operation: operation)
end

.feature_stats(feature_name) ⇒ Object

Get total usage count for a feature (combines memory and Redis counts)



287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/magick.rb', line 287

def feature_stats(feature_name)
  return {} unless performance_metrics

  {
    usage_count: performance_metrics.usage_count(feature_name),
    average_duration: performance_metrics.average_duration(feature_name: feature_name),
    average_duration_by_operation: {
      enabled: performance_metrics.average_duration(feature_name: feature_name, operation: 'enabled?'),
      value: performance_metrics.average_duration(feature_name: feature_name, operation: 'value'),
      get_value: performance_metrics.average_duration(feature_name: feature_name, operation: 'get_value')
    }
  }
end

.feature_usage_count(feature_name) ⇒ Object

Get usage count for a feature



302
303
304
# File 'lib/magick.rb', line 302

def feature_usage_count(feature_name)
  performance_metrics&.usage_count(feature_name) || 0
end

.featuresObject



129
130
131
# File 'lib/magick.rb', line 129

def features
  @features ||= {}
end

.import(data, format: :json) ⇒ Object



208
209
210
211
212
213
214
# File 'lib/magick.rb', line 208

def import(data, format: :json)
  imported = ExportImport.import(data, adapter_registry || default_adapter_registry)
  FEATURES_MUTEX.synchronize do
    @features = (@features || {}).merge(imported)
  end
  imported
end

.most_used_features(limit: 10) ⇒ Object

Get most used features



314
315
316
# File 'lib/magick.rb', line 314

def most_used_features(limit: 10)
  performance_metrics&.most_used_features(limit: limit) || {}
end

.performance_metricsObject

Getter for performance_metrics



82
83
84
# File 'lib/magick.rb', line 82

def performance_metrics
  @performance_metrics
end

.performance_metrics=(value) ⇒ Object

Override performance_metrics setter to auto-enable Redis tracking



56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/magick.rb', line 56

def performance_metrics=(value)
  @performance_metrics = value
  # Auto-enable Redis tracking if Redis adapter is available
  if value && adapter_registry.is_a?(Adapters::Registry) && adapter_registry.redis_available?
    value.enable_redis_tracking(enable: true)
  end
  # Update all existing features to enable performance metrics tracking
  if value
    features.each_value do |feature|
      feature.instance_variable_set(:@_perf_metrics_enabled, true)
    end
  end
  value
end

.preload!Object

Preload all features into memory cache in minimal queries. Call after configuration and feature registration to avoid per-feature cache misses.



320
321
322
323
324
325
326
327
328
329
330
331
332
333
# File 'lib/magick.rb', line 320

def preload!
  registry = adapter_registry || default_adapter_registry
  return unless registry

  # Bulk load all feature data into memory (1-2 queries total)
  all_data = registry.preload!

  # Also preload registered features' state from the cached data
  features.each_value do |feature|
    feature.reload
  end

  all_data
end

.register_feature(name, **options) ⇒ Object



133
134
135
136
137
138
139
# File 'lib/magick.rb', line 133

def register_feature(name, **options)
  feature = Feature.new(name, adapter_registry || default_adapter_registry, **options)
  FEATURES_MUTEX.synchronize do
    @features = (@features || {}).merge(name.to_s => feature)
  end
  feature
end

.reload_feature(feature_name) ⇒ Object

Reload a feature from the adapter (useful when feature is changed externally)



158
159
160
161
# File 'lib/magick.rb', line 158

def reload_feature(feature_name)
  feature = features[feature_name.to_s] || self[feature_name]
  feature.reload
end

.reset!Object



335
336
337
338
339
340
341
342
343
344
345
346
# File 'lib/magick.rb', line 335

def reset!
  safely_shutdown(@adapter_registry) { |r| r.shutdown }
  safely_shutdown(@default_adapter_registry) { |r| r.shutdown }
  @features = {}
  @adapter_registry = nil
  @default_adapter = nil
  @default_adapter_registry = nil
  @versioning = nil
  @audit_log = nil
  @versioning_enabled = nil
  @performance_metrics&.clear!
end

.shutdown!(timeout: 5) ⇒ Object

Gracefully terminate background threads (Redis Pub/Sub subscriber, async metrics processor) so the host process can exit promptly. Intended for use in Rails shutdown hooks, at_exit, or tests.



351
352
353
354
355
# File 'lib/magick.rb', line 351

def shutdown!(timeout: 5)
  safely_shutdown(@adapter_registry) { |r| r.shutdown(timeout: timeout) }
  safely_shutdown(@performance_metrics, &:stop_async_processor)
  true
end

.suppress_change_recordingObject

Reentrancy guard for Feature#record_change: the outermost public mutator records once; nested mutator calls (enable -> set_value) run silently so one logical operation never produces multiple entries.



266
267
268
269
270
271
272
# File 'lib/magick.rb', line 266

def suppress_change_recording
  previous = Thread.current[:magick_change_recording]
  Thread.current[:magick_change_recording] = true
  yield
ensure
  Thread.current[:magick_change_recording] = previous
end

.variant(feature_name, context = {}) ⇒ Object



167
168
169
170
# File 'lib/magick.rb', line 167

def variant(feature_name, context = {})
  feature = features[feature_name.to_s] || self[feature_name]
  feature.get_variant(context)
end

.variant_value(feature_name, context = {}) ⇒ Object



172
173
174
175
# File 'lib/magick.rb', line 172

def variant_value(feature_name, context = {})
  feature = features[feature_name.to_s] || self[feature_name]
  feature.get_variant_value(context)
end

.versioning_enabled?Boolean

Returns:

  • (Boolean)


225
226
227
# File 'lib/magick.rb', line 225

def versioning_enabled?
  @versioning_enabled.nil? || @versioning_enabled != false
end

.with_actor(actor) ⇒ Object

Attribute all changes made inside the block to the given actor. Audit entries pick it up as user_id and versions as created_by, unless the call site passes an explicit user_id:.

Magick.with_actor(current_user.id) { Magick[:checkout].enable }


234
235
236
237
238
239
240
# File 'lib/magick.rb', line 234

def with_actor(actor)
  previous = Thread.current[:magick_actor]
  Thread.current[:magick_actor] = actor
  yield
ensure
  Thread.current[:magick_actor] = previous
end