Implement comprehensive analytics system for UI/UX improvements
Browse files- app.py +98 -5
- src/analytics/redis_logger.py +130 -0
- src/analytics/session_analytics.py +138 -0
- src/analytics/user_logger.py +84 -0
app.py
CHANGED
|
@@ -14,14 +14,14 @@ from core.session import session_manager
|
|
| 14 |
from core.memory import check_redis_health
|
| 15 |
from core.errors import translate_error
|
| 16 |
from core.personality import personality
|
|
|
|
|
|
|
| 17 |
import logging
|
| 18 |
|
| 19 |
# Set up logging
|
| 20 |
logging.basicConfig(level=logging.INFO)
|
| 21 |
logger = logging.getLogger(__name__)
|
| 22 |
|
| 23 |
-
st.set_page_config(page_title="CosmicCat AI Assistant", page_icon="🐱", layout="wide")
|
| 24 |
-
|
| 25 |
# Initialize session state
|
| 26 |
if "messages" not in st.session_state:
|
| 27 |
st.session_state.messages = []
|
|
@@ -33,6 +33,18 @@ if "cosmic_mode" not in st.session_state:
|
|
| 33 |
st.session_state.cosmic_mode = True
|
| 34 |
if "show_welcome" not in st.session_state:
|
| 35 |
st.session_state.show_welcome = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
# Sidebar
|
| 38 |
with st.sidebar:
|
|
@@ -48,12 +60,23 @@ with st.sidebar:
|
|
| 48 |
selected_model_name = st.selectbox(
|
| 49 |
"Select Model",
|
| 50 |
options=list(model_options.keys()),
|
| 51 |
-
index=0
|
|
|
|
|
|
|
|
|
|
| 52 |
)
|
| 53 |
st.session_state.selected_model = model_options[selected_model_name]
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
# Cosmic mode toggle
|
| 56 |
-
st.session_state.cosmic_mode = st.checkbox("Enable Cosmic Mode", value=st.session_state.cosmic_mode
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
st.divider()
|
| 59 |
|
|
@@ -62,30 +85,55 @@ with st.sidebar:
|
|
| 62 |
ngrok_url_input = st.text_input(
|
| 63 |
"Ollama Server URL",
|
| 64 |
value=st.session_state.ngrok_url_temp,
|
| 65 |
-
help="Enter your ngrok URL"
|
|
|
|
|
|
|
|
|
|
| 66 |
)
|
| 67 |
|
| 68 |
if ngrok_url_input != st.session_state.ngrok_url_temp:
|
| 69 |
st.session_state.ngrok_url_temp = ngrok_url_input
|
| 70 |
st.success("✅ URL updated!")
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
if st.button("📡 Test Connection"):
|
|
|
|
|
|
|
| 73 |
try:
|
| 74 |
from core.providers.ollama import OllamaProvider
|
| 75 |
ollama_provider = OllamaProvider(st.session_state.selected_model)
|
| 76 |
is_valid = ollama_provider.validate_model()
|
|
|
|
|
|
|
| 77 |
if is_valid:
|
| 78 |
st.success("✅ Connection successful!")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
else:
|
| 80 |
st.error("❌ Model validation failed")
|
|
|
|
|
|
|
|
|
|
| 81 |
except Exception as e:
|
|
|
|
| 82 |
st.error(f"❌ Error: {str(e)[:50]}...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
if st.button("🗑️ Clear History"):
|
|
|
|
| 85 |
st.session_state.messages = []
|
| 86 |
# Also clear backend session
|
| 87 |
session_manager.clear_session("default_user")
|
| 88 |
st.success("History cleared!")
|
|
|
|
| 89 |
|
| 90 |
st.divider()
|
| 91 |
|
|
@@ -131,16 +179,32 @@ with st.sidebar:
|
|
| 131 |
# Add wake-up button if scaled to zero or initializing
|
| 132 |
if "scaled to zero" in status_message.lower() or "initializing" in status_message.lower():
|
| 133 |
if st.button("⚡ Wake Up HF Endpoint", key="wake_up_hf"):
|
|
|
|
| 134 |
with st.spinner("Attempting to wake up HF endpoint... This may take 2-4 minutes during initialization..."):
|
|
|
|
| 135 |
if hf_monitor.attempt_wake_up():
|
|
|
|
| 136 |
st.success("✅ Wake-up request sent! The endpoint should be initializing now. Try your request again in a moment.")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
time.sleep(3)
|
| 138 |
st.experimental_rerun()
|
| 139 |
else:
|
|
|
|
| 140 |
st.error("❌ Failed to send wake-up request. Please try again or wait for initialization to complete.")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
|
| 142 |
except Exception as e:
|
| 143 |
st.info(f"🤗 HF Endpoint: Error checking status - {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
|
| 145 |
# Redis Status
|
| 146 |
try:
|
|
@@ -153,10 +217,25 @@ with st.sidebar:
|
|
| 153 |
|
| 154 |
st.divider()
|
| 155 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
# Debug Info
|
| 157 |
st.subheader("🐛 Debug Info")
|
| 158 |
st.markdown(f"**Environment:** {'HF Space' if config.is_hf_space else 'Local'}")
|
| 159 |
st.markdown(f"**Model:** {st.session_state.selected_model}")
|
|
|
|
| 160 |
|
| 161 |
# Main interface
|
| 162 |
st.title("🐱 CosmicCat AI Assistant")
|
|
@@ -181,6 +260,9 @@ for message in st.session_state.messages:
|
|
| 181 |
user_input = st.chat_input("Type your message here...", key="chat_input")
|
| 182 |
|
| 183 |
if user_input:
|
|
|
|
|
|
|
|
|
|
| 184 |
chat_handler.process_user_message(user_input, selected_model_name)
|
| 185 |
|
| 186 |
# About tab
|
|
@@ -206,3 +288,14 @@ with tab1:
|
|
| 206 |
- **Secondary model**: HF Endpoint (advanced processing)
|
| 207 |
- **Memory system**: Redis-based session management
|
| 208 |
""")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
from core.memory import check_redis_health
|
| 15 |
from core.errors import translate_error
|
| 16 |
from core.personality import personality
|
| 17 |
+
from src.analytics.user_logger import user_logger
|
| 18 |
+
from src.analytics.session_analytics import session_analytics
|
| 19 |
import logging
|
| 20 |
|
| 21 |
# Set up logging
|
| 22 |
logging.basicConfig(level=logging.INFO)
|
| 23 |
logger = logging.getLogger(__name__)
|
| 24 |
|
|
|
|
|
|
|
| 25 |
# Initialize session state
|
| 26 |
if "messages" not in st.session_state:
|
| 27 |
st.session_state.messages = []
|
|
|
|
| 33 |
st.session_state.cosmic_mode = True
|
| 34 |
if "show_welcome" not in st.session_state:
|
| 35 |
st.session_state.show_welcome = True
|
| 36 |
+
if "session_id" not in st.session_state:
|
| 37 |
+
st.session_state.session_id = f"sess_{int(time.time())}_{os.urandom(4).hex()}"
|
| 38 |
+
|
| 39 |
+
# Start session tracking
|
| 40 |
+
session_analytics.start_session_tracking("default_user", st.session_state.session_id)
|
| 41 |
+
|
| 42 |
+
st.set_page_config(page_title="CosmicCat AI Assistant", page_icon="🐱", layout="wide")
|
| 43 |
+
|
| 44 |
+
# Log page view
|
| 45 |
+
session_analytics.track_interaction("default_user", st.session_state.session_id, "page_view", {
|
| 46 |
+
"page": "main_chat"
|
| 47 |
+
})
|
| 48 |
|
| 49 |
# Sidebar
|
| 50 |
with st.sidebar:
|
|
|
|
| 60 |
selected_model_name = st.selectbox(
|
| 61 |
"Select Model",
|
| 62 |
options=list(model_options.keys()),
|
| 63 |
+
index=0,
|
| 64 |
+
on_change=lambda: session_analytics.track_interaction("default_user", st.session_state.session_id, "model_selection", {
|
| 65 |
+
"selected_model": st.session_state.selected_model if 'selected_model' in st.session_state else model_options[list(model_options.keys())[0]]
|
| 66 |
+
})
|
| 67 |
)
|
| 68 |
st.session_state.selected_model = model_options[selected_model_name]
|
| 69 |
|
| 70 |
+
# Log model selection
|
| 71 |
+
session_analytics.track_interaction("default_user", st.session_state.session_id, "model_selection", {
|
| 72 |
+
"selected_model": st.session_state.selected_model
|
| 73 |
+
})
|
| 74 |
+
|
| 75 |
# Cosmic mode toggle
|
| 76 |
+
st.session_state.cosmic_mode = st.checkbox("Enable Cosmic Mode", value=st.session_state.cosmic_mode,
|
| 77 |
+
on_change=lambda: session_analytics.track_interaction("default_user", st.session_state.session_id, "cosmic_mode_toggle", {
|
| 78 |
+
"enabled": st.session_state.cosmic_mode
|
| 79 |
+
}))
|
| 80 |
|
| 81 |
st.divider()
|
| 82 |
|
|
|
|
| 85 |
ngrok_url_input = st.text_input(
|
| 86 |
"Ollama Server URL",
|
| 87 |
value=st.session_state.ngrok_url_temp,
|
| 88 |
+
help="Enter your ngrok URL",
|
| 89 |
+
on_change=lambda: session_analytics.track_interaction("default_user", st.session_state.session_id, "url_update", {
|
| 90 |
+
"url_changed": ngrok_url_input != st.session_state.ngrok_url_temp
|
| 91 |
+
})
|
| 92 |
)
|
| 93 |
|
| 94 |
if ngrok_url_input != st.session_state.ngrok_url_temp:
|
| 95 |
st.session_state.ngrok_url_temp = ngrok_url_input
|
| 96 |
st.success("✅ URL updated!")
|
| 97 |
+
session_analytics.track_interaction("default_user", st.session_state.session_id, "url_updated", {
|
| 98 |
+
"new_url": ngrok_url_input
|
| 99 |
+
})
|
| 100 |
|
| 101 |
if st.button("📡 Test Connection"):
|
| 102 |
+
start_time = time.time()
|
| 103 |
+
session_analytics.track_interaction("default_user", st.session_state.session_id, "test_connection_click")
|
| 104 |
try:
|
| 105 |
from core.providers.ollama import OllamaProvider
|
| 106 |
ollama_provider = OllamaProvider(st.session_state.selected_model)
|
| 107 |
is_valid = ollama_provider.validate_model()
|
| 108 |
+
end_time = time.time()
|
| 109 |
+
|
| 110 |
if is_valid:
|
| 111 |
st.success("✅ Connection successful!")
|
| 112 |
+
session_analytics.track_interaction("default_user", st.session_state.session_id, "connection_success", {
|
| 113 |
+
"response_time": end_time - start_time
|
| 114 |
+
})
|
| 115 |
+
user_logger.log_performance_metric("default_user", "connection_test", end_time - start_time)
|
| 116 |
else:
|
| 117 |
st.error("❌ Model validation failed")
|
| 118 |
+
session_analytics.track_interaction("default_user", st.session_state.session_id, "connection_failed", {
|
| 119 |
+
"error": "model_validation_failed"
|
| 120 |
+
})
|
| 121 |
except Exception as e:
|
| 122 |
+
end_time = time.time()
|
| 123 |
st.error(f"❌ Error: {str(e)[:50]}...")
|
| 124 |
+
session_analytics.track_interaction("default_user", st.session_state.session_id, "connection_error", {
|
| 125 |
+
"error": str(e)[:100],
|
| 126 |
+
"response_time": end_time - start_time
|
| 127 |
+
})
|
| 128 |
+
user_logger.log_error("default_user", "connection_test", str(e))
|
| 129 |
|
| 130 |
if st.button("🗑️ Clear History"):
|
| 131 |
+
session_analytics.track_interaction("default_user", st.session_state.session_id, "clear_history_click")
|
| 132 |
st.session_state.messages = []
|
| 133 |
# Also clear backend session
|
| 134 |
session_manager.clear_session("default_user")
|
| 135 |
st.success("History cleared!")
|
| 136 |
+
session_analytics.track_interaction("default_user", st.session_state.session_id, "history_cleared")
|
| 137 |
|
| 138 |
st.divider()
|
| 139 |
|
|
|
|
| 179 |
# Add wake-up button if scaled to zero or initializing
|
| 180 |
if "scaled to zero" in status_message.lower() or "initializing" in status_message.lower():
|
| 181 |
if st.button("⚡ Wake Up HF Endpoint", key="wake_up_hf"):
|
| 182 |
+
session_analytics.track_interaction("default_user", st.session_state.session_id, "wake_up_hf_click")
|
| 183 |
with st.spinner("Attempting to wake up HF endpoint... This may take 2-4 minutes during initialization..."):
|
| 184 |
+
start_time = time.time()
|
| 185 |
if hf_monitor.attempt_wake_up():
|
| 186 |
+
end_time = time.time()
|
| 187 |
st.success("✅ Wake-up request sent! The endpoint should be initializing now. Try your request again in a moment.")
|
| 188 |
+
session_analytics.track_interaction("default_user", st.session_state.session_id, "hf_wake_up_success", {
|
| 189 |
+
"response_time": end_time - start_time
|
| 190 |
+
})
|
| 191 |
+
user_logger.log_performance_metric("default_user", "hf_wake_up", end_time - start_time)
|
| 192 |
time.sleep(3)
|
| 193 |
st.experimental_rerun()
|
| 194 |
else:
|
| 195 |
+
end_time = time.time()
|
| 196 |
st.error("❌ Failed to send wake-up request. Please try again or wait for initialization to complete.")
|
| 197 |
+
session_analytics.track_interaction("default_user", st.session_state.session_id, "hf_wake_up_failed", {
|
| 198 |
+
"response_time": end_time - start_time
|
| 199 |
+
})
|
| 200 |
+
user_logger.log_error("default_user", "hf_wake_up", "Failed to wake up HF endpoint")
|
| 201 |
|
| 202 |
except Exception as e:
|
| 203 |
st.info(f"🤗 HF Endpoint: Error checking status - {str(e)}")
|
| 204 |
+
session_analytics.track_interaction("default_user", st.session_state.session_id, "hf_status_error", {
|
| 205 |
+
"error": str(e)
|
| 206 |
+
})
|
| 207 |
+
user_logger.log_error("default_user", "hf_status_check", str(e))
|
| 208 |
|
| 209 |
# Redis Status
|
| 210 |
try:
|
|
|
|
| 217 |
|
| 218 |
st.divider()
|
| 219 |
|
| 220 |
+
# Feedback Section
|
| 221 |
+
st.subheader("⭐ Feedback")
|
| 222 |
+
rating = st.radio("How would you rate your experience?", [1, 2, 3, 4, 5], horizontal=True)
|
| 223 |
+
feedback_comment = st.text_area("Additional comments (optional):")
|
| 224 |
+
if st.button("Submit Feedback"):
|
| 225 |
+
user_logger.log_feedback("default_user", rating, feedback_comment)
|
| 226 |
+
session_analytics.track_interaction("default_user", st.session_state.session_id, "feedback_submitted", {
|
| 227 |
+
"rating": rating,
|
| 228 |
+
"has_comment": bool(feedback_comment)
|
| 229 |
+
})
|
| 230 |
+
st.success("Thank you for your feedback! 🙏")
|
| 231 |
+
|
| 232 |
+
st.divider()
|
| 233 |
+
|
| 234 |
# Debug Info
|
| 235 |
st.subheader("🐛 Debug Info")
|
| 236 |
st.markdown(f"**Environment:** {'HF Space' if config.is_hf_space else 'Local'}")
|
| 237 |
st.markdown(f"**Model:** {st.session_state.selected_model}")
|
| 238 |
+
st.markdown(f"**Session ID:** {st.session_state.session_id}")
|
| 239 |
|
| 240 |
# Main interface
|
| 241 |
st.title("🐱 CosmicCat AI Assistant")
|
|
|
|
| 260 |
user_input = st.chat_input("Type your message here...", key="chat_input")
|
| 261 |
|
| 262 |
if user_input:
|
| 263 |
+
session_analytics.track_interaction("default_user", st.session_state.session_id, "chat_message_sent", {
|
| 264 |
+
"message_length": len(user_input)
|
| 265 |
+
})
|
| 266 |
chat_handler.process_user_message(user_input, selected_model_name)
|
| 267 |
|
| 268 |
# About tab
|
|
|
|
| 288 |
- **Secondary model**: HF Endpoint (advanced processing)
|
| 289 |
- **Memory system**: Redis-based session management
|
| 290 |
""")
|
| 291 |
+
|
| 292 |
+
# Log about page view
|
| 293 |
+
session_analytics.track_interaction("default_user", st.session_state.session_id, "about_page_view")
|
| 294 |
+
|
| 295 |
+
# End session tracking when app closes
|
| 296 |
+
def on_session_end():
|
| 297 |
+
session_analytics.end_session_tracking("default_user", st.session_state.session_id)
|
| 298 |
+
|
| 299 |
+
# Register cleanup function
|
| 300 |
+
import atexit
|
| 301 |
+
atexit.register(on_session_end)
|
src/analytics/redis_logger.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import time
|
| 3 |
+
import logging
|
| 4 |
+
from typing import Dict, List, Any, Optional
|
| 5 |
+
from datetime import datetime, timedelta
|
| 6 |
+
from core.redis_client import redis_client
|
| 7 |
+
|
| 8 |
+
logger = logging.getLogger(__name__)
|
| 9 |
+
|
| 10 |
+
class RedisAnalyticsLogger:
|
| 11 |
+
"""Redis-based analytics storage system"""
|
| 12 |
+
|
| 13 |
+
def __init__(self):
|
| 14 |
+
self.redis_client = redis_client.get_client()
|
| 15 |
+
|
| 16 |
+
def store_event(self, event_type: str, data: Dict[str, Any], user_id: Optional[str] = None):
|
| 17 |
+
"""Store an analytics event in Redis"""
|
| 18 |
+
try:
|
| 19 |
+
event_data = {
|
| 20 |
+
"event_type": event_type,
|
| 21 |
+
"timestamp": datetime.now().isoformat(),
|
| 22 |
+
"data": data
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
# Create unique key with timestamp
|
| 26 |
+
timestamp = int(time.time() * 1000) # Millisecond precision
|
| 27 |
+
key = f"analytics:{event_type}:{timestamp}"
|
| 28 |
+
if user_id:
|
| 29 |
+
key = f"analytics:{user_id}:{event_type}:{timestamp}"
|
| 30 |
+
|
| 31 |
+
# Store event data
|
| 32 |
+
self.redis_client.setex(key, 2592000, json.dumps(event_data)) # 30 days expiry
|
| 33 |
+
|
| 34 |
+
# Add to sorted set for time-based queries
|
| 35 |
+
index_key = f"analytics:index:{event_type}"
|
| 36 |
+
self.redis_client.zadd(index_key, {key: timestamp})
|
| 37 |
+
|
| 38 |
+
logger.debug(f"Stored analytics event: {key}")
|
| 39 |
+
return True
|
| 40 |
+
except Exception as e:
|
| 41 |
+
logger.error(f"Failed to store analytics event: {e}")
|
| 42 |
+
return False
|
| 43 |
+
|
| 44 |
+
def get_events(self, event_type: str, user_id: Optional[str] = None,
|
| 45 |
+
start_time: Optional[datetime] = None,
|
| 46 |
+
end_time: Optional[datetime] = None,
|
| 47 |
+
limit: int = 100) -> List[Dict[str, Any]]:
|
| 48 |
+
"""Retrieve analytics events"""
|
| 49 |
+
try:
|
| 50 |
+
# Determine index key
|
| 51 |
+
index_key = f"analytics:index:{event_type}"
|
| 52 |
+
if user_id:
|
| 53 |
+
index_key = f"analytics:{user_id}:index:{event_type}"
|
| 54 |
+
|
| 55 |
+
# Calculate time range
|
| 56 |
+
if start_time is None:
|
| 57 |
+
start_time = datetime.now() - timedelta(days=30)
|
| 58 |
+
if end_time is None:
|
| 59 |
+
end_time = datetime.now()
|
| 60 |
+
|
| 61 |
+
start_timestamp = int(start_time.timestamp() * 1000)
|
| 62 |
+
end_timestamp = int(end_time.timestamp() * 1000)
|
| 63 |
+
|
| 64 |
+
# Get event keys in time range
|
| 65 |
+
event_keys = self.redis_client.zrevrangebyscore(
|
| 66 |
+
index_key,
|
| 67 |
+
end_timestamp,
|
| 68 |
+
start_timestamp,
|
| 69 |
+
start=0,
|
| 70 |
+
num=limit
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
# Retrieve event data
|
| 74 |
+
events = []
|
| 75 |
+
for key in event_keys:
|
| 76 |
+
try:
|
| 77 |
+
data = self.redis_client.get(key)
|
| 78 |
+
if data:
|
| 79 |
+
events.append(json.loads(data))
|
| 80 |
+
except Exception as e:
|
| 81 |
+
logger.warning(f"Failed to retrieve event {key}: {e}")
|
| 82 |
+
|
| 83 |
+
return events
|
| 84 |
+
except Exception as e:
|
| 85 |
+
logger.error(f"Failed to retrieve analytics events: {e}")
|
| 86 |
+
return []
|
| 87 |
+
|
| 88 |
+
def get_event_count(self, event_type: str, user_id: Optional[str] = None,
|
| 89 |
+
start_time: Optional[datetime] = None,
|
| 90 |
+
end_time: Optional[datetime] = None) -> int:
|
| 91 |
+
"""Get count of events in time range"""
|
| 92 |
+
try:
|
| 93 |
+
index_key = f"analytics:index:{event_type}"
|
| 94 |
+
if user_id:
|
| 95 |
+
index_key = f"analytics:{user_id}:index:{event_type}"
|
| 96 |
+
|
| 97 |
+
if start_time is None:
|
| 98 |
+
start_time = datetime.now() - timedelta(days=30)
|
| 99 |
+
if end_time is None:
|
| 100 |
+
end_time = datetime.now()
|
| 101 |
+
|
| 102 |
+
start_timestamp = int(start_time.timestamp() * 1000)
|
| 103 |
+
end_timestamp = int(end_time.timestamp() * 1000)
|
| 104 |
+
|
| 105 |
+
return self.redis_client.zcount(index_key, start_timestamp, end_timestamp)
|
| 106 |
+
except Exception as e:
|
| 107 |
+
logger.error(f"Failed to count analytics events: {e}")
|
| 108 |
+
return 0
|
| 109 |
+
|
| 110 |
+
def aggregate_events(self, event_type: str, aggregation_field: str,
|
| 111 |
+
start_time: Optional[datetime] = None,
|
| 112 |
+
end_time: Optional[datetime] = None) -> Dict[str, int]:
|
| 113 |
+
"""Aggregate events by a specific field"""
|
| 114 |
+
try:
|
| 115 |
+
events = self.get_events(event_type, start_time=start_time, end_time=end_time)
|
| 116 |
+
aggregation = {}
|
| 117 |
+
|
| 118 |
+
for event in events:
|
| 119 |
+
data = event.get("data", {})
|
| 120 |
+
field_value = data.get(aggregation_field)
|
| 121 |
+
if field_value:
|
| 122 |
+
aggregation[field_value] = aggregation.get(field_value, 0) + 1
|
| 123 |
+
|
| 124 |
+
return aggregation
|
| 125 |
+
except Exception as e:
|
| 126 |
+
logger.error(f"Failed to aggregate events: {e}")
|
| 127 |
+
return {}
|
| 128 |
+
|
| 129 |
+
# Global instance
|
| 130 |
+
redis_logger = RedisAnalyticsLogger()
|
src/analytics/session_analytics.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
import json
|
| 3 |
+
import logging
|
| 4 |
+
from typing import Dict, List, Any, Optional
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
from core.redis_client import redis_client
|
| 7 |
+
from src.analytics.user_logger import user_logger
|
| 8 |
+
|
| 9 |
+
logger = logging.getLogger(__name__)
|
| 10 |
+
|
| 11 |
+
class SessionAnalytics:
|
| 12 |
+
"""Session-level tracking and analytics"""
|
| 13 |
+
|
| 14 |
+
def __init__(self):
|
| 15 |
+
self.redis_client = redis_client.get_client()
|
| 16 |
+
|
| 17 |
+
def start_session_tracking(self, user_id: str, session_id: str):
|
| 18 |
+
"""Start tracking a user session"""
|
| 19 |
+
try:
|
| 20 |
+
session_data = {
|
| 21 |
+
"user_id": user_id,
|
| 22 |
+
"session_id": session_id,
|
| 23 |
+
"start_time": datetime.now().isoformat(),
|
| 24 |
+
"interactions": [],
|
| 25 |
+
"duration": 0,
|
| 26 |
+
"page_views": 0,
|
| 27 |
+
"actions": {}
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
key = f"analytics:sessions:{session_id}"
|
| 31 |
+
self.redis_client.setex(key, 86400, json.dumps(session_data)) # 24 hours expiry
|
| 32 |
+
|
| 33 |
+
# Log session start
|
| 34 |
+
user_logger.log_user_action(user_id, "session_start", {
|
| 35 |
+
"session_id": session_id
|
| 36 |
+
})
|
| 37 |
+
|
| 38 |
+
logger.info(f"Started session tracking: {session_id}")
|
| 39 |
+
except Exception as e:
|
| 40 |
+
logger.error(f"Failed to start session tracking: {e}")
|
| 41 |
+
|
| 42 |
+
def track_interaction(self, user_id: str, session_id: str, interaction_type: str,
|
| 43 |
+
details: Dict[str, Any] = None):
|
| 44 |
+
"""Track a user interaction within a session"""
|
| 45 |
+
try:
|
| 46 |
+
key = f"analytics:sessions:{session_id}"
|
| 47 |
+
session_data_str = self.redis_client.get(key)
|
| 48 |
+
|
| 49 |
+
if session_data_str:
|
| 50 |
+
session_data = json.loads(session_data_str)
|
| 51 |
+
|
| 52 |
+
# Add interaction
|
| 53 |
+
interaction = {
|
| 54 |
+
"type": interaction_type,
|
| 55 |
+
"timestamp": datetime.now().isoformat(),
|
| 56 |
+
"details": details or {}
|
| 57 |
+
}
|
| 58 |
+
session_data["interactions"].append(interaction)
|
| 59 |
+
|
| 60 |
+
# Update action counts
|
| 61 |
+
session_data["actions"][interaction_type] = session_data["actions"].get(interaction_type, 0) + 1
|
| 62 |
+
|
| 63 |
+
# Update page views for navigation events
|
| 64 |
+
if interaction_type == "page_view":
|
| 65 |
+
session_data["page_views"] += 1
|
| 66 |
+
|
| 67 |
+
# Update duration
|
| 68 |
+
start_time = datetime.fromisoformat(session_data["start_time"])
|
| 69 |
+
session_data["duration"] = (datetime.now() - start_time).total_seconds()
|
| 70 |
+
|
| 71 |
+
# Save updated session data
|
| 72 |
+
self.redis_client.setex(key, 86400, json.dumps(session_data))
|
| 73 |
+
|
| 74 |
+
# Log the interaction
|
| 75 |
+
user_logger.log_user_action(user_id, f"session_interaction_{interaction_type}", {
|
| 76 |
+
"session_id": session_id,
|
| 77 |
+
"details": details or {}
|
| 78 |
+
})
|
| 79 |
+
|
| 80 |
+
except Exception as e:
|
| 81 |
+
logger.error(f"Failed to track interaction: {e}")
|
| 82 |
+
|
| 83 |
+
def end_session_tracking(self, user_id: str, session_id: str):
|
| 84 |
+
"""End session tracking and generate summary"""
|
| 85 |
+
try:
|
| 86 |
+
key = f"analytics:sessions:{session_id}"
|
| 87 |
+
session_data_str = self.redis_client.get(key)
|
| 88 |
+
|
| 89 |
+
if session_data_str:
|
| 90 |
+
session_data = json.loads(session_data_str)
|
| 91 |
+
|
| 92 |
+
# Update final duration
|
| 93 |
+
start_time = datetime.fromisoformat(session_data["start_time"])
|
| 94 |
+
session_data["end_time"] = datetime.now().isoformat()
|
| 95 |
+
session_data["duration"] = (datetime.now() - start_time).total_seconds()
|
| 96 |
+
|
| 97 |
+
# Save final session data
|
| 98 |
+
self.redis_client.setex(key, 2592000, json.dumps(session_data)) # 30 days expiry for completed sessions
|
| 99 |
+
|
| 100 |
+
# Log session end with summary
|
| 101 |
+
user_logger.log_user_action(user_id, "session_end", {
|
| 102 |
+
"session_id": session_id,
|
| 103 |
+
"duration": session_data["duration"],
|
| 104 |
+
"interactions": len(session_data["interactions"]),
|
| 105 |
+
"page_views": session_data["page_views"],
|
| 106 |
+
"actions": session_data["actions"]
|
| 107 |
+
})
|
| 108 |
+
|
| 109 |
+
logger.info(f"Ended session tracking: {session_id}")
|
| 110 |
+
|
| 111 |
+
except Exception as e:
|
| 112 |
+
logger.error(f"Failed to end session tracking: {e}")
|
| 113 |
+
|
| 114 |
+
def get_session_summary(self, session_id: str) -> Optional[Dict[str, Any]]:
|
| 115 |
+
"""Get session summary data"""
|
| 116 |
+
try:
|
| 117 |
+
key = f"analytics:sessions:{session_id}"
|
| 118 |
+
session_data_str = self.redis_client.get(key)
|
| 119 |
+
|
| 120 |
+
if session_data_str:
|
| 121 |
+
return json.loads(session_data_str)
|
| 122 |
+
return None
|
| 123 |
+
except Exception as e:
|
| 124 |
+
logger.error(f"Failed to get session summary: {e}")
|
| 125 |
+
return None
|
| 126 |
+
|
| 127 |
+
def get_user_sessions(self, user_id: str, limit: int = 10) -> List[Dict[str, Any]]:
|
| 128 |
+
"""Get recent sessions for a user"""
|
| 129 |
+
try:
|
| 130 |
+
# This would require a more complex indexing system
|
| 131 |
+
# For now, we'll return an empty list as this requires additional implementation
|
| 132 |
+
return []
|
| 133 |
+
except Exception as e:
|
| 134 |
+
logger.error(f"Failed to get user sessions: {e}")
|
| 135 |
+
return []
|
| 136 |
+
|
| 137 |
+
# Global instance
|
| 138 |
+
session_analytics = SessionAnalytics()
|
src/analytics/user_logger.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import time
|
| 3 |
+
import logging
|
| 4 |
+
from typing import Dict, Any, Optional
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
from core.redis_client import redis_client
|
| 7 |
+
|
| 8 |
+
logger = logging.getLogger(__name__)
|
| 9 |
+
|
| 10 |
+
class UserLogger:
|
| 11 |
+
"""Comprehensive user interaction logging system"""
|
| 12 |
+
|
| 13 |
+
def __init__(self):
|
| 14 |
+
self.redis_client = redis_client.get_client()
|
| 15 |
+
|
| 16 |
+
def log_user_action(self, user_id: str, action: str, details: Dict[str, Any] = None):
|
| 17 |
+
"""Log a user action"""
|
| 18 |
+
try:
|
| 19 |
+
event_data = {
|
| 20 |
+
"user_id": user_id,
|
| 21 |
+
"action": action,
|
| 22 |
+
"timestamp": datetime.now().isoformat(),
|
| 23 |
+
"details": details or {}
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
# Store in Redis with expiration (30 days)
|
| 27 |
+
key = f"analytics:user_events:{user_id}:{int(time.time())}"
|
| 28 |
+
self.redis_client.setex(
|
| 29 |
+
key,
|
| 30 |
+
2592000, # 30 days in seconds
|
| 31 |
+
json.dumps(event_data)
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
# Also store in global events
|
| 35 |
+
global_key = f"analytics:global_events:{int(time.time())}"
|
| 36 |
+
self.redis_client.setex(
|
| 37 |
+
global_key,
|
| 38 |
+
2592000, # 30 days in seconds
|
| 39 |
+
json.dumps(event_data)
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
logger.info(f"Logged user action: {user_id} - {action}")
|
| 43 |
+
except Exception as e:
|
| 44 |
+
logger.error(f"Failed to log user action: {e}")
|
| 45 |
+
|
| 46 |
+
def log_ui_state(self, user_id: str, state: str, details: Dict[str, Any] = None):
|
| 47 |
+
"""Log UI state changes"""
|
| 48 |
+
self.log_user_action(user_id, f"ui_state_{state}", details)
|
| 49 |
+
|
| 50 |
+
def log_performance_metric(self, user_id: str, metric_name: str, value: float, details: Dict[str, Any] = None):
|
| 51 |
+
"""Log performance metrics"""
|
| 52 |
+
metric_details = {
|
| 53 |
+
"value": value,
|
| 54 |
+
"metric": metric_name
|
| 55 |
+
}
|
| 56 |
+
if details:
|
| 57 |
+
metric_details.update(details)
|
| 58 |
+
|
| 59 |
+
self.log_user_action(user_id, f"performance_{metric_name}", metric_details)
|
| 60 |
+
|
| 61 |
+
def log_error(self, user_id: str, error_type: str, error_message: str, details: Dict[str, Any] = None):
|
| 62 |
+
"""Log errors"""
|
| 63 |
+
error_details = {
|
| 64 |
+
"error_type": error_type,
|
| 65 |
+
"error_message": error_message
|
| 66 |
+
}
|
| 67 |
+
if details:
|
| 68 |
+
error_details.update(details)
|
| 69 |
+
|
| 70 |
+
self.log_user_action(user_id, f"error_{error_type}", error_details)
|
| 71 |
+
|
| 72 |
+
def log_feedback(self, user_id: str, rating: int, comment: str = "", details: Dict[str, Any] = None):
|
| 73 |
+
"""Log user feedback"""
|
| 74 |
+
feedback_details = {
|
| 75 |
+
"rating": rating,
|
| 76 |
+
"comment": comment
|
| 77 |
+
}
|
| 78 |
+
if details:
|
| 79 |
+
feedback_details.update(details)
|
| 80 |
+
|
| 81 |
+
self.log_user_action(user_id, "user_feedback", feedback_details)
|
| 82 |
+
|
| 83 |
+
# Global instance
|
| 84 |
+
user_logger = UserLogger()
|