- 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>
251 lines
8.6 KiB
Python
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
|