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>
This commit is contained in:
parent
e6cca4ec19
commit
0337d1a0bb
229
app.py
229
app.py
@ -1106,233 +1106,10 @@ def health_full():
|
||||
|
||||
|
||||
# ============================================================
|
||||
# USER ANALYTICS API ROUTES
|
||||
# USER ANALYTICS API ROUTES - MOVED TO blueprints/api/routes_analytics.py
|
||||
# ============================================================
|
||||
|
||||
@app.route('/api/analytics/track', methods=['POST'])
|
||||
@csrf.exempt
|
||||
def api_analytics_track():
|
||||
"""Track clicks and interactions from frontend"""
|
||||
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()
|
||||
|
||||
|
||||
@app.route('/api/analytics/heartbeat', methods=['POST'])
|
||||
@csrf.exempt
|
||||
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()
|
||||
|
||||
|
||||
@app.route('/api/analytics/scroll', methods=['POST'])
|
||||
@csrf.exempt
|
||||
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()
|
||||
|
||||
|
||||
@app.route('/api/analytics/error', methods=['POST'])
|
||||
@csrf.exempt
|
||||
def api_analytics_error():
|
||||
"""Track JavaScript errors from frontend"""
|
||||
import hashlib
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@app.route('/api/analytics/performance', methods=['POST'])
|
||||
@csrf.exempt
|
||||
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()
|
||||
|
||||
|
||||
@app.route('/api/analytics/conversion', methods=['POST'])
|
||||
@csrf.exempt
|
||||
def api_analytics_conversion():
|
||||
"""Track conversion events from frontend (contact clicks)"""
|
||||
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
|
||||
|
||||
# Routes: /api/analytics/track, /api/analytics/heartbeat, /api/analytics/scroll,
|
||||
# /api/analytics/error, /api/analytics/performance, /api/analytics/conversion
|
||||
|
||||
# ============================================================
|
||||
# RECOMMENDATIONS API ROUTES
|
||||
|
||||
@ -27,6 +27,18 @@ def register_blueprints(app):
|
||||
except ImportError as e:
|
||||
logger.debug(f"Blueprint reports not yet available: {e}")
|
||||
|
||||
# API blueprint (analytics tracking)
|
||||
try:
|
||||
from blueprints.api import bp as api_bp
|
||||
from blueprints.api.routes_analytics import exempt_from_csrf
|
||||
app.register_blueprint(api_bp)
|
||||
exempt_from_csrf(app)
|
||||
logger.info("Registered blueprint: api (with CSRF exemption)")
|
||||
except ImportError as e:
|
||||
logger.debug(f"Blueprint api not yet available: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error registering api blueprint: {e}")
|
||||
|
||||
# Community blueprints - register directly (not nested)
|
||||
# to preserve endpoint names like 'calendar_index' instead of 'community.calendar.calendar_index'
|
||||
try:
|
||||
|
||||
12
blueprints/api/__init__.py
Normal file
12
blueprints/api/__init__.py
Normal file
@ -0,0 +1,12 @@
|
||||
"""
|
||||
API Blueprint
|
||||
==============
|
||||
|
||||
Public API routes for analytics tracking and other frontend interactions.
|
||||
"""
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
bp = Blueprint('api', __name__, url_prefix='/api')
|
||||
|
||||
from . import routes_analytics # noqa: E402, F401
|
||||
250
blueprints/api/routes_analytics.py
Normal file
250
blueprints/api/routes_analytics.py
Normal file
@ -0,0 +1,250 @@
|
||||
"""
|
||||
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
|
||||
Loading…
Reference in New Issue
Block a user