nordabiz/blueprints/api/routes_analytics.py
Maciej Pienczyn 0337d1a0bb refactor: Migrate analytics API routes to api blueprint
- Created blueprints/api/ with 6 routes:
  - /api/analytics/track
  - /api/analytics/heartbeat
  - /api/analytics/scroll
  - /api/analytics/error
  - /api/analytics/performance
  - /api/analytics/conversion
- Added CSRF exemption for analytics routes
- Removed ~230 lines from app.py (7729 -> 7506)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 17:35:08 +01:00

251 lines
8.6 KiB
Python

"""
Analytics API Routes - API blueprint
Migrated from app.py as part of the blueprint refactoring.
Contains public API routes for user analytics tracking.
"""
import hashlib
import logging
from datetime import datetime
from flask import jsonify, request, session, current_app
from flask_login import current_user
from database import SessionLocal, UserSession, UserClick, PageView, JSError
from . import bp
logger = logging.getLogger(__name__)
def exempt_from_csrf(app):
"""Exempt analytics routes from CSRF protection."""
csrf = app.extensions.get('csrf')
if csrf:
csrf.exempt(bp)
# ============================================================
# USER ANALYTICS API ROUTES
# ============================================================
@bp.route('/analytics/track', methods=['POST'])
def api_analytics_track():
"""Track clicks and interactions from frontend"""
# Exempt from CSRF for analytics tracking
data = request.get_json()
if not data:
return jsonify({'error': 'No data'}), 400
analytics_session_id = session.get('analytics_session_id')
if not analytics_session_id:
return jsonify({'error': 'No session'}), 400
db = SessionLocal()
try:
user_session = db.query(UserSession).filter_by(session_id=analytics_session_id).first()
if not user_session:
return jsonify({'error': 'Session not found'}), 404
event_type = data.get('type')
if event_type == 'click':
click = UserClick(
session_id=user_session.id,
page_view_id=data.get('page_view_id'),
user_id=current_user.id if current_user.is_authenticated else None,
element_type=data.get('element_type', '')[:50] if data.get('element_type') else None,
element_id=data.get('element_id', '')[:100] if data.get('element_id') else None,
element_text=(data.get('element_text', '') or '')[:255],
element_class=(data.get('element_class', '') or '')[:500],
target_url=data.get('target_url', '')[:2000] if data.get('target_url') else None,
x_position=data.get('x'),
y_position=data.get('y')
)
db.add(click)
user_session.clicks_count = (user_session.clicks_count or 0) + 1
db.commit()
elif event_type == 'page_time':
# Update time on page
page_view_id = data.get('page_view_id')
time_seconds = data.get('time_seconds')
if page_view_id and time_seconds:
page_view = db.query(PageView).filter_by(id=page_view_id).first()
if page_view:
page_view.time_on_page_seconds = min(time_seconds, 86400) # Max 24h
db.commit()
return jsonify({'success': True}), 200
except Exception as e:
logger.error(f"Analytics track error: {e}")
db.rollback()
return jsonify({'error': 'Internal error'}), 500
finally:
db.close()
@bp.route('/analytics/heartbeat', methods=['POST'])
def api_analytics_heartbeat():
"""Keep session alive and update duration"""
analytics_session_id = session.get('analytics_session_id')
if not analytics_session_id:
return jsonify({'success': False}), 200
db = SessionLocal()
try:
user_session = db.query(UserSession).filter_by(session_id=analytics_session_id).first()
if user_session:
user_session.last_activity_at = datetime.now()
user_session.duration_seconds = int(
(datetime.now() - user_session.started_at).total_seconds()
)
db.commit()
return jsonify({'success': True}), 200
except Exception as e:
logger.error(f"Analytics heartbeat error: {e}")
db.rollback()
return jsonify({'success': False}), 200
finally:
db.close()
@bp.route('/analytics/scroll', methods=['POST'])
def api_analytics_scroll():
"""Track scroll depth from frontend"""
data = request.get_json()
if not data:
return jsonify({'error': 'No data'}), 400
page_view_id = data.get('page_view_id')
scroll_depth = data.get('scroll_depth')
if not page_view_id or scroll_depth is None:
return jsonify({'error': 'Missing data'}), 400
db = SessionLocal()
try:
page_view = db.query(PageView).filter_by(id=page_view_id).first()
if page_view:
# Zapisz tylko jeśli większe niż poprzednie (max scroll depth)
current_depth = page_view.scroll_depth_percent or 0
if scroll_depth > current_depth:
page_view.scroll_depth_percent = min(scroll_depth, 100)
db.commit()
return jsonify({'success': True}), 200
except Exception as e:
logger.error(f"Analytics scroll error: {e}")
db.rollback()
return jsonify({'success': False}), 200
finally:
db.close()
@bp.route('/analytics/error', methods=['POST'])
def api_analytics_error():
"""Track JavaScript errors from frontend"""
data = request.get_json()
if not data:
return jsonify({'error': 'No data'}), 400
message = data.get('message', '')[:2000]
if not message:
return jsonify({'error': 'No message'}), 400
analytics_session_id = session.get('analytics_session_id')
db = SessionLocal()
try:
# Znajdź session ID
session_db_id = None
if analytics_session_id:
user_session = db.query(UserSession).filter_by(session_id=analytics_session_id).first()
if user_session:
session_db_id = user_session.id
# Utwórz hash dla agregacji
error_key = f"{message}|{data.get('source', '')}|{data.get('lineno', '')}"
error_hash = hashlib.sha256(error_key.encode()).hexdigest()
js_error = JSError(
session_id=session_db_id,
message=message,
source=data.get('source', '')[:500] if data.get('source') else None,
lineno=data.get('lineno'),
colno=data.get('colno'),
stack=data.get('stack', '')[:5000] if data.get('stack') else None,
url=data.get('url', '')[:2000] if data.get('url') else None,
user_agent=request.headers.get('User-Agent', '')[:500],
error_hash=error_hash
)
db.add(js_error)
db.commit()
return jsonify({'success': True}), 200
except Exception as e:
logger.error(f"Analytics error tracking error: {e}")
db.rollback()
return jsonify({'success': False}), 200
finally:
db.close()
@bp.route('/analytics/performance', methods=['POST'])
def api_analytics_performance():
"""Track page performance metrics from frontend"""
data = request.get_json()
if not data:
return jsonify({'error': 'No data'}), 400
page_view_id = data.get('page_view_id')
if not page_view_id:
return jsonify({'error': 'Missing page_view_id'}), 400
db = SessionLocal()
try:
page_view = db.query(PageView).filter_by(id=page_view_id).first()
if page_view:
# Zapisz metryki performance (tylko jeśli jeszcze nie zapisane)
if page_view.dom_content_loaded_ms is None:
page_view.dom_content_loaded_ms = data.get('dom_content_loaded_ms')
if page_view.load_time_ms is None:
page_view.load_time_ms = data.get('load_time_ms')
if page_view.first_paint_ms is None:
page_view.first_paint_ms = data.get('first_paint_ms')
if page_view.first_contentful_paint_ms is None:
page_view.first_contentful_paint_ms = data.get('first_contentful_paint_ms')
db.commit()
return jsonify({'success': True}), 200
except Exception as e:
logger.error(f"Analytics performance error: {e}")
db.rollback()
return jsonify({'success': False}), 200
finally:
db.close()
@bp.route('/analytics/conversion', methods=['POST'])
def api_analytics_conversion():
"""Track conversion events from frontend (contact clicks)"""
# Import track_conversion from app module
from app import track_conversion
data = request.get_json()
if not data:
return jsonify({'error': 'No data'}), 400
event_type = data.get('event_type')
if not event_type:
return jsonify({'error': 'Missing event_type'}), 400
track_conversion(
event_type=event_type,
company_id=data.get('company_id'),
target_type=data.get('target_type'),
target_value=data.get('target_value'),
metadata=data.get('metadata')
)
return jsonify({'success': True}), 200