From f7993a9c71e5e5a53272af708ab1fa4bb78da133 Mon Sep 17 00:00:00 2001 From: Cristian Pufu Date: Fri, 12 Dec 2025 17:44:51 +0200 Subject: [PATCH] fix: auto-gen session id --- pyproject.toml | 2 +- src/aptabase/client.py | 46 +++++++++++++++++++++++++++++------------- uv.lock | 2 +- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b07a39f..9451963 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "aptabase" -version = "0.0.2" +version = "0.0.3" description = "Python SDK for Aptabase analytics" readme = "README.md" requires-python = ">=3.11" diff --git a/src/aptabase/client.py b/src/aptabase/client.py index 062fefa..f0bd721 100644 --- a/src/aptabase/client.py +++ b/src/aptabase/client.py @@ -4,6 +4,7 @@ import asyncio import logging +from datetime import datetime, timedelta from typing import Any from urllib.parse import urljoin @@ -20,6 +21,8 @@ "SH": None, # Self-hosted, requires custom base_url in options } +_SESSION_TIMEOUT = timedelta(hours=1) + class Aptabase: """Aptabase analytics client.""" @@ -71,6 +74,7 @@ def __init__( self._client: httpx.AsyncClient | None = None self._flush_task: asyncio.Task[Any] | None = None self._session_id: str | None = None + self._last_touched: datetime | None = None def _get_base_url(self, app_key: str) -> str: """Determine the base URL from the app key.""" @@ -132,13 +136,7 @@ async def stop(self) -> None: await self._client.aclose() self._client = None - async def track( - self, - event_name: str, - props: dict[str, Any] | None = None, - *, - session_id: str | None = None, - ) -> None: + async def track(self, event_name: str, props: dict[str, Any] | None = None) -> None: """Track an analytics event. Args: @@ -152,15 +150,17 @@ async def track( if props is not None and not isinstance(props, dict): raise ValidationError("Event properties must be a dictionary") + # Get or create session (handles timeout automatically) + session_id = self._get_or_create_session() + event = Event( name=event_name, props=props, - session_id=session_id or self._session_id, + session_id=session_id, ) async with self._queue_lock: self._event_queue.append(event) - # Auto-flush if we reach the batch size if len(self._event_queue) >= self._max_batch_size: await self._flush_events() @@ -216,8 +216,26 @@ async def _periodic_flush(self) -> None: except asyncio.CancelledError: pass - def set_session_id(self, session_id: str) -> None: - """Set the session ID for future events.""" - if not session_id or not isinstance(session_id, str): - raise ValidationError("Session ID must be a non-empty string") - self._session_id = session_id + def _get_or_create_session(self) -> str: + """Get current session or create new one if expired.""" + now = datetime.now() + + if self._session_id is None or self._last_touched is None: + self._session_id = self._new_session_id() + self._last_touched = now + elif now - self._last_touched > _SESSION_TIMEOUT: + self._session_id = self._new_session_id() + self._last_touched = now + else: + self._last_touched = now + + return self._session_id + + @staticmethod + def _new_session_id() -> str: + """Generate a new session ID.""" + import random + + epoch_seconds = int(datetime.now().timestamp()) + random_part = random.randint(0, 99999999) + return str(epoch_seconds * 100000000 + random_part) diff --git a/uv.lock b/uv.lock index 7fe9302..438f8d7 100644 --- a/uv.lock +++ b/uv.lock @@ -17,7 +17,7 @@ wheels = [ [[package]] name = "aptabase" -version = "0.0.2" +version = "0.0.3" source = { editable = "." } dependencies = [ { name = "httpx" },