Observability & APM
See reactive traffic at the component level: every action, render, and broadcast is an ActiveSupport::Notifications event, and one config line wires AppSignal, Sentry, or Datadog to name transactions and report errors with context.
The events#
Every reactive hot path emits an ActiveSupport::Notifications event under the phlex_reactive namespace, so any subscriber — an APM, a metric, your own log line — sees which component and action a request was, how long a render took, and how far a broadcast fanned out. Four events fire:
| Event | Fires | Payload |
|---|---|---|
action.phlex_reactive | once per action request | component, action, outcome |
render.phlex_reactive | per component render | component, bytesize |
broadcast.phlex_reactive | per broadcast_to call (Action Cable and pgbus) | component, stream_action, streamables |
defer.phlex_reactive | once per deferred render | component, outcome |
The action outcome is one of ok / denied_undeclared / invalid_token / not_found / unauthorized / unverified / error (an uncaught action-body crash). The defer outcome is ok / no_content / invalid_token / not_found / unauthorized / error.
invalid_token event has no trusted component name (the token did not verify), so it is omitted.Turnkey APM adapters#
Subscribing by hand (below) is the DIY path. For the common trackers there's a one-liner that both names the transaction and reports errors — so reactive traffic stops rolling into one blurry ActionsController#create transaction and a crash arrives at your tracker with context:
Phlex::Reactive.apm = :appsignal # or :sentry, :datadog, or a custom objectWith it set:
- Each reactive action shows in the APM as its own transaction/span —
Counter#increment, notPhlex::Reactive::ActionsController#create— tagged with the component, action, and outcome. - An action body that raises a genuine error (a 500, not a registered 4xx) is reported to the tracker with
component/actiontags, then re-raised unchanged so Rails' own error reporting still fires.
The SDK is runtime-detected — no gem dependency is added, the same optionality invariant as pgbus. If the named SDK isn't loaded, apm = logs one warning at boot and no-ops. A custom object works too — anything responding to record_action(payload, duration_ms) and record_error(error, payload):
# Any object with the two-method contract integrates any tool.
class MyStatsdApm
def record_action(payload, duration_ms)
StatsD.timing("reactive.#{payload[:outcome]}", duration_ms,
tags: ["component:#{payload[:component]}", "action:#{payload[:action]}"])
end
def record_error(error, payload)
StatsD.increment("reactive.error",
tags: ["component:#{payload[:component]}", "action:#{payload[:action]}"])
end
end
Phlex::Reactive.apm = MyStatsdApm.newafter_initialize), so the vendor SDK is loaded by then. Detection is deferred to that point on purpose — load order doesn't matter, and the setting can live anywhere in your initializer.Errors: reported with context, never swallowed#
A component action that raises a previously-uncaught error (a genuine 500 — not one of the registered 4xx cases like an authorization error) is now observed at the endpoint before it propagates. In order:
- the
action.phlex_reactiveevent is taggedoutcome: :error; - the exception is reported to the resolved APM adapter and every registered
on_action_errorhook, with the name-only{ component:, action: }context; - the
error_flashrenders (if configured), so the user sees a flash on a crash — 500s now flow through the same path the 4xx errors already used; - the error is re-raised unchanged, so Rails' own error reporting and your middleware fire exactly as before.
The status never changes, and the existing 4xx rescues are untouched. Every reporter is isolated: a broken reporter (or a raising error_flash lambda) can never turn one 500 into a different 500, or swallow the original.
For a tracker with no built-in adapter, report errors yourself — this hook fires on any uncaught action-body error, without choosing an apm symbol:
Phlex::Reactive.on_action_error do |error, ctx|
Honeybadger.notify(error, context: { component: ctx[:component], action: ctx[:action] })
endAnd to show the user a flash when an action crashes — the same hook the 4xx errors already use (kind is :error for a crash):
Phlex::Reactive.error_flash = ->(kind) { "Something went wrong — please retry." }Watch traffic in your own log (no APM)#
To watch reactive traffic in your log without an APM, flip on the bundled LogSubscriber (default off). It logs one compact line per event at DEBUG:
Phlex::Reactive.log_events = true
# [reactive] Counter#increment ok (3.1ms)
# [reactive] Counter#drop_table denied_undeclared (0.2ms)
# [reactive] render Counter 512B (0.9ms)
# [reactive] broadcast replace Counter →2 (1.4ms)
# [reactive] defer SlowTotals ok (18.2ms)Subscribe by hand#
The adapters are a convenience over the raw event stream. You can always subscribe directly, exactly as you would for any Rails notification:
ActiveSupport::Notifications.subscribe('action.phlex_reactive') do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
# event.payload => { component:, action:, outcome: }
# event.duration => ms
MyAPM.record("reactive.#{event.payload[:outcome]}", event.duration,
component: event.payload[:component], action: event.payload[:action])
end