defmodule TimeTravel.TelemetryHandler do alias TimeTravel.Jumper # Available LiveView and LiveComponent Telemetry events _live_view_names = [ [:phoenix, :live_view, :mount, :start], [:phoenix, :live_view, :mount, :stop], [:phoenix, :live_view, :handle_params, :start], [:phoenix, :live_view, :handle_params, :stop], [:phoenix, :live_view, :handle_event, :start], [:phoenix, :live_view, :handle_event, :stop] ] _live_component_names = [ [:phoenix, :live_component, :handle_event, :start], [:phoenix, :live_component, :handle_event, :stop] ] require Logger def live_view_event_handler(name, measurement, metadata, context) do event_name = name |> Enum.map(&to_string/1) |> Enum.map(&String.replace(&1, "_", " ")) |> Enum.map_join(" ", &String.capitalize/1) # Positive: no negative number keys # monotonic: always increasing so the list can be sorted time = System.unique_integer([:positive, :monotonic]) # Time key is cast to a string separately so that "time" stays an integer in the JSON response # and makes the retrieval of indices easier time_key = to_string(time) # Store original assigns in our server keys_and_assigns = [metadata.socket.id, time_key, Map.delete(metadata.socket.assigns, :flash)] Jumper.set(keys_and_assigns) {:ok, clean_assigns} = metadata.socket.assigns |> safe_assigns() |> Jason.encode() {:ok, event_args} = name |> Enum.at(2) |> event_args(metadata) |> safe_assigns() |> Jason.encode() # Send clean assigns to be inspected through the socket metadata.socket.endpoint.broadcast("lvdbg:#{metadata.socket.id}", "SaveAssigns", %{ payload: clean_assigns, time: time, event_name: event_name, event_args: event_args, socket_id: metadata.socket.id, component: metadata[:component], component_pid: Base.encode64(:erlang.term_to_binary(metadata.socket.root_pid)) }) end defp event_args(:mount, metadata), do: Map.take(metadata, [:params, :session]) defp event_args(:handle_event, metadata), do: Map.take(metadata, [:params, :uri]) defp event_args(:handle_params, metadata), do: Map.take(metadata, [:params, :uri]) # Make assigns JSON serializable def safe_assigns(assigns) when is_map(assigns) do Enum.reduce(assigns, %{}, fn {k, v}, acc -> Map.put(acc, k, safe_assign(v)) end) end def safe_assigns(assigns) when is_list(assigns) do Enum.map(assigns, &safe_assign/1) end def safe_assign(%Ecto.Association.NotLoaded{} = v), do: inspect(v) def safe_assign(%DateTime{} = v), do: DateTime.to_iso8601(v) def safe_assign(%Date{} = v), do: Date.to_iso8601(v) def safe_assign(v) when is_function(v), do: inspect(v) def safe_assign(v) when is_tuple(v), do: v |> Tuple.to_list() |> safe_assigns() def safe_assign(v) when is_pid(v), do: inspect(v) def safe_assign(v) when is_binary(v) do case String.valid?(v) do true -> v _ -> inspect(v) end end def safe_assign(v) when is_bitstring(v), do: inspect(v) def safe_assign(v) when is_struct(v) do v |> Map.delete(:__meta__) |> Map.from_struct() |> safe_assigns() end def safe_assign(v) when is_map(v), do: safe_assigns(v) def safe_assign(v) when is_list(v), do: safe_assigns(v) def safe_assign(v), do: v end