44
55import asyncio
66import logging
7+ from datetime import datetime , timedelta
78from typing import Any
89from urllib .parse import urljoin
910
2021 "SH" : None , # Self-hosted, requires custom base_url in options
2122}
2223
24+ _SESSION_TIMEOUT = timedelta (hours = 1 )
25+
2326
2427class Aptabase :
2528 """Aptabase analytics client."""
@@ -71,6 +74,7 @@ def __init__(
7174 self ._client : httpx .AsyncClient | None = None
7275 self ._flush_task : asyncio .Task [Any ] | None = None
7376 self ._session_id : str | None = None
77+ self ._last_touched : datetime | None = None
7478
7579 def _get_base_url (self , app_key : str ) -> str :
7680 """Determine the base URL from the app key."""
@@ -132,13 +136,7 @@ async def stop(self) -> None:
132136 await self ._client .aclose ()
133137 self ._client = None
134138
135- async def track (
136- self ,
137- event_name : str ,
138- props : dict [str , Any ] | None = None ,
139- * ,
140- session_id : str | None = None ,
141- ) -> None :
139+ async def track (self , event_name : str , props : dict [str , Any ] | None = None ) -> None :
142140 """Track an analytics event.
143141
144142 Args:
@@ -152,15 +150,17 @@ async def track(
152150 if props is not None and not isinstance (props , dict ):
153151 raise ValidationError ("Event properties must be a dictionary" )
154152
153+ # Get or create session (handles timeout automatically)
154+ session_id = self ._get_or_create_session ()
155+
155156 event = Event (
156157 name = event_name ,
157158 props = props ,
158- session_id = session_id or self . _session_id ,
159+ session_id = session_id ,
159160 )
160161
161162 async with self ._queue_lock :
162163 self ._event_queue .append (event )
163-
164164 # Auto-flush if we reach the batch size
165165 if len (self ._event_queue ) >= self ._max_batch_size :
166166 await self ._flush_events ()
@@ -216,8 +216,26 @@ async def _periodic_flush(self) -> None:
216216 except asyncio .CancelledError :
217217 pass
218218
219- def set_session_id (self , session_id : str ) -> None :
220- """Set the session ID for future events."""
221- if not session_id or not isinstance (session_id , str ):
222- raise ValidationError ("Session ID must be a non-empty string" )
223- self ._session_id = session_id
219+ def _get_or_create_session (self ) -> str :
220+ """Get current session or create new one if expired."""
221+ now = datetime .now ()
222+
223+ if self ._session_id is None or self ._last_touched is None :
224+ self ._session_id = self ._new_session_id ()
225+ self ._last_touched = now
226+ elif now - self ._last_touched > _SESSION_TIMEOUT :
227+ self ._session_id = self ._new_session_id ()
228+ self ._last_touched = now
229+ else :
230+ self ._last_touched = now
231+
232+ return self ._session_id
233+
234+ @staticmethod
235+ def _new_session_id () -> str :
236+ """Generate a new session ID."""
237+ import random
238+
239+ epoch_seconds = int (datetime .now ().timestamp ())
240+ random_part = random .randint (0 , 99999999 )
241+ return str (epoch_seconds * 100000000 + random_part )
0 commit comments