We’re trying to expire a user’s login session after a certain amount of time with no activity from the user. I’m planning to reset an inactivity timer every time handle_event/3 is called. However, I don’t want to have to do this within all handle_event/3 function clauses in all of our LiveViews.
I’m not sure what you can do with it, but it looks like handle_event invokes telemetry:
It’s going to invoke any telemetry handlers inline, which means it will be executed in the process of the LiveView. You can’t modify state from the telemetry handler, but you could send self a message to update some internal state (or update your external state if you’re tracking outside of the LiveView process).
Few things to be careful of:
The cost for updating your tracked state should be cheap, because it’s going to block the LiveView process from doing work
Telemetry handlers will detach if there’s an exception. If this happened, you would need to re-attach or all of your users would appear as inactive
Very interesting! It feels weird having to do this using telemetry and I’m still hoping there’s another way. But I’ll sure look into it. Thanks for the detailed response!
Definitely don’t disagree with you. It feels a bit odd to do something like this with telemetry—although I’m still working out my feelings on whether telemetry should be used to extend library behavior in a simple way.
I’ll give my second thought here. I ran into a scenario where I wanted my LiveViews to all mount with the same authentication function, where I make sure that there’s a valid user and also do things such as handle initial user-flow and onboarding redirections. Since every LiveView in my system needs the same functionality, I wanted to do it in a way that would be impossible to forget.
I extended the use MyAppWeb, :live macro to include a macro that defines mount/3 with the authentication function in it. I call auth_mount/3 in the mount/3 function if everything is looking good and the LiveView should proceed, or I handle the error without ever mounting the LiveView.
I think you could do something similar here where handle_event is defined in a macro and it is proxied to something_handle_event. Or another riff on this would be to define handle_event_bump_session as a macro that builds up handle_event/3 with your session code.
Both approaches get into some light macro definition, but is the way I was thinking about solving this originally. The solutions aren’t easily composable though, which I dislike.
I find myself doing something very similar to what @sb8244 is describing.
In all of my LV applications so far, I create a generic LV template like:
defmodule AppWeb.LiveTemplate do
defmacro __using__(_opts) do
quote do
use AppWeb, :live_view
alias AppWeb.Router.Helpers, as: Routes
alias App.{Contexts..}
require Logger
def handle_event(
"user_inactive",
target,
socket
) do
# Do Something
{:noreply, socket}
end
end
end
end
end
You can then use this in your live views:
defmodule AppWeb.LivePage do
use AppWeb.LiveTemplate
def mount() do
# etc...
end
end
I’ve found this pattern to be super useful when defining events that all my live views need to respond to or like @sb8244 said, creating common authentication functionality.
I wouldn’t be surprised if there’s a better way to do this though. One thing that confuses me is having to set up the Routes alias again inside this macro even though it’s also in AppWeb.
Instead of using handle_event/3 to track user activity, we’ve ended up using the mousemove, scroll, keydown and resize DOM events as described in this CSS-Tricks article.
We used a LiveComponent, with a phx-hook, that is rendered within the live.html.leex layout template so that all live views are tracked.
defmodule FooWeb.ActivityTracker do
use FooWeb, :live_component
def render(assigns) do
~L"""
<div phx-hook="ActivityTracker"></div>
"""
end
def handle_event("activity", _params, socket) do
Foo.InactiveLoginExpirer.track_activity(socket.assigns.login_id)
{:noreply, socket}
end
end
I considered defining a handle_event/3 like that in the FooWeb.live_view macro but we ended up not needing it after using a stateful LiveComponent since the events will all be handled by the live component.
I’ll be keeping that pattern in mind as it may come in handy in the future. Thanks!
One minor thing you may want to look at is ensuring that you are cleaning up the window listening on hook destroy callback. Otherwise, you will leak memory of the hook and possibly the element.