File size: 8,613 Bytes
c509b44
 
 
59f118a
 
c509b44
 
b65ef75
c16e1c9
 
59f118a
 
 
 
 
 
 
 
 
 
 
0452a50
59f118a
 
 
 
 
0452a50
 
 
 
 
 
 
 
 
 
 
 
c16e1c9
 
aa63765
c16e1c9
c509b44
b65ef75
 
c16e1c9
 
 
c509b44
c16e1c9
 
 
 
b65ef75
c16e1c9
0452a50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c509b44
 
 
 
 
 
c16e1c9
 
 
c509b44
 
 
 
 
 
c16e1c9
 
 
 
aa63765
c16e1c9
c509b44
b65ef75
 
c16e1c9
 
c509b44
 
c16e1c9
 
 
 
b65ef75
c16e1c9
0452a50
 
 
 
 
 
 
 
c509b44
 
 
c16e1c9
 
c509b44
 
c16e1c9
 
 
aa63765
c16e1c9
c509b44
 
b65ef75
 
c16e1c9
 
 
c509b44
c16e1c9
 
 
 
b65ef75
c16e1c9
0452a50
 
 
 
 
 
 
 
c509b44
 
 
 
 
 
 
c16e1c9
 
 
c509b44
 
c16e1c9
 
 
aa63765
c16e1c9
c509b44
b65ef75
 
c16e1c9
 
 
adf80ee
c509b44
 
 
 
b65ef75
c509b44
0452a50
 
 
 
 
 
 
 
 
 
 
 
 
 
c509b44
 
adf80ee
 
 
c509b44
 
 
 
adf80ee
c509b44
 
 
 
 
 
 
b65ef75
 
c509b44
 
 
 
c16e1c9
 
 
 
b65ef75
c16e1c9
0452a50
 
 
 
 
 
 
 
 
 
 
 
 
 
c509b44
 
 
c16e1c9
 
c509b44
 
c16e1c9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
from fastapi import APIRouter, Header, HTTPException, Query
from typing import Optional
from datetime import datetime, timedelta
import logging
import os

from ..storage.analytics_store import AnalyticsStore
from ..utils.access_control import require_api_permission

router = APIRouter()
logger = logging.getLogger(__name__)

# Initialize analytics store, but don't crash the app if Supabase is not available.
# In environments like Hugging Face Spaces where Supabase isn't configured,
# we disable analytics gracefully instead of raising at import time.
try:
    # Only attempt to initialize Supabase analytics when credentials are present
    if os.getenv("SUPABASE_URL") and os.getenv("SUPABASE_SERVICE_KEY"):
        analytics_store: Optional[AnalyticsStore] = AnalyticsStore()
    else:
        analytics_store = None
        logger.debug(
            "AnalyticsStore: Supabase credentials not configured. "
            "Analytics endpoints will return 503."
        )
except RuntimeError as exc:
    analytics_store = None
    # Only log at warning level if credentials are configured (actual error)
    # Otherwise log at debug level (expected when Supabase is not configured)
    if os.getenv("SUPABASE_URL") and os.getenv("SUPABASE_SERVICE_KEY"):
        logger.warning(
            "AnalyticsStore initialization failed (%s). Analytics endpoints will return 503.",
            str(exc).split('\n')[0],  # Only first line
        )
    else:
        logger.debug(
            "AnalyticsStore not configured (%s). Analytics endpoints will return 503.",
            str(exc).split('\n')[0],
        )


@router.get("/overview")
async def analytics_overview(
    x_tenant_id: str = Header(None),
    days: int = Query(30, description="Number of days to look back"),
    x_user_role: str = Header("viewer")
):
    """
    Returns an overview of analytics for the dashboard.
    Includes total queries, tool usage, red-flag count, and active users.
    """

    if not x_tenant_id:
        raise HTTPException(status_code=400, detail="Missing tenant ID")
    require_api_permission(x_user_role, "view_analytics")

    # Return empty data if analytics is not configured (instead of 503)
    if analytics_store is None:
        return {
            "tenant_id": x_tenant_id,
            "overview": {
                "total_queries": 0,
                "tool_usage": {},
                "redflag_count": 0,
                "active_users": 0,
                "last_query": None,
                "rag_quality": {
                    "total_searches": 0,
                    "avg_hits_per_search": 0,
                    "avg_score": 0.0,
                    "avg_top_score": 0.0,
                    "avg_latency_ms": 0.0
                }
            }
        }

    since_timestamp = int((datetime.now() - timedelta(days=days)).timestamp()) if days else None
    
    tool_usage = analytics_store.get_tool_usage_stats(x_tenant_id, since_timestamp)
    activity = analytics_store.get_activity_summary(x_tenant_id, since_timestamp)
    rag_quality = analytics_store.get_rag_quality_metrics(x_tenant_id, since_timestamp)

    return {
        "tenant_id": x_tenant_id,
        "overview": {
            "total_queries": activity["total_queries"],
            "tool_usage": tool_usage,
            "redflag_count": activity["redflag_count"],
            "active_users": activity["active_users"],
            "last_query": activity["last_query"],
            "rag_quality": rag_quality
        }
    }


@router.get("/tool-usage")
async def analytics_tool_usage(
    x_tenant_id: str = Header(None),
    days: int = Query(30, description="Number of days to look back"),
    x_user_role: str = Header("viewer")
):
    """
    Returns how often each tool (RAG, Web, Admin, LLM) was used with detailed stats.
    Includes counts, latency, tokens, and success/error rates.
    """

    if not x_tenant_id:
        raise HTTPException(status_code=400, detail="Missing tenant ID")
    require_api_permission(x_user_role, "view_analytics")

    # Return empty data if analytics is not configured (instead of 503)
    if analytics_store is None:
        return {
            "tenant_id": x_tenant_id,
            "tool_usage": {},
            "period_days": days
        }

    since_timestamp = int((datetime.now() - timedelta(days=days)).timestamp()) if days else None
    tool_usage = analytics_store.get_tool_usage_stats(x_tenant_id, since_timestamp)

    return {
        "tenant_id": x_tenant_id,
        "tool_usage": tool_usage,
        "period_days": days
    }


@router.get("/redflags")
async def analytics_redflags(
    x_tenant_id: str = Header(None),
    limit: int = Query(50, description="Maximum number of violations to return"),
    days: int = Query(30, description="Number of days to look back"),
    x_user_role: str = Header("viewer")
):
    """
    Returns red-flag violations for this tenant.
    Includes rule details, severity, confidence, and timestamps.
    """

    if not x_tenant_id:
        raise HTTPException(status_code=400, detail="Missing tenant ID")
    require_api_permission(x_user_role, "view_analytics")

    # Return empty data if analytics is not configured (instead of 503)
    if analytics_store is None:
        return {
            "tenant_id": x_tenant_id,
            "redflags": [],
            "count": 0
        }

    since_timestamp = int((datetime.now() - timedelta(days=days)).timestamp()) if days else None
    redflags = analytics_store.get_redflag_violations(x_tenant_id, limit, since_timestamp)

    # Convert timestamps to ISO format
    for violation in redflags:
        if "timestamp" in violation:
            violation["timestamp_iso"] = datetime.fromtimestamp(violation["timestamp"]).isoformat()

    return {
        "tenant_id": x_tenant_id,
        "redflags": redflags,
        "count": len(redflags)
    }


@router.get("/activity")
async def analytics_activity(
    x_tenant_id: str = Header(None),
    days: int = Query(30, description="Number of days to look back"),
    x_user_role: str = Header("viewer")
):
    """
    Returns general tenant activity statistics.
    Includes total queries, active users, last query timestamp, and individual activity records for heatmap visualization.
    """

    if not x_tenant_id:
        raise HTTPException(status_code=400, detail="Missing tenant ID")
    require_api_permission(x_user_role, "view_analytics")

    # Return empty data if analytics is not configured (instead of 503)
    if analytics_store is None:
        return {
            "tenant_id": x_tenant_id,
            "activity": {
                "total_queries": 0,
                "active_users": 0,
                "redflag_count": 0,
                "last_query": None
            },
            "activities": [],
            "period_days": days
        }

    since_timestamp = int((datetime.now() - timedelta(days=days)).timestamp()) if days else None
    activity = analytics_store.get_activity_summary(x_tenant_id, since_timestamp)
    
    # Also fetch individual activity records for heatmap visualization
    activities = analytics_store.get_activity_records(x_tenant_id, since_timestamp)

    return {
        "tenant_id": x_tenant_id,
        "activity": activity,
        "activities": activities,  # Individual records with timestamps for heatmap
        "period_days": days
    }


@router.get("/rag-quality")
async def analytics_rag_quality(
    x_tenant_id: str = Header(None),
    days: int = Query(30, description="Number of days to look back"),
    x_user_role: str = Header("viewer")
):
    """
    Returns RAG quality metrics including recall/precision indicators.
    Includes average hits, scores, and latency.
    """

    if not x_tenant_id:
        raise HTTPException(status_code=400, detail="Missing tenant ID")
    require_api_permission(x_user_role, "view_analytics")

    # Return empty data if analytics is not configured (instead of 503)
    if analytics_store is None:
        return {
            "tenant_id": x_tenant_id,
            "rag_quality": {
                "total_searches": 0,
                "avg_hits_per_search": 0,
                "avg_score": 0.0,
                "avg_top_score": 0.0,
                "avg_latency_ms": 0.0
            },
            "period_days": days
        }

    since_timestamp = int((datetime.now() - timedelta(days=days)).timestamp()) if days else None
    rag_quality = analytics_store.get_rag_quality_metrics(x_tenant_id, since_timestamp)

    return {
        "tenant_id": x_tenant_id,
        "rag_quality": rag_quality,
        "period_days": days
    }