phoenix_ex_ratatui emits :telemetry events at the boundaries the LiveView integration itself controls. They sit one layer above the events ex_ratatui already emits — together they give a complete profile of a browser-driven TUI without double-counting any single operation.

Event tree at a glance

[:phoenix_ex_ratatui, :transport, :connect]    span     mount + Transport boot (first full frame)
[:phoenix_ex_ratatui, :transport, :disconnect] single   Transport.stop/2 teardown
[:phoenix_ex_ratatui, :render, :frame]         span     diff encoded to JSON + push_event
[:phoenix_ex_ratatui, :input, :forward]        single   decoded client input  runtime
[:phoenix_ex_ratatui, :intent, :dispatch]      single   runtime intent  push_navigate and friends

Span events emit :start / :stop / :exception suffixes. Most handlers only attach to :stop for timing and :exception for failures.

See PhoenixExRatatui.Telemetry for the full metadata reference.

Quick start: log every event

# in MyApp.Application.start/2, or an IEx session:
PhoenixExRatatui.Telemetry.attach_default_logger(level: :info)

Detach with PhoenixExRatatui.Telemetry.detach_default_logger/0.

Wiring Telemetry.Metrics

If Telemetry.Metrics is already running (e.g. behind a LiveDashboard), add these alongside whatever ex_ratatui metrics matter:

defmodule MyApp.Telemetry do
  import Telemetry.Metrics

  def metrics do
    [
      # Time-to-first-frame on mount.
      summary("phoenix_ex_ratatui.transport.connect.stop.duration",
        unit: {:native, :millisecond}
      ),

      # How many sessions opened / closed.
      counter("phoenix_ex_ratatui.transport.disconnect"),

      # Per-frame encode + push cost (typically microseconds).
      summary("phoenix_ex_ratatui.render.frame.stop.duration",
        unit: {:native, :microsecond}
      ),

      # Client inputs flowing browser → server.
      counter("phoenix_ex_ratatui.input.forward"),

      # Navigation intents dispatched into the socket.
      counter("phoenix_ex_ratatui.intent.dispatch")
    ]
  end
end

Pairing with ex_ratatui's events

The two trees are complementary, not duplicative:

ConcernOwned by
mount/1 runtime, App handle_event/2, render command building[:ex_ratatui, ...]
Mount + Transport boot, diff-to-JSON encode + push_event/3 over the socket, client input forwarding, intent dispatch[:phoenix_ex_ratatui, ...]

Attach to whichever is needed. A typical setup attaches [:ex_ratatui, :runtime, :event, :stop] for App-level latency and [:phoenix_ex_ratatui, :render, :frame, :stop] for the wire cost — together they show where time is going without instrumenting either layer manually.

Custom handlers

The public helpers PhoenixExRatatui.Telemetry.span/3 and PhoenixExRatatui.Telemetry.execute/3 are thin wrappers around :telemetry.span/3 and :telemetry.execute/3 that prepend :phoenix_ex_ratatui to the event name. Use them when building a higher-level wrapper around the macros and emitting events under the same namespace.

For one-off handlers, attach directly:

:telemetry.attach(
  "my-app-frame-tracker",
  [:phoenix_ex_ratatui, :render, :frame, :stop],
  &MyApp.TuiTracker.handle_frame/4,
  nil
)

Always use a captured module function (&MyApp.TuiTracker.handle_frame/4), not an anonymous function — :telemetry logs a performance warning otherwise.