Skip to content

Commit 066845e

Browse files
authored
Phoenix.LiveView.Debug (#3776)
* wip: Phoenix.LiveView.Debug * don't rely on :sys.get_state * list_liveviews and return more details
1 parent 656f094 commit 066845e

3 files changed

Lines changed: 255 additions & 0 deletions

File tree

lib/phoenix_live_view/channel.ex

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,25 @@ defmodule Phoenix.LiveView.Channel do
408408
{:noreply, register_entry_upload(state, from, info)}
409409
end
410410

411+
# Phoenix.LiveView.Debug.socket/1
412+
def handle_call({@prefix, :debug_get_socket}, _from, state) do
413+
{:reply, {:ok, state.socket}, state}
414+
end
415+
416+
# Phoenix.LiveView.Debug.live_components/1
417+
def handle_call(
418+
{@prefix, :debug_live_components},
419+
_from,
420+
%{components: {components, _, _}} = state
421+
) do
422+
component_info =
423+
Enum.map(components, fn {cid, {mod, id, assigns, private, _prints}} ->
424+
%{id: id, cid: cid, module: mod, assigns: assigns, children_cids: private.children_cids}
425+
end)
426+
427+
{:reply, {:ok, component_info}, state}
428+
end
429+
411430
def handle_call(msg, from, %{socket: socket} = state) do
412431
case socket.view.handle_call(msg, from, socket) do
413432
{:reply, reply, %Socket{} = new_socket} ->
@@ -1089,6 +1108,10 @@ defmodule Phoenix.LiveView.Channel do
10891108
with {:ok, %Session{view: view} = new_verified, route, url} <-
10901109
authorize_session(verified, endpoint, params),
10911110
{:ok, config} <- load_live_view(view) do
1111+
# TODO: replace with Process.put_label/2 when we require Elixir 1.17
1112+
Process.put(:"$process_label", {Phoenix.LiveView, view, phx_socket.topic})
1113+
Process.put(:"$phx_transport_pid", phx_socket.transport_pid)
1114+
10921115
verified_mount(
10931116
new_verified,
10941117
config,

lib/phoenix_live_view/debug.ex

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
defmodule Phoenix.LiveView.Debug do
2+
@moduledoc """
3+
Functions for runtime introspection and debugging of LiveViews.
4+
5+
This module provides utilities for inspecting and debugging LiveView processes
6+
at runtime. It allows you to:
7+
8+
* List all currently connected LiveViews
9+
* Check if a process is a LiveView
10+
* Get the socket of a LiveView process
11+
* Inspect LiveComponents rendered in a LiveView
12+
13+
## Examples
14+
15+
# List all LiveViews
16+
iex> Phoenix.LiveView.Debug.list_liveviews()
17+
[%{pid: #PID<0.123.0>, view: MyAppWeb.PostLive.Index, topic: "lv:12345678", transport_pid: #PID<0.122.0>}]
18+
19+
# Check if a process is a LiveView
20+
iex> Phoenix.LiveView.Debug.liveview_process?(pid(0,123,0))
21+
true
22+
23+
# Get the socket of a LiveView process
24+
iex> Phoenix.LiveView.Debug.socket(pid(0,123,0))
25+
{:ok, %Phoenix.LiveView.Socket{...}}
26+
27+
# Get information about LiveComponents
28+
iex> Phoenix.LiveView.Debug.live_components(pid(0,123,0))
29+
{:ok, [%{id: "component-1", module: MyAppWeb.PostLive.Index.Component1, ...}]}
30+
31+
"""
32+
33+
@doc """
34+
Returns a list of all currently connected LiveView processes (on the current node).
35+
36+
Each entry is a map with the following keys:
37+
38+
- `pid`: The PID of the LiveView process.
39+
- `view`: The module of the LiveView.
40+
- `topic`: The topic of the LiveView's channel.
41+
- `transport_pid`: The PID of the transport process.
42+
43+
The `transport_pid` can be used to group LiveViews on the same page.
44+
45+
## Examples
46+
47+
iex> list_liveviews()
48+
[%{pid: #PID<0.123.0>, view: MyAppWeb.PostLive.Index, topic: "lv:12345678", transport_pid: #PID<0.122.0>}]
49+
50+
"""
51+
def list_liveviews do
52+
for pid <- Process.list(), dict = lv_process_dict(pid), not is_nil(dict) do
53+
{Phoenix.LiveView, view, topic} = keyfind(dict, :"$process_label")
54+
%{pid: pid, view: view, topic: topic, transport_pid: keyfind(dict, :"$phx_transport_pid")}
55+
end
56+
end
57+
58+
defp keyfind(list, key) do
59+
case List.keyfind(list, key, 0) do
60+
{^key, value} -> value
61+
_ -> nil
62+
end
63+
end
64+
65+
defp lv_process_dict(pid) do
66+
# LiveViews set the "$process_label" to {Phoenix.LiveView, view, topic}
67+
with info when is_list(info) <- Process.info(pid, [:dictionary]),
68+
dictionary when not is_nil(dictionary) <- keyfind(info, :dictionary),
69+
label when not is_nil(label) <- keyfind(dictionary, :"$process_label"),
70+
{Phoenix.LiveView, view, topic} when is_atom(view) and is_binary(topic) <- label do
71+
dictionary
72+
else
73+
_ -> nil
74+
end
75+
end
76+
77+
@doc """
78+
Checks if the given pid is a LiveView process.
79+
80+
## Examples
81+
82+
iex> list_liveviews() |> Enum.at(0) |> Map.fetch!(:pid) |> liveview_process?()
83+
true
84+
85+
iex> liveview_process?(pid(0,456,0))
86+
false
87+
88+
"""
89+
def liveview_process?(pid) do
90+
not is_nil(lv_process_dict(pid))
91+
end
92+
93+
@doc """
94+
Returns the socket of the LiveView process.
95+
96+
## Examples
97+
98+
iex> list_liveviews() |> Enum.at(0) |> Map.fetch!(:pid) |> socket()
99+
{:ok, %Phoenix.LiveView.Socket{...}}
100+
101+
iex> socket(pid(0,123,0))
102+
{:error, :not_alive_or_not_a_liveview}
103+
104+
"""
105+
def socket(liveview_pid) do
106+
GenServer.call(liveview_pid, {:phoenix, :debug_get_socket})
107+
catch
108+
:exit, _ -> {:error, :not_alive_or_not_a_liveview}
109+
end
110+
111+
@doc """
112+
Returns a list with information about all LiveComponents rendered in the LiveView.
113+
114+
## Examples
115+
116+
iex> live_components(pid)
117+
{:ok,
118+
[
119+
%{
120+
id: "component-1",
121+
module: MyAppWeb.PostLive.Index.Component1,
122+
cid: 1,
123+
assigns: %{
124+
id: "component-1",
125+
__changed__: %{},
126+
flash: %{},
127+
myself: %Phoenix.LiveComponent.CID{cid: 1},
128+
...
129+
}
130+
}
131+
]}
132+
133+
"""
134+
def live_components(liveview_pid) do
135+
GenServer.call(liveview_pid, {:phoenix, :debug_live_components})
136+
rescue
137+
_ -> {:error, :not_alive_or_not_a_liveview}
138+
end
139+
end
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
defmodule Phoenix.LiveView.DebugTest do
2+
use ExUnit.Case, async: true
3+
4+
alias Phoenix.LiveView.Debug
5+
import Phoenix.LiveViewTest
6+
7+
@endpoint Phoenix.LiveViewTest.Support.Endpoint
8+
9+
defmodule TestLV do
10+
use Phoenix.LiveView
11+
12+
defmodule Component do
13+
use Phoenix.LiveComponent
14+
15+
def render(assigns) do
16+
~H"""
17+
<p>Hello</p>
18+
"""
19+
end
20+
end
21+
22+
def mount(_params, _session, socket) do
23+
{:ok, assign(socket, :hello, :world)}
24+
end
25+
26+
def render(assigns) do
27+
~H"""
28+
<div>
29+
<p>Hello</p>
30+
<.live_component id="component-1" module={Component} />
31+
</div>
32+
"""
33+
end
34+
end
35+
36+
describe "list_liveviews/0" do
37+
test "returns a list of all currently connected LiveView processes" do
38+
conn = Plug.Test.conn(:get, "/")
39+
{:ok, view, _} = live_isolated(conn, TestLV)
40+
live_views = Debug.list_liveviews()
41+
42+
assert is_list(live_views)
43+
assert lv = Enum.find(live_views, fn lv -> lv.pid == view.pid end)
44+
assert lv.view == TestLV
45+
assert lv.transport_pid
46+
assert lv.topic
47+
end
48+
end
49+
50+
describe "liveview_process?/1" do
51+
test "returns true if the given pid is a LiveView process" do
52+
conn = Plug.Test.conn(:get, "/")
53+
{:ok, view, _} = live_isolated(conn, TestLV)
54+
assert Debug.liveview_process?(view.pid)
55+
end
56+
end
57+
58+
describe "socket/1" do
59+
test "returns the socket of the given LiveView process" do
60+
conn = Plug.Test.conn(:get, "/")
61+
{:ok, view, _} = live_isolated(conn, TestLV)
62+
assert {:ok, socket} = Debug.socket(view.pid)
63+
assert socket.assigns.hello == :world
64+
end
65+
66+
test "returns an error if the given pid is not a LiveView process" do
67+
defmodule NotALiveView do
68+
use GenServer
69+
70+
def start_link(opts) do
71+
GenServer.start_link(__MODULE__, opts)
72+
end
73+
74+
def init(opts) do
75+
{:ok, opts}
76+
end
77+
end
78+
79+
pid = start_supervised!(NotALiveView)
80+
assert {:error, :not_alive_or_not_a_liveview} = Debug.socket(pid)
81+
end
82+
end
83+
84+
describe "live_components/1" do
85+
test "returns a list of all LiveComponents rendered in the given LiveView" do
86+
conn = Plug.Test.conn(:get, "/")
87+
{:ok, view, _} = live_isolated(conn, TestLV)
88+
89+
assert {:ok, [%{id: "component-1", module: TestLV.Component}]} =
90+
Debug.live_components(view.pid)
91+
end
92+
end
93+
end

0 commit comments

Comments
 (0)