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
-
.adapter_registry ⇒ Object
Returns the value of attribute adapter_registry.
-
.audit_log ⇒ Object
Returns the value of attribute audit_log.
-
.default_adapter ⇒ Object
Returns the value of attribute default_adapter.
-
.versioning ⇒ Object
Returns the value of attribute versioning.
-
.versioning_enabled ⇒ Object
writeonly
When false, Feature#record_change skips version snapshots (audit log entries are still written).
-
.warn_on_deprecated ⇒ Object
Returns the value of attribute warn_on_deprecated.
Class Method Summary collapse
- .[](feature_name) ⇒ Object
- .bulk_disable(feature_names, _context = {}) ⇒ Object
- .bulk_enable(feature_names, _context = {}) ⇒ Object
- .change_recording_suppressed? ⇒ Boolean
- .configure(&block) ⇒ Object
- .current_actor ⇒ Object
-
.default_adapter_registry ⇒ Object
Get default adapter registry (public method for use by other classes).
-
.definition_mode ⇒ Object
Suppress audit/version recording while declarative feature definitions are (re)applied.
- .definition_mode? ⇒ Boolean
- .disabled?(feature_name, context = {}) ⇒ Boolean
- .disabled_for?(feature_name, object, **additional_context) ⇒ Boolean
-
.enable_redis_tracking(enable: true) ⇒ Object
Manually enable Redis tracking for performance metrics Useful if Redis adapter becomes available after initial configuration.
- .enabled?(feature_name, context = {}) ⇒ Boolean
- .enabled_for?(feature_name, object, **additional_context) ⇒ Boolean
- .exists?(feature_name) ⇒ Boolean
- .export(format: :json) ⇒ Object
-
.feature_average_duration(feature_name, operation: nil) ⇒ Object
Get average duration for a feature (optionally filtered by operation).
-
.feature_stats(feature_name) ⇒ Object
Get total usage count for a feature (combines memory and Redis counts).
-
.feature_usage_count(feature_name) ⇒ Object
Get usage count for a feature.
- .features ⇒ Object
- .import(data, format: :json) ⇒ Object
-
.most_used_features(limit: 10) ⇒ Object
Get most used features.
-
.performance_metrics ⇒ Object
Getter for performance_metrics.
-
.performance_metrics=(value) ⇒ Object
Override performance_metrics setter to auto-enable Redis tracking.
-
.preload! ⇒ Object
Preload all features into memory cache in minimal queries.
- .register_feature(name, **options) ⇒ Object
-
.reload_feature(feature_name) ⇒ Object
Reload a feature from the adapter (useful when feature is changed externally).
- .reset! ⇒ Object
-
.shutdown!(timeout: 5) ⇒ Object
Gracefully terminate background threads (Redis Pub/Sub subscriber, async metrics processor) so the host process can exit promptly.
-
.suppress_change_recording ⇒ Object
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.
- .variant(feature_name, context = {}) ⇒ Object
- .variant_value(feature_name, context = {}) ⇒ Object
- .versioning_enabled? ⇒ Boolean
-
.with_actor(actor) ⇒ Object
Attribute all changes made inside the block to the given actor.
Class Attribute Details
.adapter_registry ⇒ Object
Returns the value of attribute adapter_registry.
52 53 54 |
# File 'lib/magick.rb', line 52 def adapter_registry @adapter_registry end |
.audit_log ⇒ Object
Returns the value of attribute audit_log.
52 53 54 |
# File 'lib/magick.rb', line 52 def audit_log @audit_log end |
.default_adapter ⇒ Object
Returns the value of attribute default_adapter.
52 53 54 |
# File 'lib/magick.rb', line 52 def default_adapter @default_adapter end |
.versioning ⇒ Object
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_deprecated ⇒ Object
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
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_actor ⇒ Object
242 243 244 |
# File 'lib/magick.rb', line 242 def current_actor Thread.current[:magick_actor] end |
.default_adapter_registry ⇒ Object
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_mode ⇒ Object
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
259 260 261 |
# File 'lib/magick.rb', line 259 def definition_mode? Thread.current[:magick_definition_mode] == true end |
.disabled?(feature_name, context = {}) ⇒ 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
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
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
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
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 |
.features ⇒ Object
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_metrics ⇒ Object
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, **) feature = Feature.new(name, adapter_registry || default_adapter_registry, **) 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_recording ⇒ Object
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
225 226 227 |
# File 'lib/magick.rb', line 225 def versioning_enabled? @versioning_enabled.nil? || @versioning_enabled != false end |
.with_actor(actor) ⇒ Object
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 |