Files
Juan Denis 97a96d963b Give common API errors stable codes and service wording
Toasts showed backend prose verbatim, with no code to translate
against and "application" where the UI says "service". Not-found,
missing-field, permission, conflict and agent-offline errors now come
from factories in app/exceptions.py that pair one fixed message with
a dotted code (not_found.<resource>, validation.required,
permission.denied, conflict.exists, agent.offline), so the client can
translate them.

The shared app-loading helpers and the apps, domains, servers,
databases and backups routes raise those errors instead of hand
shaping bodies, which drops the hand-shaped body count from 1132 to
850. Five routes picked an HTTP status by comparing error text; they
now key off a result code or a sentinel so rewording can't change a
status. Five broad excepts that echoed str(exc) now log the crash and
return the request id instead.
2026-10-04 18:59:25 -04:00

61 lines
2.0 KiB
Python

"""API analytics endpoints."""
from flask import Blueprint, jsonify, request
from app.middleware.rbac import admin_required, auth_required, get_current_user
from app.services.api_analytics_service import ApiAnalyticsService
from app.services.api_key_service import ApiKeyService
from app.exceptions import permission_denied
api_analytics_bp = Blueprint('api_analytics', __name__)
@api_analytics_bp.route('/overview', methods=['GET'])
@admin_required
def overview():
"""Get API usage overview."""
period = request.args.get('period', '24h')
return jsonify(ApiAnalyticsService.get_overview(period))
@api_analytics_bp.route('/endpoints', methods=['GET'])
@admin_required
def endpoints():
"""Get top endpoints by usage."""
period = request.args.get('period', '24h')
limit = request.args.get('limit', 20, type=int)
return jsonify({'endpoints': ApiAnalyticsService.get_endpoint_stats(period, limit)})
@api_analytics_bp.route('/errors', methods=['GET'])
@admin_required
def errors():
"""Get error breakdown."""
period = request.args.get('period', '24h')
return jsonify({'errors': ApiAnalyticsService.get_error_stats(period)})
@api_analytics_bp.route('/timeseries', methods=['GET'])
@admin_required
def timeseries():
"""Get time series data for charts."""
period = request.args.get('period', '24h')
interval = request.args.get('interval', 'hour')
return jsonify({'data': ApiAnalyticsService.get_time_series(period, interval)})
@api_analytics_bp.route('/keys/<int:key_id>/usage', methods=['GET'])
@auth_required()
def key_usage(key_id):
"""Get usage stats for a specific API key."""
period = request.args.get('period', '24h')
# Allow key owners to view their own usage
user = get_current_user()
api_key = ApiKeyService.get_key(key_id)
if not api_key:
return jsonify({'error': 'API key not found'}), 404
if not user.is_admin and api_key.user_id != user.id:
raise permission_denied()
return jsonify(ApiAnalyticsService.get_key_usage(key_id, period))