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/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/feature_dependency.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, Rails, RequestStoreIntegration, Targeting, TestingHelpers Classes: AdapterError, AuditLog, CircuitBreaker, Config, Documentation, Error, ExportImport, Feature, FeatureDependency, FeatureNotFoundError, FeatureVariant, InvalidFeatureTypeError, InvalidFeatureValueError, PerformanceMetrics, Versioning

Constant Summary collapse

VERSION =
'1.3.2'

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.adapter_registryObject

Returns the value of attribute adapter_registry.



47
48
49
# File 'lib/magick.rb', line 47

def adapter_registry
  @adapter_registry
end

.audit_logObject

Returns the value of attribute audit_log.



47
48
49
# File 'lib/magick.rb', line 47

def audit_log
  @audit_log
end

.default_adapterObject

Returns the value of attribute default_adapter.



47
48
49
# File 'lib/magick.rb', line 47

def default_adapter
  @default_adapter
end

.versioningObject

Returns the value of attribute versioning.



47
48
49
# File 'lib/magick.rb', line 47

def versioning
  @versioning
end

.warn_on_deprecatedObject

Returns the value of attribute warn_on_deprecated.



47
48
49
# File 'lib/magick.rb', line 47

def warn_on_deprecated
  @warn_on_deprecated
end

Class Method Details

.[](feature_name) ⇒ Object



112
113
114
115
# File 'lib/magick.rb', line 112

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



175
176
177
178
179
180
181
# File 'lib/magick.rb', line 175

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



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

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

.configure(&block) ⇒ Object



81
82
83
84
85
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
# File 'lib/magick.rb', line 81

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

.default_adapter_registryObject

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



278
279
280
281
282
283
284
285
286
287
288
# File 'lib/magick.rb', line 278

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

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

Returns:

  • (Boolean)


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

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

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

Returns:

  • (Boolean)


139
140
141
# File 'lib/magick.rb', line 139

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



206
207
208
209
210
# File 'lib/magick.rb', line 206

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)


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

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)


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

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)


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

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

.export(format: :json) ⇒ Object



183
184
185
186
187
188
189
190
191
192
# File 'lib/magick.rb', line 183

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)



233
234
235
236
237
# File 'lib/magick.rb', line 233

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)



213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/magick.rb', line 213

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



228
229
230
# File 'lib/magick.rb', line 228

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

.featuresObject



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

def features
  @features ||= {}
end

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



194
195
196
197
198
# File 'lib/magick.rb', line 194

def import(data, format: :json)
  imported = ExportImport.import(data, adapter_registry || default_adapter_registry)
  imported.each { |name, feature| features[name] = feature }
  imported
end

.most_used_features(limit: 10) ⇒ Object

Get most used features



240
241
242
# File 'lib/magick.rb', line 240

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

.performance_metricsObject

Getter for performance_metrics



77
78
79
# File 'lib/magick.rb', line 77

def performance_metrics
  @performance_metrics
end

.performance_metrics=(value) ⇒ Object

Override performance_metrics setter to auto-enable Redis tracking



51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/magick.rb', line 51

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.



246
247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/magick.rb', line 246

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



121
122
123
124
125
# File 'lib/magick.rb', line 121

def register_feature(name, **options)
  feature = Feature.new(name, adapter_registry || default_adapter_registry, **options)
  features[name.to_s] = feature
  feature
end

.reload_feature(feature_name) ⇒ Object

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



144
145
146
147
# File 'lib/magick.rb', line 144

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

.reset!Object



261
262
263
264
265
266
# File 'lib/magick.rb', line 261

def reset!
  @features = {}
  @adapter_registry = nil
  @default_adapter = 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.



271
272
273
274
275
# File 'lib/magick.rb', line 271

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

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



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

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



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

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