How to integrate appsignal with phoenix liveview?

I’m wrapping my Appsignal instrumentation and everything works fine.

def handle_event("submit", %{"waitlist" => params}, socket) do
  live_view_action(__MODULE__, "submit", socket, fn ->
    {:noreply, socket}
  end)
end

I wanted to move this to a decorator so I could do something like this:

@decorate instrument("submit")
def handle_event("submit", %{"waitlist" => params}, socket) do
  {:noreply, socket}
end

# And in my decorator:

import Appsignal.Phoenix.LiveView, only: [live_view_action: 4]

def get_socket(context) do
  # Context looks like this:
  # %Decorator.Decorate.Context{
  #   args: [
  #     "submit",
  #     {:%{}, [line: 23], [{"waitlist", {:params, [line: 23], nil}}]},
  #     {:socket, [line: 23], nil}
  #   ],
  #   arity: 3,
  #   module: HelloWorld.ComingSoonLive,
  #   name: :handle_event
  # }

  Enum.find(context.args, fn arg ->
    [{_, type} | _] = IEx.Info.info(arg)

    if type == "tuple" && elem(arg, 0) == :socket do
      arg
    end
  end)
end

def instrument(name, body, context) do
  quote do
    socket = unquote(get_socket(context))

    live_view_action(unquote(context.module), unquote(name), socket, fn ->
      unquote(body)
    end)
  end
end

socket is always nil in the context at compilation time so I can’t even run my project.

How can I use the value of socket at runtime where I know the socket won’t be nil?

3 Likes

Instead of either of these, I would highly suggest the new Phoenix.LiveView.on_mount/1 function combined with Phoenix.LiveView.attach_hook/4

On mount, attach a hook for handle_event, then instrument.

4 Likes

This looks much better, I’ll post the working solution once I figure it out. Thank you Ben!

2 Likes

Thanks again @benwilson512 - here’s a guide for posterity, hope it helps someone save a day or two.


Right, you want to use Appsignal to monitor your Phoenix Liveview application? Here’s how you do it.

Note: This guide assumes you’re on Liveview 0.17.5 or newer because we depend on the new liveview event hooks.

First install the appsignal_phoenix package. Because this package already depends on the appsignal package, there’s no need for you to manually add it.

defp deps do
  [
    {:appsignal_phoenix, "~> 2.0"},

Then we’re going to hook into the lifecycle in two spots. One for the mount event, and one for the handle_event.

In your myproject_web.ex add two on_mount handlers.

  def live_view do
    quote do
      use Phoenix.LiveView,
        layout: {GamingWeb.LayoutView, "live.html"}

      on_mount(GamingWeb.InitAssigns)
      on_mount(GamingWeb.Appsignal)

      unquote(view_helpers())
    end
  end

Tracking mount events

Let’s start by creating the InitAssigns module.

defmodule GamingWeb.InitAssigns do
  @moduledoc false
  import Phoenix.LiveView
  import Appsignal.Phoenix.LiveView, only: [live_view_action: 5]

  def on_mount(:default, params, session, socket) do
    # here's where we're calling appsignal instrumentation
    live_view_action(socket.view, "mount", params, socket, fn ->
      socket =
        socket
        |> assign_new(:current_user, fn ->
          get_user(session["user_token"])
        end)

      {:cont, socket}
    end)
  end
end

We’re done, every mount will be tracked properly on Appsignal.

image.png

Tracking handle_event events

We’re going to track the handle_event lifecycle event. If you want you can track others like:

defmodule GamingWeb.Appsignal do
  @moduledoc false
  import Phoenix.LiveView
  import Appsignal.Phoenix.LiveView, only: [live_view_action: 5]

  def on_mount(:default, _params, _session, socket) do
    socket =
      attach_hook(socket, :mount_hook, :handle_event, fn
        event, params, socket ->
          live_view_action(socket.view, event, params, socket, fn ->
            {:cont, socket}
          end)
      end)

    {:cont, socket}
  end
end

And we’re done. Here I’m tracking a simple form event named submit.

def handle_event("submit", %{"waitlist" => params}, socket) do
  {:noreply, socket}
end

image.png

6 Likes

I tested your approach. Sadly, it doesn’t fully work. The attached hook runs before the real handle_event life cycle of your LiveView. So the live_view_action instrumentation is not wrapped around everything you do there (e.g. Ecto queries etc.), it only tracks the construction of the {:cont, socket} tuple.

2 Likes

You’re right. This isn’t tracking the query timings of the actual liveview mount function. Maybe someone can help. I’ll try to dig into this over the weekend.

1 Like

I just coded a GenServer that listens to phoenix liveview telemetry events to send them to AppSignal.
Here it is: A Phoenix Telemetry agent to monitor all LiveView events & errors · GitHub

It instruments all requests (with params, event name …) and also report errors with same metadata + stacktrace.

Still a last issue to solve: errors are reported twice to AppSignal (since the error is re-raised it is also catched by AppSignal background reporter)

2 Likes

Can you describe you how enable AppSignalTelemetry for your liveview app? Where do you set it? In the application list?

Yes add it as a child in application.ex

Interesting! This goes far beyond my current Elixir skills, so that’s nice to see.

1 Like

I would love to contribute this server to appsignal’s repository, but I need their help to fix the duplicate error issue

2 Likes

I thought about how to solve this without having to wrap AppSignal’s live_view_action function around the body of every handle_params and handle_event function in all of my live views. That is just redundant.

I came up with the following solution: in my Phoenix web module I define the import of live_view_action once in the live_view macro, and add macros instrumented_handle_params and instrumented_handle_event. They just delegate to do_handle_... methods, with live_view_action wrapped around it. In my live views, I simply have to use these macros, and rename my handle_... functions to do_handle_....

It looks like this:

defmodule MyPhoenixAppWeb do

  # [...]

  def live_view do
    quote do
      use Phoenix.LiveView,
        layout: {MyPhoenixAppWeb.LayoutView, "live.html"}

      import Appsignal.Phoenix.LiveView, only: [live_view_action: 4, live_view_action: 5]

      unquote(view_helpers())
    end
  end

  def instrumented_handle_params do
    quote do
      @impl true
      def handle_params(params, url, socket) do
        live_view_action(__MODULE__, "handle_params", params, socket, fn ->
          do_handle_params(params, url, socket)
        end)
      end
    end
  end

  def instrumented_handle_event do
    quote do
      @impl true
      def handle_event(event, params, socket) do
        live_view_action(__MODULE__, event, params, socket, fn ->
          do_handle_event(event, params, socket)
        end)
      end
    end
  end
  
  # [...]
  
end

defmodule MyPhoenixAppWeb.MyLive.Index do
  use MyPhoenixAppWeb, :live_view
  use MyPhoenixAppWeb, :instrumented_handle_params
  use MyPhoenixAppWeb, :instrumented_handle_event
  
  defp do_handle_params(params, _url, %{assigns: %{live_action: live_action}} = socket) do
    {:noreply,
    socket
    |> assign(filter_params: fetch_filter_params(params))
    |> apply_action(live_action, params)}
  end
  
  defp do_handle_event("my_event_name", %{"id" => id}, socket) do
    # [...]
  end
  
  # [...]
  
end

Achievements:

  • Call to AppSignal’s live_view_action only in one place
  • My live views only sparsely change: just add a line use MyPhoenixAppWeb, :instrumented_handle_params or use MyPhoenixAppWeb, :instrumented_handle_event at the top

The approach can be extended to also instrument the mount function, I just didn’t need it yet. Also, the approach can be extended to live components, then you want to instrument the update life cycle function, too.

This was a big help. I now see actual timings on AppSignal using your genserver example.

One thing I noticed is that everything is lopped into a big event in the timeline. I don’t see specific timings for our Ecto queries, or Elixir code.

How did you add support for that on your app?

1 Like

Thank you!

I actually don’t have a detailed breakdown of time spent in queries either. I will investigate to see if I can do something, tut it can be quite difficult because LiveView & Ecto are running in separate processes.

By the way, I just updated my gist, it’s now solving the duplicate error issue. You just need to use the latest appsignal from GitHub, because of this feature I needed:

1 Like

Have you already tried my approach, for comparison? It should give you the breakdown of Ecto queries, for example.

@sergio It’s now working properly with detailed breakdown, just updated my gist :wink:

3 Likes

bro you’re a beast! i’m going to try it out this AM after standup this looks promising!

1 Like

It worked out really well - amazing! :facepunch:

I wonder if there’s a way to stacktrace/measure the Elixir gap as shown in the graph here. Something is happening on the Elixir side that takes 300ms.

1 Like

I’m still wondering why the rendering part is not in the breakdown.
Something else to figure out :slight_smile:

My PR has been merged, Liveview instrumentation is now available in appsignal_phoenix :slight_smile:

5 Likes