Module: StyleCapsule::Instrumentation

Defined in:
lib/style_capsule/instrumentation.rb

Overview

Centralized instrumentation module for StyleCapsule

Provides efficient, non-blocking instrumentation using ActiveSupport::Notifications. Metrics (time, size) are only calculated when subscribers are present, ensuring zero performance impact when instrumentation is not being used.

All events follow Rails naming convention: style_capsule.module.event

Examples:

Subscribing to CSS processing events

ActiveSupport::Notifications.subscribe("style_capsule.css_processor.scope") do |name, start, finish, id, payload|
  duration_ms = (finish - start) * 1000
  # input_size is derived from :css_content when subscribers exist
  input_size = payload[:input_size]
  Rails.logger.info "CSS scoped in #{duration_ms}ms, input: #{input_size} bytes"
end

Using Event object for more details

ActiveSupport::Notifications.subscribe("style_capsule.css_processor.scope") do |event|
  Rails.logger.info "CSS scoped in #{event.duration}ms, input: #{event.payload[:input_size]} bytes"
end

Subscribing to file write events

ActiveSupport::Notifications.subscribe("style_capsule.css_file_writer.write") do |name, start, finish, id, payload|
  StatsD.timing("style_capsule.write.duration", (finish - start) * 1000)
  StatsD.histogram("style_capsule.write.size", payload[:size])
end

Class Method Summary collapse

Class Method Details

.available?Boolean

Check if ActiveSupport::Notifications is available

Returns:

  • (Boolean)


34
35
36
# File 'lib/style_capsule/instrumentation.rb', line 34

def self.available?
  !!defined?(ActiveSupport::Notifications)
end

.instrument(event_name, payload = {}) { ... } ⇒ Object

Instrument an operation with automatic timing and size metrics

This method is highly efficient:

  • Only measures time if subscribers exist (ActiveSupport::Notifications is optimized for this)
  • Only calculates sizes if subscribers exist (lazy evaluation)
  • Uses monotonic time for accurate measurements

Examples:

Instrumenting CSS processing

result = Instrumentation.instrument(
  "style_capsule.css_processor.scope",
  component_class: component_class.name,
  capsule_id: capsule_id,
  strategy: :selector_patching,
  css_content: css_content
) do
  CssProcessor.scope_selectors(css_content, capsule_id)
end
# When subscribers exist, :input_size is set from :css_content before the block runs.

Parameters:

  • event_name (String)

    Event name following Rails convention (e.g., "style_capsule.css_processor.scope")

  • payload (Hash) (defaults to: {})

    Additional payload data

Yields:

  • The operation to instrument

Yield Returns:

  • (Object)

    Result of the operation (used for output size calculation if needed)

Returns:

  • (Object)

    Return value of the block



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/style_capsule/instrumentation.rb', line 61

def self.instrument(event_name, payload = {}, &block)
  return yield unless available?

  # Check if there are any subscribers (ActiveSupport optimizes this check)
  # If no subscribers, just execute the block without any overhead
  unless ActiveSupport::Notifications.notifier.listening?(event_name)
    return yield
  end

  apply_input_size!(payload)

  # Use ActiveSupport::Notifications.instrument which automatically:
  # - Measures duration using monotonic time (available in event.duration)
  # - Only does work if subscribers exist (zero overhead if no subscribers)
  # Note: output_size is not included in payload (subscribers can calculate from result if needed)
  result = nil
  ActiveSupport::Notifications.instrument(event_name, payload) do
    result = yield
    result
  end

  result
end

.instrument_css_processing(strategy:, component_class:, capsule_id:, css_content:) { ... } ⇒ String

Instrument CSS processing operations

Parameters:

  • strategy (Symbol)

    Scoping strategy (:selector_patching or :nesting)

  • component_class (Class, String)

    Component class

  • capsule_id (String)

    Capsule ID

  • css_content (String)

    CSS content (:input_size derived lazily when subscribed)

Yields:

  • The CSS processing operation

Returns:

  • (String)

    Processed CSS



124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/style_capsule/instrumentation.rb', line 124

def self.instrument_css_processing(strategy:, component_class:, capsule_id:, css_content:, &block)
  component_name = component_class.is_a?(Class) ? component_class.name : component_class.to_s

  instrument(
    "style_capsule.css_processor.scope",
    strategy: strategy,
    component_class: component_name,
    capsule_id: capsule_id,
    css_content: css_content
  ) do
    result = yield
    result
  end
end

.instrument_fallback(component_class:, capsule_id:, original_path:, fallback_path:, exception:, exception_object:) ⇒ void

This method returns an undefined value.

Instrument fallback to temporary directory

Parameters:

  • component_class (Class)

    Component class

  • capsule_id (String)

    Capsule ID

  • original_path (String)

    Original file path that failed

  • fallback_path (String)

    Fallback file path used

  • exception (Array<String>)

    Exception [class_name, message]

  • exception_object (Exception)

    The exception object



168
169
170
171
172
173
174
175
176
177
178
# File 'lib/style_capsule/instrumentation.rb', line 168

def self.instrument_fallback(component_class:, capsule_id:, original_path:, fallback_path:, exception:, exception_object:)
  notify(
    "style_capsule.css_file_writer.fallback",
    component_class: component_class.name,
    capsule_id: capsule_id,
    original_path: original_path,
    fallback_path: fallback_path,
    exception: exception,
    exception_object: exception_object
  )
end

.instrument_fallback_failure(component_class:, capsule_id:, original_path:, fallback_path:, original_exception:, original_exception_object:, fallback_exception:, fallback_exception_object:) ⇒ void

This method returns an undefined value.

Instrument fallback failure (both original and fallback failed)

Parameters:

  • component_class (Class)

    Component class

  • capsule_id (String)

    Capsule ID

  • original_path (String)

    Original file path that failed

  • fallback_path (String)

    Fallback file path that also failed

  • original_exception (Array<String>)

    Original exception [class_name, message]

  • original_exception_object (Exception)

    Original exception object

  • fallback_exception (Array<String>)

    Fallback exception [class_name, message]

  • fallback_exception_object (Exception)

    Fallback exception object



191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/style_capsule/instrumentation.rb', line 191

def self.instrument_fallback_failure(component_class:, capsule_id:, original_path:, fallback_path:, original_exception:, original_exception_object:, fallback_exception:, fallback_exception_object:)
  notify(
    "style_capsule.css_file_writer.fallback_failure",
    component_class: component_class.name,
    capsule_id: capsule_id,
    original_path: original_path,
    fallback_path: fallback_path,
    original_exception: original_exception,
    original_exception_object: original_exception_object,
    fallback_exception: fallback_exception,
    fallback_exception_object: fallback_exception_object
  )
end

.instrument_file_write(component_class:, capsule_id:, file_path:, size:) { ... } ⇒ Object

Instrument CSS file write operations

Parameters:

  • component_class (Class)

    Component class

  • capsule_id (String)

    Capsule ID

  • file_path (String)

    File path

  • size (Integer)

    CSS content size in bytes

Yields:

  • The file write operation

Returns:

  • (Object)

    Return value of the block



147
148
149
150
151
152
153
154
155
156
157
# File 'lib/style_capsule/instrumentation.rb', line 147

def self.instrument_file_write(component_class:, capsule_id:, file_path:, size:, &block)
  instrument(
    "style_capsule.css_file_writer.write",
    component_class: component_class.name,
    capsule_id: capsule_id,
    file_path: file_path,
    size: size
  ) do
    yield
  end
end

.instrument_registration(namespace:, file_path: nil, inline_size: nil, cache_strategy: :none) { ... } ⇒ Object

Instrument stylesheet registration

Parameters:

  • namespace (Symbol, String)

    Namespace

  • file_path (String, nil) (defaults to: nil)

    File path (if file-based)

  • inline_size (Integer, nil) (defaults to: nil)

    Inline CSS size in bytes (if inline)

  • cache_strategy (Symbol) (defaults to: :none)

    Cache strategy

Yields:

  • The registration operation

Returns:

  • (Object)

    Return value of the block



232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/style_capsule/instrumentation.rb', line 232

def self.instrument_registration(namespace:, file_path: nil, inline_size: nil, cache_strategy: :none, &block)
  payload = {
    namespace: namespace.to_s,
    cache_strategy: cache_strategy
  }
  payload[:file_path] = file_path if file_path
  payload[:inline_size] = inline_size if inline_size

  instrument(
    "style_capsule.stylesheet_registry.register",
    payload
  ) do
    yield
  end
end

.instrument_write_failure(component_class:, capsule_id:, file_path:, exception:, exception_object:) ⇒ void

This method returns an undefined value.

Instrument write failure (non-permission errors)

Parameters:

  • component_class (Class)

    Component class

  • capsule_id (String)

    Capsule ID

  • file_path (String)

    File path that failed

  • exception (Array<String>)

    Exception [class_name, message]

  • exception_object (Exception)

    The exception object



213
214
215
216
217
218
219
220
221
222
# File 'lib/style_capsule/instrumentation.rb', line 213

def self.instrument_write_failure(component_class:, capsule_id:, file_path:, exception:, exception_object:)
  notify(
    "style_capsule.css_file_writer.write_failure",
    component_class: component_class.name,
    capsule_id: capsule_id,
    file_path: file_path,
    exception: exception,
    exception_object: exception_object
  )
end

.notify(event_name, payload = {}) ⇒ void

This method returns an undefined value.

Instrument an event without a block (for events that don't have duration)

Examples:

Instrumenting a fallback event

Instrumentation.notify(
  "style_capsule.css_file_writer.fallback",
  component_class: component_class.name,
  original_path: original_path,
  fallback_path: fallback_path,
  exception: [e.class.name, e.message],
  exception_object: e
)

Parameters:

  • event_name (String)

    Event name

  • payload (Hash) (defaults to: {})

    Payload data



107
108
109
110
111
112
113
114
# File 'lib/style_capsule/instrumentation.rb', line 107

def self.notify(event_name, payload = {})
  return unless available?

  # Only notify if subscribers exist
  return unless ActiveSupport::Notifications.notifier.listening?(event_name)

  ActiveSupport::Notifications.instrument(event_name, payload)
end