TimeTravel
TimeTravel is a record & replay debugger for Phoenix LiveView applications.
TimeTravel allows you to record the state of your LiveView as you interact with the page and replay the interactions while viewing the state of the socket's assigns at any given point in time.
By attaching to the Telemetry events Phoenix LiveView emits we are able to construct a timeline of events & state to rewind and replay as many times as you like in order to track down bugs in the socket's state.
TimeTravel was inspired by Elm Reactor and the rr-project
Demo
See the Time Travel Demo Repo for a working example
See a short example video here:
Installation
If available in Hex, the package can be installed
by adding time_travel to your list of dependencies in mix.exs:
def deps do
[
{:time_travel, "~> 0.1.0"}
]
end
Steps
- Download and install the Chrome Extension
- Add
:time_travelas an Elixir dependency inmix.exs(see above) - (If you have already set up Phoenix Channels) Add the
lvdbgchannel specification inyour_app_web/channels/user_socket.ex:
## Channels
channel "lvdbg:*", TimeTravel.LiveViewDebugChannel
- If you have not set up a Phoenix socket, run
mix phx.gen.socket User - Make sure you have a socket configuration set up in
lib_your_app_web/endpoint.ex:
socket "/socket", YourAppWeb.UserSocket,
websocket: true,
longpoll: false
- Create the channel in
app.js(before you declare the liveSocket):
let socketId = document.querySelector('div[data-phx-main]').getAttribute("id");
let timeTravelSocket = new Socket("/socket")
timeTravelSocket.connect();
let channel = timeTravelSocket.channel('lvdbg:' + socketId);
channel.join()
.receive("ok", ({messages}) => console.log("catching up", messages) )
.receive("error", ({reason}) => console.log("failed join", reason) )
.receive("timeout", () => console.log("Networking issue. Still waiting..."))
channel.on("lv_event", payload => {
window.dispatchEvent(new CustomEvent('SaveAssigns', {detail: payload}));
});
window.addEventListener('RestoreAssigns', e => {
channel.push("restore-assigns", {...e.detail, socketId: socketId});
});
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})
- Set your Endpoint and PubSub modules in
config/config.exs:
# config/config.exs
config :time_travel,
endpoint: MyAppWeb.Endpoint,
pubsub: MyAppWeb.PubSub
- Attach to the telemetry handlers in the init callback in
lib/your_app_web/telemetry.ex:
# telemetry.ex
# init callback
:ok =
:telemetry.attach_many(
"live-view-handler",
[
[:phoenix, :live_view, :mount, :stop],
[:phoenix, :live_view, :handle_event, :stop],
[:phoenix, :live_view, :handle_params, :stop]
],
&TimeTravel.TelemetryHandler.live_view_event_handler/4,
%{}
)
- Finally, subscribe to and handle the callbacks on the LiveView you want to debug:
# page_live.ex
def mount(params, session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(MyAppWeb.PubSub, "time_travel")
end
{:ok, socket}
end
def handle_info({:time_travel, _socket_id, nil}, socket) do
{:noreply, socket}
end
def handle_info({:time_travel, socket_id, assigns}, %{id: id} = socket) when id == socket_id do
{:noreply, assign(socket, assigns)}
end
def handle_info({:time_travel, _socket_id, _assigns}, params, socket) do
{:noreply, socket}
end
For a full example see the Time Travel Demo Repo