Router.Helpers.live_dashboard_path/4 is undefined or private

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
      )

“solved” it by removing phoenix_live_dashboard package