Skip to content

Emit telemetry when async results are applied - #4463

Open
dannote wants to merge 1 commit into
phoenixframework:mainfrom
dannote:handle-async-telemetry
Open

dannote wants to merge 1 commit into
phoenixframework:mainfrom
dannote:handle-async-telemetry

Conversation

@dannote

@dannote dannote commented Oct 3, 2026

Copy link
Copy Markdown

Problem

LiveView emits telemetry spans for the callbacks that change a LiveView's or LiveComponent's state: mount, handle_params and handle_event for LiveViews, update and handle_event for LiveComponents. Async results are the exception. When a start_async/4 result reaches handle_async/3, or an assign_async/4 or stream_async/4 result is assigned, nothing is emitted.

For LiveViews, the following [:phoenix, :live_view, :render] span at least carries the updated socket. For LiveComponents, the only signal is that render span, whose socket is the parent LiveView's. The component's new state can't be observed, and its change tracking is cleared by the render that follows.

On main, a single-file test that records every documented LiveView telemetry event shows the gap. A component loads data with assign_async/4 and start_async/4, and has a button:

async results: [
  {[:phoenix, :live_view, :render, :stop], Example.Price},
  {[:phoenix, :live_view, :render, :stop], Example.Price}
]
component click: [
  {[:phoenix, :live_component, :handle_event, :stop], Example.Price},
  {[:phoenix, :live_view, :render, :stop], Example.Price}
]

Motivation

I maintain PhoenixReplay, which records LiveView sessions and replays them by re-rendering the application's own templates with the recorded assigns. LiveComponents have no on_mount hook, so it records component state from the documented component telemetry (update, handle_event, destroyed), with no changes to user components.

A component that loads its data with assign_async/4 is recorded as loading for the rest of the session: the result is applied without telemetry, and the render that follows clears the component's __changed__. Loading data asynchronously is the recommended pattern for components, so this is common. The only workaround I found is to call Phoenix.LiveView.Debug.live_components/1 from another process after each component render that no event explains. That copies every component's assigns, and it uses a debugging API for routine work.

Beyond replay tools, these spans make async work observable like the other callbacks:

  • how long applying a result takes,
  • which results arrive as {:exit, reason},
  • exceptions raised in handle_async/3, which currently surface only as a crash.

Change

Phoenix.LiveView.Async.handle_async/6 is where every async result is applied, for LiveViews and LiveComponents, after stale results are discarded. It now wraps applying a result in a span:

  • [:phoenix, :live_view, :handle_async, :start | :stop | :exception]
  • [:phoenix, :live_component, :handle_async, :start | :stop | :exception]

Measurements and metadata follow the handle_event spans. Metadata is socket, plus name, the name given to start_async/4 or stream_async/4 or the keys given to assign_async/4, and type, which is :start, :assign or :stream. Component events also include component. The :stop socket is the socket after the result was applied. :exception adds kind and reason, so type is used rather than kind for the async kind.

Stale results, which LiveView already ignores (for example after cancel_async/3 or a newer start_async/4 with the same name), emit nothing.

The telemetry guide documents the six events.

Tests

telemetry_test.exs covers LiveView and LiveComponent spans for start_async and assign_async results, and the :exception event when handle_async/3 raises. The last case adds one scenario to StartAsyncLive. The full Elixir suite passes.


I know feature proposals usually start on the forum. I went with a small PR because the change mirrors the existing spans closely; I'm happy to move the discussion there, rename the events, or change the metadata.

Add [:phoenix, :live_view, :handle_async] and
[:phoenix, :live_component, :handle_async] spans around applying
start_async, assign_async and stream_async results, mirroring the
handle_event spans. Stale results that are ignored emit nothing.
@SteffenDE

Copy link
Copy Markdown
Member

which results arrive as {:exit, reason}

For start_async, that's not the case, since the result is not passed as metadata. Maybe we should add result as well?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants