Detecting user exit / browser tab closed / session end

Situation : I was struggling to detect when an user exited the application (not explicit Logout, but just closed the browser tab). I implemented a heartbeat thread which will be used to monitor if the user is working on a task, if the user leaves (either by closing the tab or logging out) then return the unprocessed tasks to queue immediately. (This is different from when user goes idle, where the message can timeout and return to queue after the timeout seconds.)

Problem : There is no standard way in streamlit to tell if the browser tab is active (again not idle, but the tab is closed), which made the heartbeat thread go on forever once started with exception where there is an explicit logout triggered inside the session itself. I tried adding some variable to session_state dictionary, but for some reason it persists for a long time (infinite?) after the tab is closed and thus the heartbeat thread never stops ticking.

My solution : After much hacking and digging into source code / discussions, I came up with following solution :

def heartbeat(user_id):
    with open("writelog.log", 'a') as f:
        f.write(f"User '{user_id}' Alive at {datetime.datetime.now()}\n")

def start_beating(user_id):
    thread = threading.Timer(interval=2, function=start_beating, args=(user_id,) )
   
    # insert context to the current thread, needed for 
    # getting session specific attributes like st.session_state

    add_script_run_ctx(thread)

     # context is required to get session_id of the calling
     # thread (which would be the script thread) 
    ctx = get_script_run_ctx()     

    runtime = get_instance()     # this is the main runtime, contains all the sessions

    if runtime.is_active_session(session_id=ctx.session_id) and some_more_conditions():
        # Session is running
        thread.start()
        heartbeat(user_id)
    else:
        # Session is not running, Do what you want to do on user exit here
        return

I found two places where sessions are stored inside the SessionManager

  1. runtime._session_mgr.session_storage.list() : Contained in SessionStorage which contains the cache of sessions. I however did not understand when the sessions are inserted and removed in this list, sometimes the sessions did not exist while I was clearly in one and also it take the β€˜ttl’ time (2 mins by default) to get it removed from the cache.
  2. runtime.is_active_session() or runtime._session_mgr.list_active_sessions() This is what I used above, it seems to update immediately as the user open / close tab

Questions :

  1. It is a good way to handle it ? My initial impression is that it works, however some discussion here : https://github.com/streamlit/streamlit/issues/6166 indicates that the delay in session object being deleted is to handle intermittent connection drops. Should I be using the cache storage instead or there is a better session resolver to get the actual working copy of the session object ?
  2. Is there any other recommended way to achieve this ?
  3. Are there side effects / drawbacks to my approach?

Note : I did find other threads similar to this, but none actually had a working solution, This might be a duplicated effort.

1 Like

@potbot_88 Can you share what are the functions we need to import

1 Like

Here :

import streamlit as st
import threading 

from streamlit.runtime.scriptrunner import add_script_run_ctx
from streamlit.runtime.scriptrunner.script_run_context import get_script_run_ctx
from streamlit.runtime import get_instance
3 Likes

I am facing one problem, the session is active in the tab and the heartbeat is running but suppose i be inactive for few minutes and then it immediately executes the else block but i dont want to go the control to else block until an unless the tab is active in his chrome window, once he closes or refreshes the tab then only i want to execute the else block, how do i do that?

1 Like

I never faced such an issue, the app works well for user inactivity for infinitely (seemingly) long periods. As long as the browser is not suspending the tab or user refreshing/closing it, this mechanism should work. Also, it is not clear why heartbeat is working but the else condition was fired in your case. Can you show the exact code which you are using?