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