Current section

Files

Jump to
live_debugger lib live_debugger structs trace function_trace.ex
Raw

lib/live_debugger/structs/trace/function_trace.ex

defmodule LiveDebugger.Structs.Trace.FunctionTrace do
@moduledoc """
This module provides a struct to represent a function call trace.
CID is optional - it is filled when trace comes from LiveComponent.
When trace has PID and CID it means that it comes from LiveComponent.
When trace has only PID it means that it comes from LiveView.
There cannot be a trace with CID and without PID.
"""
alias LiveDebugger.CommonTypes
alias LiveDebugger.Structs.Trace
alias LiveDebugger.Structs.Trace.ErrorTrace
defmodule SourceLocation do
@moduledoc false
defstruct [:source_file, :line]
@type t :: %__MODULE__{
source_file: String.t(),
line: non_neg_integer()
}
end
defstruct [
:id,
:pid,
:timestamp,
:module,
:function,
:arity,
:args,
:socket_id,
:transport_pid,
:cid,
:execution_time,
:type,
:return_value,
:error,
:source
]
@type t() :: %__MODULE__{
id: Trace.id(),
pid: pid(),
timestamp: non_neg_integer(),
module: module(),
function: atom(),
arity: non_neg_integer(),
args: list(),
socket_id: String.t(),
transport_pid: pid() | nil,
cid: CommonTypes.cid() | nil,
timestamp: non_neg_integer(),
execution_time: non_neg_integer() | nil,
type: :call | :return_from | :exception_from,
return_value: term() | nil,
error: ErrorTrace.t() | nil,
source: SourceLocation.t() | nil
}
@doc """
Creates a new trace struct.
"""
@spec new(integer(), module(), atom(), list(), pid(), :erlang.timestamp(), Keyword.t()) :: t()
def new(id, module, function, args, pid, timestamp, opts \\ []) do
socket_id = Keyword.get(opts, :socket_id, get_socket_id_from_args(args))
transport_pid = Keyword.get(opts, :transport_pid, get_transport_pid_from_args(args))
cid = Keyword.get(opts, :cid, get_cid_from_args(args))
type = Keyword.get(opts, :type, :call)
return_value = Keyword.get(opts, :return_value)
%__MODULE__{
id: id,
module: module,
function: function,
arity: length(args),
args: args,
socket_id: socket_id,
transport_pid: transport_pid,
pid: pid,
cid: cid,
timestamp: :timer.now_diff(timestamp, {0, 0, 0}),
execution_time: nil,
type: type,
return_value: return_value
}
end
@doc """
Returns the node id from the trace.
It is PID if trace comes from a LiveView, CID if trace comes from a LiveComponent.
"""
@spec node_id(t()) :: pid() | CommonTypes.cid()
def node_id(%__MODULE__{cid: cid}) when not is_nil(cid), do: cid
def node_id(%__MODULE__{pid: pid}), do: pid
@doc """
Checks if the trace is a delete live component trace.
"""
@spec live_component_delete?(t()) :: boolean()
def live_component_delete?(%__MODULE__{
module: Phoenix.LiveView.Diff,
function: :delete_component,
arity: 2
}) do
true
end
def live_component_delete?(_), do: false
@spec callback_name(t()) :: String.t()
def callback_name(%__MODULE__{function: function, arity: arity}) do
"#{function}/#{arity}"
end
@spec arg_name(t(), non_neg_integer()) :: String.t()
def arg_name(trace, arg_index)
# Callbacks common for LiveView and LiveComponent
def arg_name(%__MODULE__{function: :handle_async}, 0), do: "name"
def arg_name(%__MODULE__{function: :handle_async}, 1), do: "async_fun_result"
def arg_name(%__MODULE__{function: :handle_async}, 2), do: "socket"
def arg_name(%__MODULE__{function: :handle_call}, 0), do: "message"
def arg_name(%__MODULE__{function: :handle_call}, 1), do: "from"
def arg_name(%__MODULE__{function: :handle_call}, 2), do: "socket"
def arg_name(%__MODULE__{function: :handle_cast}, 0), do: "message"
def arg_name(%__MODULE__{function: :handle_cast}, 1), do: "socket"
def arg_name(%__MODULE__{function: :handle_event}, 0), do: "event"
def arg_name(%__MODULE__{function: :handle_event}, 1), do: "unsigned_params"
def arg_name(%__MODULE__{function: :handle_event}, 2), do: "socket"
def arg_name(%__MODULE__{function: :handle_info}, 0), do: "message"
def arg_name(%__MODULE__{function: :handle_info}, 1), do: "socket"
def arg_name(%__MODULE__{function: :handle_params}, 0), do: "unsigned_params"
def arg_name(%__MODULE__{function: :handle_params}, 1), do: "uri"
def arg_name(%__MODULE__{function: :handle_params}, 2), do: "socket"
def arg_name(%__MODULE__{function: :render}, 0), do: "assigns"
def arg_name(%__MODULE__{function: :terminate}, 0), do: "reason"
def arg_name(%__MODULE__{function: :terminate}, 1), do: "socket"
# LiveView specific
def arg_name(%__MODULE__{function: :mount, arity: 3}, 0), do: "params"
def arg_name(%__MODULE__{function: :mount, arity: 3}, 1), do: "session"
def arg_name(%__MODULE__{function: :mount, arity: 3}, 2), do: "socket"
# LiveComponent specific
def arg_name(%__MODULE__{function: :mount, arity: 1}, 0), do: "socket"
def arg_name(%__MODULE__{function: :update}, 0), do: "assigns"
def arg_name(%__MODULE__{function: :update}, 1), do: "socket"
def arg_name(%__MODULE__{function: :update_many}, 0), do: "list"
defp get_transport_pid_from_args(args) do
args
|> Enum.map(&maybe_get_transport_pid(&1))
|> Enum.find(fn elem -> is_pid(elem) end)
end
defp maybe_get_transport_pid(%{transport_pid: transport}), do: transport
defp maybe_get_transport_pid(%{socket: %{transport_pid: transport}}), do: transport
defp maybe_get_transport_pid(_), do: nil
defp get_socket_id_from_args(args) do
args
|> Enum.map(&maybe_get_socket_id(&1))
|> Enum.find(fn elem -> is_binary(elem) end)
end
defp maybe_get_socket_id(%Phoenix.LiveView.Socket{id: id}), do: id
defp maybe_get_socket_id(%{socket: %Phoenix.LiveView.Socket{id: id}}), do: id
defp maybe_get_socket_id(_), do: nil
defp get_cid_from_args(args) do
args
|> Enum.map(&maybe_get_cid(&1))
|> Enum.find(fn elem -> is_struct(elem, Phoenix.LiveComponent.CID) end)
end
defp maybe_get_cid(%{myself: cid}), do: cid
defp maybe_get_cid(%{assigns: %{myself: cid}}), do: cid
defp maybe_get_cid(_), do: nil
end