Hi!
I’m almost 100% sure that it has to be a compatibility issue (I’m still on 1.6.16
) but README of GitHub - phoenixframework/phoenix_live_dashboard: Realtime dashboard with metrics, request logging, plus storage, OS and VM insights never said it is 1.7 only, so I’m assuming it should work
OK, here is the issue: trying to open a live dashboard fails with this exception:
May 07 10:35:17 phoenix[1722199]: Server: app.Bento.com:80 (http)
May 07 10:35:17 phoenix[1722199]: Request: GET /dashboard
May 07 10:35:17 phoenix[1722199]: ** (exit) an exception was raised:
May 07 10:35:17 phoenix[1722199]: ** (UndefinedFunctionError) function BentoWeb.Router.Helpers.live_dashboard_path/4 is undefined or private
May 07 10:35:17 phoenix[1722199]: (Bento 0.1.0) BentoWeb.Router.Helpers.live_dashboard_path(#Phoenix.LiveView.Socket<id: "phx-GD04SU0Yo7Caw9xE", endpoint: BentoWeb.Endpoint, view: Phoenix.LiveDashboard.PageLive, parent_pid: nil, root_pid: nil, router: BentoWeb.Router, assigns: %{__changed__: %{}, flash: %{}, live_action: :home}, transport_pid: nil, sticky?: false, ...>, :page, :home, %{})
May 07 10:35:17 phoenix[1722199]: (phoenix_live_dashboard 0.8.6) lib/phoenix/live_dashboard/page_live.ex:369: Phoenix.LiveDashboard.PageLive.redirect_to_current_node/1
May 07 10:35:17 phoenix[1722199]: (phoenix_live_dashboard 0.8.6) lib/phoenix/live_dashboard/page_live.ex:38: Phoenix.LiveDashboard.PageLive.mount/3
May 07 10:35:17 phoenix[1722199]: (phoenix_live_view 1.0.10) lib/phoenix_live_view/utils.ex:348: anonymous fn/6 in Phoenix.LiveView.Utils.maybe_call_live_view_mount!/5
May 07 10:35:17 phoenix[1722199]: (telemetry 1.3.0) /app/deps/telemetry/src/telemetry.erl:324: :telemetry.span/3
May 07 10:35:17 phoenix[1722199]: (phoenix_live_view 1.0.10) lib/phoenix_live_view/static.ex:321: Phoenix.LiveView.Static.call_mount_and_handle_params!/5
May 07 10:35:17 phoenix[1722199]: (phoenix_live_view 1.0.10) lib/phoenix_live_view/static.ex:155: Phoenix.LiveView.Static.do_render/4
May 07 10:35:17 phoenix[1722199]: (phoenix_live_view 1.0.10) lib/phoenix_live_view/controller.ex:39: Phoenix.LiveView.Controller.live_render/3
And indeed, when I check the routes I don’t see it there:
$ mix phx.routes
live_dashboard_asset_path GET /dashboard/css-:md5 Phoenix.LiveDashboard.Assets :css
live_dashboard_asset_path GET /dashboard/js-:md5 Phoenix.LiveDashboard.Assets :js
GET /dashboard Phoenix.LiveDashboard.PageLive :home
GET /dashboard/:page Phoenix.LiveDashboard.PageLive :page
GET /dashboard/:node/:page Phoenix.LiveDashboard.PageLive :page
websocket WS /live/websocket Phoenix.LiveView.Socket
longpoll GET /live/longpoll Phoenix.LiveView.Socket
longpoll POST /live/longpoll Phoenix.LiveView.Socket
websocket WS /socket/websocket BentoWeb.UserSocket
longpoll GET /socket/longpoll BentoWeb.UserSocket
longpoll POST /socket/longpoll BentoWeb.UserSocket
I’ve reduced router to just live_dashboard
defmodule BentoWeb.Router do
use BentoWeb, :router
import Phoenix.LiveDashboard.Router
pipeline :browser do
plug :accepts, ["html"]
plug :put_root_layout, {BentoWeb.LayoutView, :root}
end
scope "/", BentoWeb do
pipe_through :browser
live_dashboard "/dashboard", metrics: BentoWeb.Telemetry
end
end
Next step: I tried looking at the expanded code of Router
and Router.Helpers
:
$ mix archive.install github michalmuskala/decompile
$ mix decompile BentoWeb.Router --to expanded
$ mix decompile BentoWeb.Router.Helpers --to expanded
defmodule BentoWeb.Router do
...
def __routes__() do
[
%{
helper: "live_dashboard_asset",
metadata: %{log: :debug},
path: "/dashboard/css-:md5",
plug: Phoenix.LiveDashboard.Assets,
plug_opts: :css,
verb: :get
},
%{
helper: "live_dashboard_asset",
metadata: %{log: :debug},
path: "/dashboard/js-:md5",
plug: Phoenix.LiveDashboard.Assets,
plug_opts: :js,
verb: :get
},
%{
helper: nil,
metadata: %{
log: :debug,
log_module: Phoenix.LiveDashboard.PageLive,
mfa: {Phoenix.LiveDashboard.PageLive, :__live__, 0},
phoenix_live_view:
{Phoenix.LiveDashboard.PageLive, :home,
[action: :home, router: BentoWeb.Router, as: :live_dashboard],
%{
extra: %{
on_mount: [],
root_layout: {Phoenix.LiveDashboard.LayoutView, :dash},
session:
{Phoenix.LiveDashboard.Router, :__session__,
[
nil,
{"Dashboard", :phoenix_live_dashboard},
false,
{BentoWeb.Telemetry, :metrics},
nil,
[],
{true, nil},
nil,
[],
[],
[],
nil
]}
},
name: :live_dashboard
}}
},
path: "/dashboard",
plug: Phoenix.LiveView.Plug,
plug_opts: :home,
verb: :get
},
...
end
end
So, it is helper: nil
but why it is nil
?
This is how deps/phoenix_live_dashboard/lib/phoenix/live_dashboard/router.ex
defines options it will use when injected:
def __options__(options) do
...
{
options[:live_session_name] || :live_dashboard,
[
session: {__MODULE__, :__session__, session_args},
root_layout: {Phoenix.LiveDashboard.LayoutView, :dash},
on_mount: options[:on_mount] || nil
],
[
private: %{live_socket_path: live_socket_path, csp_nonce_assign_key: csp_nonce_assign_key},
as: :live_dashboard
]
}
end
I’ve added IO.inspect
and it gave me this
{:live_dashboard,
[
session: {Phoenix.LiveDashboard.Router, :__session__,
[
nil,
{"Dashboard", :phoenix_live_dashboard},
false,
{BentoWeb.Telemetry, :metrics},
nil,
[],
{true, nil},
nil,
[],
[],
[],
nil
]},
root_layout: {Phoenix.LiveDashboard.LayoutView, :dash},
on_mount: nil
],
[
private: %{live_socket_path: "/live", csp_nonce_assign_key: nil},
as: :live_dashboard
]}
So the code of live dashboard router does this
scope =
quote bind_quoted: binding() do
scope path, alias: false, as: false do
{session_name, session_opts, route_opts} =
Phoenix.LiveDashboard.Router.__options__(opts)
import Phoenix.Router, only: [get: 4]
import Phoenix.LiveView.Router, only: [live: 4, live_session: 3]
live_session session_name, session_opts do
# LiveDashboard assets
get "/css-:md5", Phoenix.LiveDashboard.Assets, :css, as: :live_dashboard_asset
get "/js-:md5", Phoenix.LiveDashboard.Assets, :js, as: :live_dashboard_asset
# All helpers are public contracts and cannot be changed
live "/", Phoenix.LiveDashboard.PageLive, :home, route_opts
live "/:page", Phoenix.LiveDashboard.PageLive, :page, route_opts
live "/:node/:page", Phoenix.LiveDashboard.PageLive, :page, route_opts
end
end
end
and route_opts
is the last element, so it has to be
[
private: %{live_socket_path: "/live", csp_nonce_assign_key: nil},
as: :live_dashboard
]
as: :live_dashboard
, right, so why helper is nil
?
EDIT:
relevant part of mix.lock
"phoenix": {:hex, :phoenix, "1.6.16", ...
"phoenix_ecto": {:hex, :phoenix_ecto, "4.6.3", ...
"phoenix_html": {:hex, :phoenix_html, "3.3.4", ...
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.0", ...
"phoenix_live_view": {:hex, :phoenix_live_view, "1.0.11", ...
EDIT2:
I forgot the stack trace
def mount(_params, _session, socket) do
{:ok, redirect_to_current_node(socket)}
end
which calls
defp redirect_to_current_node(socket) do
push_navigate(socket, to: PageBuilder.live_dashboard_path(socket, :home, node(), %{}, %{}))
end
which in turn calls this bad boy
def live_dashboard_path(socket, route, node, old_params, new_params) when is_atom(node) do
if function_exported?(socket.router, :__live_dashboard_prefix__, 0) do
new_params = for {key, val} <- new_params, key not in ~w(page node), do: {key, val}
prefix = socket.router.__live_dashboard_prefix__()
path =
if node == node() and is_nil(old_params["node"]) do
"#{prefix}/#{route}"
else
"#{prefix}/#{URI.encode_www_form(to_string(node))}/#{route}"
end
Phoenix.VerifiedRoutes.unverified_path(socket, socket.router, path, new_params)
else
apply(
socket.router.__helpers__(),
:live_dashboard_path,
if node == node() and is_nil(old_params["node"]) do
[socket, :page, route, new_params]
else
[socket, :page, node, route, new_params]
end
)
end
end
and since we are on 1.6
it has to be this part
apply(
socket.router.__helpers__(),
:live_dashboard_path,
if node == node() and is_nil(old_params["node"]) do
[socket, :page, route, new_params]
else
[socket, :page, node, route, new_params]
end
)