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.

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

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
2 Likes