Soma / app /services /analytics.py
Komalpreet Kaur
feat: implement responsive, theme-adaptive visitor analytics modal dialog box
ccfe044
Raw
History Blame Contribute Delete
4.93 kB
import redis
from app.core.config import settings
import logging
import time
logger = logging.getLogger("soma.analytics")
class AnalyticsManager:
def __init__(self):
self._client = None
self._connected = False
self._last_attempt = 0.0
self._cooldown = 30.0 # seconds to wait before trying to reconnect if offline
def get_client(self):
# Return existing connection if still healthy
if self._client is not None:
try:
self._client.ping()
self._connected = True
return self._client
except Exception:
logger.warning("Redis connection lost, re-establishing...")
self._client = None
self._connected = False
# If we failed to connect recently, don't block the request; return None (fallback) fast.
now = time.time()
if now - self._last_attempt < self._cooldown:
return None
self._last_attempt = now
# 1. Try REDIS_URL first (Upstash URL format)
if settings.REDIS_URL:
try:
print(f"Analytics: Connecting to Upstash Redis...")
# Note: socket_timeout/socket_connect_timeout are small to prevent FastAPI from hanging
self._client = redis.Redis.from_url(
settings.REDIS_URL,
decode_responses=True,
socket_timeout=3.0,
socket_connect_timeout=3.0,
retry_on_timeout=False
)
self._client.ping()
self._connected = True
print("Analytics: Connected to Upstash Redis successfully!")
return self._client
except Exception as e:
print(f"Analytics: Failed to connect to Upstash via REDIS_URL: {e}")
self._client = None
self._connected = False
# 2. Try default REDIS_HOST / REDIS_PORT (Local fallback)
if settings.REDIS_HOST:
try:
print(f"Analytics: Connecting to local Redis at {settings.REDIS_HOST}:{settings.REDIS_PORT}...")
self._client = redis.Redis(
host=settings.REDIS_HOST,
port=settings.REDIS_PORT,
decode_responses=True,
socket_timeout=2.0,
socket_connect_timeout=2.0,
retry_on_timeout=False
)
self._client.ping()
self._connected = True
print("Analytics: Connected to local Redis successfully!")
return self._client
except Exception as e:
print(f"Analytics: Failed to connect to local Redis: {e}")
self._client = None
self._connected = False
return None
def record_hit(self, visitor_id: str) -> bool:
"""
Record a visit hit.
Increments the total visits count and adds the visitor_id to the unique set.
Optimized for Upstash Free Tier limits by being called exactly once per session.
"""
client = self.get_client()
if client is None:
print("Analytics: Redis is offline, skipping hit.")
return False
try:
# 1. Increment total visits
client.incr("soma:analytics:total_visits")
# 2. Add visitor_id to unique visitors set
client.sadd("soma:analytics:unique_visitors", visitor_id)
return True
except Exception as e:
print(f"Analytics: Failed to record hit in Redis: {e}")
return False
def get_stats(self) -> dict:
"""
Retrieve stats (total visitors, unique visitors) from Redis.
Returns mock/offline status if Redis connection fails, ensuring zero crashes.
"""
client = self.get_client()
if client is None or not self._connected:
return {
"total_visitors": 0,
"unique_visitors": 0,
"status": "offline"
}
try:
# Fetch total visits
total_val = client.get("soma:analytics:total_visits")
total = int(total_val) if total_val else 0
# Fetch cardinality of unique set
unique = client.scard("soma:analytics:unique_visitors") or 0
return {
"total_visitors": total,
"unique_visitors": unique,
"status": "online"
}
except Exception as e:
print(f"Analytics: Failed to query stats: {e}")
return {
"total_visitors": 0,
"unique_visitors": 0,
"status": "error",
"detail": str(e)
}
analytics_manager = AnalyticsManager()