LifeRPG_v2.0/modern/backend/telemetry.py
TLimoges33 7fe4ae5365
🧙‍♂️ Transform LifeRPG into The Wizard's Grimoire - Production-Ready Application
 Major Features Added:
- Complete magical theming and rebranding from LifeRPG to The Wizard's Grimoire
- Production-grade React frontend with Tailwind CSS v4 and magical aesthetics
- Comprehensive analytics dashboard with Recharts integration (ScryingPortal)
- Push notifications system with PWA service worker support
- Drag & drop functionality using @dnd-kit for habit reordering
- Social features with friends system and leaderboards
- Performance optimization tools and monitoring
- Mobile app enhancement with PWA installation support

🏗️ Technical Infrastructure:
- Advanced service worker with offline support and background sync
- Zustand state management for scalable application state
- Production-ready UI component system with enhanced Button, Card, Input
- Progressive Web App (PWA) with manifest and app installation
- FastAPI backend with comprehensive API endpoints
- Docker containerization and CI/CD pipeline setup

📱 Progressive Web App Features:
- Offline functionality with intelligent caching
- Push notification support for habit reminders
- App installation on mobile and desktop platforms
- Background sync for offline data management
- Performance monitoring and optimization tools

🎨 User Experience:
- Magical wizard/grimoire theming throughout application
- Responsive design optimized for all device sizes
- Drag & drop habit management with smooth animations
- Interactive analytics with multiple chart types
- Social connectivity with friends and competitive features
- Comprehensive notification and performance settings

🔧 Developer Experience:
- Modern development stack with Vite and React
- Comprehensive testing setup and CI/CD pipelines
- Code quality tools with pre-commit hooks
- Docker development environment
- Detailed documentation and implementation guides

This represents a complete transformation from prototype to production-ready application with enterprise-grade features and magical user experience.
2025-08-30 17:32:42 +00:00

187 lines
6.4 KiB
Python

"""
Telemetry collection for LifeRPG - opt-in anonymous usage analytics.
"""
from datetime import datetime, timezone
from typing import Dict, Optional, Any
from sqlalchemy.orm import Session
import models
import json
import os
def is_telemetry_enabled() -> bool:
"""Check if telemetry is enabled globally."""
return os.getenv('TELEMETRY_ENABLED', 'true').lower() in ('true', '1', 'yes', 'on')
def has_user_consented(db: Session, user_id: int) -> bool:
"""Check if user has consented to telemetry."""
if not is_telemetry_enabled():
return False
consent = db.query(models.Profile).filter(
models.Profile.user_id == user_id,
models.Profile.key == 'telemetry_consent'
).first()
return consent and consent.value == 'true'
def set_user_consent(db: Session, user_id: int, consent: bool) -> None:
"""Set user's telemetry consent."""
existing = db.query(models.Profile).filter(
models.Profile.user_id == user_id,
models.Profile.key == 'telemetry_consent'
).first()
if existing:
existing.value = 'true' if consent else 'false'
else:
profile = models.Profile(
user_id=user_id,
key='telemetry_consent',
value='true' if consent else 'false'
)
db.add(profile)
db.commit()
def record_event(db: Session, user_id: Optional[int], event_name: str, properties: Optional[Dict[str, Any]] = None) -> bool:
"""Record a telemetry event if user has consented."""
# Check if telemetry is enabled globally
if not is_telemetry_enabled():
return False
# For anonymous events (user_id = None), always record if globally enabled
if user_id is not None and not has_user_consented(db, user_id):
return False
# Sanitize properties to remove PII
safe_properties = sanitize_properties(properties or {})
event = models.TelemetryEvent(
user_id=user_id,
name=event_name,
payload=json.dumps(safe_properties)
)
db.add(event)
try:
db.commit()
return True
except Exception:
db.rollback()
return False
def sanitize_properties(properties: Dict[str, Any]) -> Dict[str, Any]:
"""Remove or hash any potentially identifying information."""
safe_props = {}
# List of allowed property keys that are safe to collect
allowed_keys = {
'action', 'category', 'label', 'value', 'duration', 'count',
'habit_difficulty', 'habit_cadence', 'achievement_type',
'integration_provider', 'feature_used', 'error_type',
'platform', 'version', 'browser', 'screen_resolution',
'habit_count', 'completion_count', 'streak_length'
}
for key, value in properties.items():
if key in allowed_keys:
# Further sanitize the value
if isinstance(value, str) and len(value) > 100:
# Truncate long strings
safe_props[key] = value[:100]
elif isinstance(value, (int, float, bool)):
safe_props[key] = value
elif isinstance(value, str):
safe_props[key] = value
return safe_props
# Pre-defined event helpers
def record_habit_completion(db: Session, user_id: int, habit_difficulty: int, xp_awarded: int) -> bool:
"""Record a habit completion event."""
return record_event(db, user_id, 'habit_completed', {
'habit_difficulty': habit_difficulty,
'xp_awarded': xp_awarded
})
def record_achievement_earned(db: Session, user_id: int, achievement_type: str, xp_awarded: int) -> bool:
"""Record an achievement earned event."""
return record_event(db, user_id, 'achievement_earned', {
'achievement_type': achievement_type,
'xp_awarded': xp_awarded
})
def record_level_up(db: Session, user_id: int, old_level: int, new_level: int) -> bool:
"""Record a level up event."""
return record_event(db, user_id, 'level_up', {
'old_level': old_level,
'new_level': new_level
})
def record_habit_created(db: Session, user_id: int, habit_difficulty: int, habit_cadence: str) -> bool:
"""Record a habit creation event."""
return record_event(db, user_id, 'habit_created', {
'habit_difficulty': habit_difficulty,
'habit_cadence': habit_cadence
})
def record_integration_sync(db: Session, user_id: int, provider: str, items_synced: int, success: bool) -> bool:
"""Record an integration sync event."""
return record_event(db, user_id, 'integration_sync', {
'integration_provider': provider,
'items_synced': items_synced,
'success': success
})
def record_feature_usage(db: Session, user_id: int, feature: str, duration_seconds: Optional[int] = None) -> bool:
"""Record a feature usage event."""
properties = {'feature_used': feature}
if duration_seconds is not None:
properties['duration'] = duration_seconds
return record_event(db, user_id, 'feature_used', properties)
def record_error(db: Session, user_id: Optional[int], error_type: str, context: Optional[str] = None) -> bool:
"""Record an error event (can be anonymous)."""
properties = {'error_type': error_type}
if context:
properties['context'] = context[:50] # Truncate context
return record_event(db, user_id, 'error_occurred', properties)
def get_telemetry_stats(db: Session, days: int = 30) -> Dict:
"""Get aggregated telemetry statistics for admin purposes."""
from datetime import timedelta
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
# Count events by type
event_counts = db.query(
models.TelemetryEvent.name,
db.func.count(models.TelemetryEvent.id).label('count')
).filter(
models.TelemetryEvent.created_at >= cutoff
).group_by(
models.TelemetryEvent.name
).all()
# Count unique users (approximate)
unique_users = db.query(models.TelemetryEvent.user_id).filter(
models.TelemetryEvent.created_at >= cutoff,
models.TelemetryEvent.user_id.isnot(None)
).distinct().count()
# Total events
total_events = db.query(models.TelemetryEvent).filter(
models.TelemetryEvent.created_at >= cutoff
).count()
return {
'period_days': days,
'total_events': total_events,
'unique_users': unique_users,
'events_by_type': {event.name: event.count for event in event_counts},
'telemetry_enabled': is_telemetry_enabled()
}