Spaces:
Sleeping
Sleeping
Commit
Β·
d74c0dc
1
Parent(s):
80ebded
refactor(analytics): Remove SQLite fallback, require Supabase exclusively
Browse files- backend/api/storage/analytics_store.py +214 -484
- check_env.py +106 -0
- data/analytics.db +0 -0
- test_key.py +45 -0
- test_supabase_connection.py +81 -0
- verify_supabase_setup.py +45 -1
backend/api/storage/analytics_store.py
CHANGED
|
@@ -10,8 +10,8 @@ Tracks:
|
|
| 10 |
"""
|
| 11 |
|
| 12 |
import json
|
|
|
|
| 13 |
import os
|
| 14 |
-
import sqlite3
|
| 15 |
import time
|
| 16 |
from datetime import datetime
|
| 17 |
from pathlib import Path
|
|
@@ -25,13 +25,16 @@ except ImportError:
|
|
| 25 |
Client = None # type: ignore
|
| 26 |
SUPABASE_AVAILABLE = False
|
| 27 |
|
|
|
|
|
|
|
| 28 |
|
| 29 |
class AnalyticsStore:
|
| 30 |
"""
|
| 31 |
-
Analytics logging with
|
| 32 |
|
| 33 |
-
-
|
| 34 |
-
-
|
|
|
|
| 35 |
"""
|
| 36 |
|
| 37 |
def __init__(
|
|
@@ -40,43 +43,50 @@ class AnalyticsStore:
|
|
| 40 |
use_supabase: Optional[bool] = None,
|
| 41 |
auto_create_tables: bool = False,
|
| 42 |
):
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
self._tables_verified = False
|
| 45 |
self.supabase_client: Optional[Client] = None
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
| 52 |
)
|
| 53 |
-
|
| 54 |
-
if
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
data_dir = root_dir / "data"
|
| 64 |
-
data_dir.mkdir(parents=True, exist_ok=True)
|
| 65 |
-
self.db_path = data_dir / "analytics.db"
|
| 66 |
-
else:
|
| 67 |
-
self.db_path = Path(db_path)
|
| 68 |
-
|
| 69 |
-
self._init_db()
|
| 70 |
|
| 71 |
def _init_supabase(self, auto_create_tables: bool):
|
|
|
|
| 72 |
supabase_url = os.getenv("SUPABASE_URL")
|
| 73 |
supabase_key = os.getenv("SUPABASE_SERVICE_KEY")
|
| 74 |
|
| 75 |
if not supabase_url or not supabase_key:
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
|
| 81 |
try:
|
| 82 |
self.supabase_client = create_client(supabase_url, supabase_key)
|
|
@@ -91,10 +101,19 @@ class AnalyticsStore:
|
|
| 91 |
self._ensure_supabase_tables()
|
| 92 |
else:
|
| 93 |
self._quick_table_check()
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
|
| 99 |
def _quick_table_check(self):
|
| 100 |
"""Verify that all expected Supabase tables exist."""
|
|
@@ -124,109 +143,6 @@ class AnalyticsStore:
|
|
| 124 |
else:
|
| 125 |
print(" Missing supabase_analytics_tables.sql in repo root.")
|
| 126 |
|
| 127 |
-
def _init_db(self):
|
| 128 |
-
"""Initialize database tables for analytics."""
|
| 129 |
-
with sqlite3.connect(self.db_path) as conn:
|
| 130 |
-
# Tool usage events table
|
| 131 |
-
conn.execute(
|
| 132 |
-
"""
|
| 133 |
-
CREATE TABLE IF NOT EXISTS tool_usage_events (
|
| 134 |
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 135 |
-
tenant_id TEXT NOT NULL,
|
| 136 |
-
user_id TEXT,
|
| 137 |
-
tool_name TEXT NOT NULL,
|
| 138 |
-
timestamp INTEGER NOT NULL,
|
| 139 |
-
latency_ms INTEGER,
|
| 140 |
-
tokens_used INTEGER,
|
| 141 |
-
success BOOLEAN DEFAULT 1,
|
| 142 |
-
error_message TEXT,
|
| 143 |
-
metadata TEXT
|
| 144 |
-
)
|
| 145 |
-
"""
|
| 146 |
-
)
|
| 147 |
-
|
| 148 |
-
# Red-flag violations table
|
| 149 |
-
conn.execute(
|
| 150 |
-
"""
|
| 151 |
-
CREATE TABLE IF NOT EXISTS redflag_violations (
|
| 152 |
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 153 |
-
tenant_id TEXT NOT NULL,
|
| 154 |
-
user_id TEXT,
|
| 155 |
-
rule_id TEXT NOT NULL,
|
| 156 |
-
rule_pattern TEXT,
|
| 157 |
-
severity TEXT NOT NULL,
|
| 158 |
-
matched_text TEXT,
|
| 159 |
-
confidence REAL,
|
| 160 |
-
message_preview TEXT,
|
| 161 |
-
timestamp INTEGER NOT NULL
|
| 162 |
-
)
|
| 163 |
-
"""
|
| 164 |
-
)
|
| 165 |
-
|
| 166 |
-
# RAG search events with quality metrics
|
| 167 |
-
conn.execute(
|
| 168 |
-
"""
|
| 169 |
-
CREATE TABLE IF NOT EXISTS rag_search_events (
|
| 170 |
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 171 |
-
tenant_id TEXT NOT NULL,
|
| 172 |
-
query TEXT NOT NULL,
|
| 173 |
-
hits_count INTEGER,
|
| 174 |
-
avg_score REAL,
|
| 175 |
-
top_score REAL,
|
| 176 |
-
timestamp INTEGER NOT NULL,
|
| 177 |
-
latency_ms INTEGER
|
| 178 |
-
)
|
| 179 |
-
"""
|
| 180 |
-
)
|
| 181 |
-
|
| 182 |
-
# Agent query events (overall query tracking)
|
| 183 |
-
conn.execute(
|
| 184 |
-
"""
|
| 185 |
-
CREATE TABLE IF NOT EXISTS agent_query_events (
|
| 186 |
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 187 |
-
tenant_id TEXT NOT NULL,
|
| 188 |
-
user_id TEXT,
|
| 189 |
-
message_preview TEXT,
|
| 190 |
-
intent TEXT,
|
| 191 |
-
tools_used TEXT,
|
| 192 |
-
total_tokens INTEGER,
|
| 193 |
-
total_latency_ms INTEGER,
|
| 194 |
-
success BOOLEAN DEFAULT 1,
|
| 195 |
-
timestamp INTEGER NOT NULL
|
| 196 |
-
)
|
| 197 |
-
"""
|
| 198 |
-
)
|
| 199 |
-
|
| 200 |
-
# Create indexes separately (SQLite doesn't support inline INDEX in CREATE TABLE)
|
| 201 |
-
conn.execute(
|
| 202 |
-
"""
|
| 203 |
-
CREATE INDEX IF NOT EXISTS idx_tool_usage_tenant_timestamp
|
| 204 |
-
ON tool_usage_events(tenant_id, timestamp)
|
| 205 |
-
"""
|
| 206 |
-
)
|
| 207 |
-
|
| 208 |
-
conn.execute(
|
| 209 |
-
"""
|
| 210 |
-
CREATE INDEX IF NOT EXISTS idx_redflag_tenant_timestamp
|
| 211 |
-
ON redflag_violations(tenant_id, timestamp)
|
| 212 |
-
"""
|
| 213 |
-
)
|
| 214 |
-
|
| 215 |
-
conn.execute(
|
| 216 |
-
"""
|
| 217 |
-
CREATE INDEX IF NOT EXISTS idx_rag_search_tenant_timestamp
|
| 218 |
-
ON rag_search_events(tenant_id, timestamp)
|
| 219 |
-
"""
|
| 220 |
-
)
|
| 221 |
-
|
| 222 |
-
conn.execute(
|
| 223 |
-
"""
|
| 224 |
-
CREATE INDEX IF NOT EXISTS idx_agent_query_tenant_timestamp
|
| 225 |
-
ON agent_query_events(tenant_id, timestamp)
|
| 226 |
-
"""
|
| 227 |
-
)
|
| 228 |
-
|
| 229 |
-
conn.commit()
|
| 230 |
|
| 231 |
# ------------------------------------------------------------------
|
| 232 |
# Logging helpers
|
|
@@ -247,12 +163,38 @@ class AnalyticsStore:
|
|
| 247 |
return None
|
| 248 |
|
| 249 |
def _supabase_insert(self, table: str, payload: Dict[str, Any]):
|
|
|
|
| 250 |
if not self.supabase_client:
|
| 251 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
try:
|
| 253 |
-
self.supabase_client.table(table).insert(payload).execute()
|
| 254 |
-
|
| 255 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
|
| 257 |
def _supabase_simple_select(
|
| 258 |
self,
|
|
@@ -321,42 +263,22 @@ class AnalyticsStore:
|
|
| 321 |
metadata: Optional[Dict[str, Any]] = None,
|
| 322 |
user_id: Optional[str] = None
|
| 323 |
):
|
| 324 |
-
"""Log a tool usage event."""
|
| 325 |
-
if
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
with sqlite3.connect(self.db_path) as conn:
|
| 341 |
-
conn.execute(
|
| 342 |
-
"""
|
| 343 |
-
INSERT INTO tool_usage_events
|
| 344 |
-
(tenant_id, user_id, tool_name, timestamp, latency_ms, tokens_used, success, error_message, metadata)
|
| 345 |
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 346 |
-
""",
|
| 347 |
-
(
|
| 348 |
-
tenant_id,
|
| 349 |
-
user_id,
|
| 350 |
-
tool_name,
|
| 351 |
-
self._now_ts(),
|
| 352 |
-
latency_ms,
|
| 353 |
-
tokens_used,
|
| 354 |
-
1 if success else 0,
|
| 355 |
-
error_message,
|
| 356 |
-
self._serialize_metadata(metadata),
|
| 357 |
-
),
|
| 358 |
-
)
|
| 359 |
-
conn.commit()
|
| 360 |
|
| 361 |
def log_redflag_violation(
|
| 362 |
self,
|
|
@@ -369,44 +291,23 @@ class AnalyticsStore:
|
|
| 369 |
message_preview: Optional[str] = None,
|
| 370 |
user_id: Optional[str] = None
|
| 371 |
):
|
| 372 |
-
"""Log a red-flag violation."""
|
|
|
|
|
|
|
|
|
|
| 373 |
truncated_message = message_preview[:200] if message_preview else None
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
}
|
| 387 |
-
self._supabase_insert(self.table_names["redflags"], payload)
|
| 388 |
-
return
|
| 389 |
-
|
| 390 |
-
with sqlite3.connect(self.db_path) as conn:
|
| 391 |
-
conn.execute(
|
| 392 |
-
"""
|
| 393 |
-
INSERT INTO redflag_violations
|
| 394 |
-
(tenant_id, user_id, rule_id, rule_pattern, severity, matched_text, confidence, message_preview, timestamp)
|
| 395 |
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 396 |
-
""",
|
| 397 |
-
(
|
| 398 |
-
tenant_id,
|
| 399 |
-
user_id,
|
| 400 |
-
rule_id,
|
| 401 |
-
rule_pattern,
|
| 402 |
-
severity,
|
| 403 |
-
matched_text,
|
| 404 |
-
confidence,
|
| 405 |
-
truncated_message,
|
| 406 |
-
self._now_ts(),
|
| 407 |
-
),
|
| 408 |
-
)
|
| 409 |
-
conn.commit()
|
| 410 |
|
| 411 |
def log_rag_search(
|
| 412 |
self,
|
|
@@ -417,39 +318,21 @@ class AnalyticsStore:
|
|
| 417 |
top_score: Optional[float] = None,
|
| 418 |
latency_ms: Optional[int] = None
|
| 419 |
):
|
| 420 |
-
"""Log a RAG search event with quality metrics."""
|
|
|
|
|
|
|
|
|
|
| 421 |
trimmed_query = query[:500]
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
self._supabase_insert(self.table_names["rag_search"], payload)
|
| 433 |
-
return
|
| 434 |
-
|
| 435 |
-
with sqlite3.connect(self.db_path) as conn:
|
| 436 |
-
conn.execute(
|
| 437 |
-
"""
|
| 438 |
-
INSERT INTO rag_search_events
|
| 439 |
-
(tenant_id, query, hits_count, avg_score, top_score, timestamp, latency_ms)
|
| 440 |
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
| 441 |
-
""",
|
| 442 |
-
(
|
| 443 |
-
tenant_id,
|
| 444 |
-
trimmed_query,
|
| 445 |
-
hits_count,
|
| 446 |
-
avg_score,
|
| 447 |
-
top_score,
|
| 448 |
-
self._now_ts(),
|
| 449 |
-
latency_ms,
|
| 450 |
-
),
|
| 451 |
-
)
|
| 452 |
-
conn.commit()
|
| 453 |
|
| 454 |
def log_agent_query(
|
| 455 |
self,
|
|
@@ -462,94 +345,37 @@ class AnalyticsStore:
|
|
| 462 |
success: bool = True,
|
| 463 |
user_id: Optional[str] = None
|
| 464 |
):
|
| 465 |
-
"""Log an agent query event (overall query tracking)."""
|
|
|
|
|
|
|
|
|
|
| 466 |
truncated_message = message_preview[:200]
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
"timestamp": self._now_ts(),
|
| 480 |
-
}
|
| 481 |
-
self._supabase_insert(self.table_names["agent_query"], payload)
|
| 482 |
-
return
|
| 483 |
-
|
| 484 |
-
with sqlite3.connect(self.db_path) as conn:
|
| 485 |
-
conn.execute(
|
| 486 |
-
"""
|
| 487 |
-
INSERT INTO agent_query_events
|
| 488 |
-
(tenant_id, user_id, message_preview, intent, tools_used, total_tokens, total_latency_ms, success, timestamp)
|
| 489 |
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 490 |
-
""",
|
| 491 |
-
(
|
| 492 |
-
tenant_id,
|
| 493 |
-
user_id,
|
| 494 |
-
truncated_message,
|
| 495 |
-
intent,
|
| 496 |
-
serialized_tools,
|
| 497 |
-
total_tokens,
|
| 498 |
-
total_latency_ms,
|
| 499 |
-
1 if success else 0,
|
| 500 |
-
self._now_ts(),
|
| 501 |
-
),
|
| 502 |
-
)
|
| 503 |
-
conn.commit()
|
| 504 |
|
| 505 |
def get_tool_usage_stats(
|
| 506 |
self,
|
| 507 |
tenant_id: str,
|
| 508 |
since_timestamp: Optional[int] = None
|
| 509 |
) -> Dict[str, Any]:
|
| 510 |
-
"""Get tool usage statistics for a tenant."""
|
| 511 |
-
if
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
query = """
|
| 520 |
-
SELECT
|
| 521 |
-
tool_name,
|
| 522 |
-
COUNT(*) as count,
|
| 523 |
-
AVG(latency_ms) as avg_latency_ms,
|
| 524 |
-
SUM(tokens_used) as total_tokens,
|
| 525 |
-
SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as success_count
|
| 526 |
-
FROM tool_usage_events
|
| 527 |
-
WHERE tenant_id = ?
|
| 528 |
-
"""
|
| 529 |
-
params = [tenant_id]
|
| 530 |
-
|
| 531 |
-
if since_timestamp:
|
| 532 |
-
query += " AND timestamp >= ?"
|
| 533 |
-
params.append(since_timestamp)
|
| 534 |
-
|
| 535 |
-
query += " GROUP BY tool_name"
|
| 536 |
-
|
| 537 |
-
cursor = conn.execute(query, params)
|
| 538 |
-
rows = cursor.fetchall()
|
| 539 |
-
|
| 540 |
-
stats = {}
|
| 541 |
-
for row in rows:
|
| 542 |
-
tool_name = row["tool_name"]
|
| 543 |
-
stats[tool_name] = {
|
| 544 |
-
"count": row["count"],
|
| 545 |
-
"avg_latency_ms": round(row["avg_latency_ms"] or 0, 2),
|
| 546 |
-
"total_tokens": row["total_tokens"] or 0,
|
| 547 |
-
"success_count": row["success_count"],
|
| 548 |
-
"error_count": row["count"] - row["success_count"],
|
| 549 |
-
}
|
| 550 |
-
|
| 551 |
-
return stats
|
| 552 |
-
|
| 553 |
# Supabase aggregation (computed in Python)
|
| 554 |
stats: Dict[str, Dict[str, Any]] = {}
|
| 555 |
for row in rows:
|
|
@@ -587,177 +413,81 @@ class AnalyticsStore:
|
|
| 587 |
limit: int = 50,
|
| 588 |
since_timestamp: Optional[int] = None
|
| 589 |
) -> List[Dict[str, Any]]:
|
| 590 |
-
"""Get recent red-flag violations for a tenant."""
|
| 591 |
-
if
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
with sqlite3.connect(self.db_path) as conn:
|
| 602 |
-
conn.row_factory = sqlite3.Row
|
| 603 |
-
|
| 604 |
-
query = """
|
| 605 |
-
SELECT * FROM redflag_violations
|
| 606 |
-
WHERE tenant_id = ?
|
| 607 |
-
"""
|
| 608 |
-
params = [tenant_id]
|
| 609 |
-
|
| 610 |
-
if since_timestamp:
|
| 611 |
-
query += " AND timestamp >= ?"
|
| 612 |
-
params.append(since_timestamp)
|
| 613 |
-
|
| 614 |
-
query += " ORDER BY timestamp DESC LIMIT ?"
|
| 615 |
-
params.append(limit)
|
| 616 |
-
|
| 617 |
-
cursor = conn.execute(query, params)
|
| 618 |
-
rows = cursor.fetchall()
|
| 619 |
-
|
| 620 |
-
return [dict(row) for row in rows]
|
| 621 |
|
| 622 |
def get_activity_summary(
|
| 623 |
self,
|
| 624 |
tenant_id: str,
|
| 625 |
since_timestamp: Optional[int] = None
|
| 626 |
) -> Dict[str, Any]:
|
| 627 |
-
"""Get activity summary for a tenant."""
|
| 628 |
-
if
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
active_users = len({row["user_id"] for row in queries if row.get("user_id")})
|
| 638 |
-
last_query_ts = max((row.get("timestamp") or 0) for row in queries) if queries else None
|
| 639 |
-
redflag_count = len(redflags)
|
| 640 |
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
"last_query": datetime.fromtimestamp(last_query_ts).isoformat()
|
| 646 |
-
if last_query_ts
|
| 647 |
-
else None,
|
| 648 |
-
}
|
| 649 |
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
params.append(since_timestamp)
|
| 659 |
-
|
| 660 |
-
total_queries = conn.execute(query, params).fetchone()["total"]
|
| 661 |
-
|
| 662 |
-
# Active users (unique user_ids in the period)
|
| 663 |
-
query = """
|
| 664 |
-
SELECT COUNT(DISTINCT user_id) as active_users
|
| 665 |
-
FROM agent_query_events
|
| 666 |
-
WHERE tenant_id = ? AND user_id IS NOT NULL
|
| 667 |
-
"""
|
| 668 |
-
params = [tenant_id]
|
| 669 |
-
if since_timestamp:
|
| 670 |
-
query += " AND timestamp >= ?"
|
| 671 |
-
params.append(since_timestamp)
|
| 672 |
-
|
| 673 |
-
active_users = conn.execute(query, params).fetchone()["active_users"]
|
| 674 |
-
|
| 675 |
-
# Last query timestamp
|
| 676 |
-
query = """
|
| 677 |
-
SELECT MAX(timestamp) as last_query
|
| 678 |
-
FROM agent_query_events
|
| 679 |
-
WHERE tenant_id = ?
|
| 680 |
-
"""
|
| 681 |
-
last_query_ts = conn.execute(query, [tenant_id]).fetchone()["last_query"]
|
| 682 |
-
|
| 683 |
-
# Red-flag count
|
| 684 |
-
query = "SELECT COUNT(*) as count FROM redflag_violations WHERE tenant_id = ?"
|
| 685 |
-
params = [tenant_id]
|
| 686 |
-
if since_timestamp:
|
| 687 |
-
query += " AND timestamp >= ?"
|
| 688 |
-
params.append(since_timestamp)
|
| 689 |
-
|
| 690 |
-
redflag_count = conn.execute(query, params).fetchone()["count"]
|
| 691 |
-
|
| 692 |
-
return {
|
| 693 |
-
"total_queries": total_queries,
|
| 694 |
-
"active_users": active_users or 0,
|
| 695 |
-
"redflag_count": redflag_count,
|
| 696 |
-
"last_query": datetime.fromtimestamp(last_query_ts).isoformat()
|
| 697 |
-
if last_query_ts
|
| 698 |
-
else None,
|
| 699 |
-
}
|
| 700 |
|
| 701 |
def get_rag_quality_metrics(
|
| 702 |
self,
|
| 703 |
tenant_id: str,
|
| 704 |
since_timestamp: Optional[int] = None
|
| 705 |
) -> Dict[str, Any]:
|
| 706 |
-
"""Get RAG quality metrics (recall/precision indicators)."""
|
| 707 |
-
if
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
return {
|
| 714 |
-
"total_searches": 0,
|
| 715 |
-
"avg_hits_per_search": 0,
|
| 716 |
-
"avg_score": 0,
|
| 717 |
-
"avg_top_score": 0,
|
| 718 |
-
"avg_latency_ms": 0,
|
| 719 |
-
}
|
| 720 |
-
|
| 721 |
-
total_searches = len(rows)
|
| 722 |
-
avg_hits = sum(row.get("hits_count") or 0 for row in rows) / total_searches
|
| 723 |
-
avg_avg_score = sum(row.get("avg_score") or 0 for row in rows) / total_searches
|
| 724 |
-
avg_top_score = sum(row.get("top_score") or 0 for row in rows) / total_searches
|
| 725 |
-
avg_latency = sum(row.get("latency_ms") or 0 for row in rows) / total_searches
|
| 726 |
|
|
|
|
| 727 |
return {
|
| 728 |
-
"total_searches":
|
| 729 |
-
"avg_hits_per_search":
|
| 730 |
-
"avg_score":
|
| 731 |
-
"avg_top_score":
|
| 732 |
-
"avg_latency_ms":
|
| 733 |
}
|
| 734 |
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
params = [tenant_id]
|
| 749 |
-
|
| 750 |
-
if since_timestamp:
|
| 751 |
-
query += " AND timestamp >= ?"
|
| 752 |
-
params.append(since_timestamp)
|
| 753 |
-
|
| 754 |
-
row = conn.execute(query, params).fetchone()
|
| 755 |
-
|
| 756 |
-
return {
|
| 757 |
-
"total_searches": row["total_searches"] or 0,
|
| 758 |
-
"avg_hits_per_search": round(row["avg_hits"] or 0, 2),
|
| 759 |
-
"avg_score": round(row["avg_avg_score"] or 0, 3),
|
| 760 |
-
"avg_top_score": round(row["avg_top_score"] or 0, 3),
|
| 761 |
-
"avg_latency_ms": round(row["avg_latency_ms"] or 0, 2),
|
| 762 |
-
}
|
| 763 |
|
|
|
|
| 10 |
"""
|
| 11 |
|
| 12 |
import json
|
| 13 |
+
import logging
|
| 14 |
import os
|
|
|
|
| 15 |
import time
|
| 16 |
from datetime import datetime
|
| 17 |
from pathlib import Path
|
|
|
|
| 25 |
Client = None # type: ignore
|
| 26 |
SUPABASE_AVAILABLE = False
|
| 27 |
|
| 28 |
+
logger = logging.getLogger(__name__)
|
| 29 |
+
|
| 30 |
|
| 31 |
class AnalyticsStore:
|
| 32 |
"""
|
| 33 |
+
Analytics logging with Supabase-only backend.
|
| 34 |
|
| 35 |
+
- Requires SUPABASE_URL and SUPABASE_SERVICE_KEY to be configured
|
| 36 |
+
- All data is saved to Supabase (no SQLite fallback)
|
| 37 |
+
- Raises errors if Supabase is not available
|
| 38 |
"""
|
| 39 |
|
| 40 |
def __init__(
|
|
|
|
| 43 |
use_supabase: Optional[bool] = None,
|
| 44 |
auto_create_tables: bool = False,
|
| 45 |
):
|
| 46 |
+
"""
|
| 47 |
+
Initialize AnalyticsStore with Supabase-only backend.
|
| 48 |
+
|
| 49 |
+
Args:
|
| 50 |
+
db_path: Ignored (kept for backward compatibility, not used)
|
| 51 |
+
use_supabase: Ignored (always uses Supabase if available)
|
| 52 |
+
auto_create_tables: If True, attempt to create tables if missing
|
| 53 |
+
|
| 54 |
+
Raises:
|
| 55 |
+
RuntimeError: If Supabase credentials are missing or invalid
|
| 56 |
+
"""
|
| 57 |
self._tables_verified = False
|
| 58 |
self.supabase_client: Optional[Client] = None
|
| 59 |
+
|
| 60 |
+
# Require Supabase - no fallback
|
| 61 |
+
supabase_url = os.getenv("SUPABASE_URL")
|
| 62 |
+
supabase_key = os.getenv("SUPABASE_SERVICE_KEY")
|
| 63 |
+
|
| 64 |
+
if not SUPABASE_AVAILABLE:
|
| 65 |
+
raise RuntimeError(
|
| 66 |
+
"Supabase package not installed. Install with: pip install supabase\n"
|
| 67 |
+
"AnalyticsStore requires Supabase - SQLite fallback has been removed."
|
| 68 |
)
|
| 69 |
+
|
| 70 |
+
if not supabase_url or not supabase_key:
|
| 71 |
+
raise RuntimeError(
|
| 72 |
+
"Supabase credentials are required!\n"
|
| 73 |
+
"Set SUPABASE_URL and SUPABASE_SERVICE_KEY in your .env file.\n"
|
| 74 |
+
"AnalyticsStore requires Supabase - SQLite fallback has been removed."
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
self.use_supabase = True # Always True - no fallback
|
| 78 |
+
self._init_supabase(auto_create_tables)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
def _init_supabase(self, auto_create_tables: bool):
|
| 81 |
+
"""Initialize Supabase client. Raises error if initialization fails."""
|
| 82 |
supabase_url = os.getenv("SUPABASE_URL")
|
| 83 |
supabase_key = os.getenv("SUPABASE_SERVICE_KEY")
|
| 84 |
|
| 85 |
if not supabase_url or not supabase_key:
|
| 86 |
+
raise RuntimeError(
|
| 87 |
+
"Supabase credentials are required!\n"
|
| 88 |
+
"Set SUPABASE_URL and SUPABASE_SERVICE_KEY in your .env file."
|
| 89 |
+
)
|
| 90 |
|
| 91 |
try:
|
| 92 |
self.supabase_client = create_client(supabase_url, supabase_key)
|
|
|
|
| 101 |
self._ensure_supabase_tables()
|
| 102 |
else:
|
| 103 |
self._quick_table_check()
|
| 104 |
+
|
| 105 |
+
if not self._tables_verified:
|
| 106 |
+
logger.warning(
|
| 107 |
+
"β οΈ Supabase analytics tables not verified. "
|
| 108 |
+
"Data inserts may fail. Run supabase_analytics_tables.sql in Supabase SQL Editor."
|
| 109 |
+
)
|
| 110 |
+
except Exception as exc:
|
| 111 |
+
logger.error(f"β Failed to initialize Supabase client for analytics: {exc}")
|
| 112 |
+
raise RuntimeError(
|
| 113 |
+
f"Failed to initialize Supabase client: {exc}\n"
|
| 114 |
+
"Make sure SUPABASE_URL and SUPABASE_SERVICE_KEY are correct.\n"
|
| 115 |
+
"AnalyticsStore requires Supabase - SQLite fallback has been removed."
|
| 116 |
+
) from exc
|
| 117 |
|
| 118 |
def _quick_table_check(self):
|
| 119 |
"""Verify that all expected Supabase tables exist."""
|
|
|
|
| 143 |
else:
|
| 144 |
print(" Missing supabase_analytics_tables.sql in repo root.")
|
| 145 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
# ------------------------------------------------------------------
|
| 148 |
# Logging helpers
|
|
|
|
| 163 |
return None
|
| 164 |
|
| 165 |
def _supabase_insert(self, table: str, payload: Dict[str, Any]):
|
| 166 |
+
"""Insert data into Supabase table. Raises error if insert fails."""
|
| 167 |
if not self.supabase_client:
|
| 168 |
+
raise RuntimeError(f"Supabase client not initialized. Cannot insert into {table}.")
|
| 169 |
+
|
| 170 |
+
# Check if tables are verified (warn if not)
|
| 171 |
+
if not self._tables_verified:
|
| 172 |
+
logger.warning(
|
| 173 |
+
f"Supabase tables not verified. Insert to {table} may fail. "
|
| 174 |
+
f"Run supabase_analytics_tables.sql in Supabase SQL Editor."
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
try:
|
| 178 |
+
response = self.supabase_client.table(table).insert(payload).execute()
|
| 179 |
+
logger.debug(f"Successfully inserted into {table}: {len(response.data) if response.data else 1} row(s)")
|
| 180 |
+
except Exception as exc:
|
| 181 |
+
error_msg = str(exc)
|
| 182 |
+
logger.error(
|
| 183 |
+
f"β Supabase insert failed for table '{table}': {error_msg}\n"
|
| 184 |
+
f" Payload keys: {list(payload.keys())}\n"
|
| 185 |
+
f" Tenant ID: {payload.get('tenant_id', 'N/A')}\n"
|
| 186 |
+
f" This may indicate:\n"
|
| 187 |
+
f" 1. Table '{table}' does not exist in Supabase\n"
|
| 188 |
+
f" 2. Missing columns or schema mismatch\n"
|
| 189 |
+
f" 3. RLS (Row Level Security) policy blocking insert\n"
|
| 190 |
+
f" 4. Invalid Supabase credentials\n"
|
| 191 |
+
f" Solution: Run supabase_analytics_tables.sql in Supabase SQL Editor"
|
| 192 |
+
)
|
| 193 |
+
# Re-raise - no SQLite fallback
|
| 194 |
+
raise RuntimeError(
|
| 195 |
+
f"Failed to insert into Supabase table '{table}': {error_msg}\n"
|
| 196 |
+
"AnalyticsStore requires Supabase - SQLite fallback has been removed."
|
| 197 |
+
) from exc
|
| 198 |
|
| 199 |
def _supabase_simple_select(
|
| 200 |
self,
|
|
|
|
| 263 |
metadata: Optional[Dict[str, Any]] = None,
|
| 264 |
user_id: Optional[str] = None
|
| 265 |
):
|
| 266 |
+
"""Log a tool usage event to Supabase."""
|
| 267 |
+
if not self.supabase_client:
|
| 268 |
+
raise RuntimeError("Supabase client not initialized. Cannot log tool usage.")
|
| 269 |
+
|
| 270 |
+
payload = {
|
| 271 |
+
"tenant_id": tenant_id,
|
| 272 |
+
"user_id": user_id,
|
| 273 |
+
"tool_name": tool_name,
|
| 274 |
+
"timestamp": self._now_ts(),
|
| 275 |
+
"latency_ms": latency_ms,
|
| 276 |
+
"tokens_used": tokens_used,
|
| 277 |
+
"success": success,
|
| 278 |
+
"error_message": error_message,
|
| 279 |
+
"metadata": metadata,
|
| 280 |
+
}
|
| 281 |
+
self._supabase_insert(self.table_names["tool_usage"], payload)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
|
| 283 |
def log_redflag_violation(
|
| 284 |
self,
|
|
|
|
| 291 |
message_preview: Optional[str] = None,
|
| 292 |
user_id: Optional[str] = None
|
| 293 |
):
|
| 294 |
+
"""Log a red-flag violation to Supabase."""
|
| 295 |
+
if not self.supabase_client:
|
| 296 |
+
raise RuntimeError("Supabase client not initialized. Cannot log redflag violation.")
|
| 297 |
+
|
| 298 |
truncated_message = message_preview[:200] if message_preview else None
|
| 299 |
+
payload = {
|
| 300 |
+
"tenant_id": tenant_id,
|
| 301 |
+
"user_id": user_id,
|
| 302 |
+
"rule_id": rule_id,
|
| 303 |
+
"rule_pattern": rule_pattern,
|
| 304 |
+
"severity": severity,
|
| 305 |
+
"matched_text": matched_text,
|
| 306 |
+
"confidence": confidence,
|
| 307 |
+
"message_preview": truncated_message,
|
| 308 |
+
"timestamp": self._now_ts(),
|
| 309 |
+
}
|
| 310 |
+
self._supabase_insert(self.table_names["redflags"], payload)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
|
| 312 |
def log_rag_search(
|
| 313 |
self,
|
|
|
|
| 318 |
top_score: Optional[float] = None,
|
| 319 |
latency_ms: Optional[int] = None
|
| 320 |
):
|
| 321 |
+
"""Log a RAG search event with quality metrics to Supabase."""
|
| 322 |
+
if not self.supabase_client:
|
| 323 |
+
raise RuntimeError("Supabase client not initialized. Cannot log RAG search.")
|
| 324 |
+
|
| 325 |
trimmed_query = query[:500]
|
| 326 |
+
payload = {
|
| 327 |
+
"tenant_id": tenant_id,
|
| 328 |
+
"query": trimmed_query,
|
| 329 |
+
"hits_count": hits_count,
|
| 330 |
+
"avg_score": avg_score,
|
| 331 |
+
"top_score": top_score,
|
| 332 |
+
"timestamp": self._now_ts(),
|
| 333 |
+
"latency_ms": latency_ms,
|
| 334 |
+
}
|
| 335 |
+
self._supabase_insert(self.table_names["rag_search"], payload)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
|
| 337 |
def log_agent_query(
|
| 338 |
self,
|
|
|
|
| 345 |
success: bool = True,
|
| 346 |
user_id: Optional[str] = None
|
| 347 |
):
|
| 348 |
+
"""Log an agent query event (overall query tracking) to Supabase."""
|
| 349 |
+
if not self.supabase_client:
|
| 350 |
+
raise RuntimeError("Supabase client not initialized. Cannot log agent query.")
|
| 351 |
+
|
| 352 |
truncated_message = message_preview[:200]
|
| 353 |
+
payload = {
|
| 354 |
+
"tenant_id": tenant_id,
|
| 355 |
+
"user_id": user_id,
|
| 356 |
+
"message_preview": truncated_message,
|
| 357 |
+
"intent": intent,
|
| 358 |
+
"tools_used": tools_used,
|
| 359 |
+
"total_tokens": total_tokens,
|
| 360 |
+
"total_latency_ms": total_latency_ms,
|
| 361 |
+
"success": success,
|
| 362 |
+
"timestamp": self._now_ts(),
|
| 363 |
+
}
|
| 364 |
+
self._supabase_insert(self.table_names["agent_query"], payload)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
|
| 366 |
def get_tool_usage_stats(
|
| 367 |
self,
|
| 368 |
tenant_id: str,
|
| 369 |
since_timestamp: Optional[int] = None
|
| 370 |
) -> Dict[str, Any]:
|
| 371 |
+
"""Get tool usage statistics for a tenant from Supabase."""
|
| 372 |
+
if not self.supabase_client:
|
| 373 |
+
raise RuntimeError("Supabase client not initialized. Cannot get tool usage stats.")
|
| 374 |
+
|
| 375 |
+
rows = self._supabase_fetch_all(
|
| 376 |
+
self.table_names["tool_usage"], tenant_id, since_timestamp
|
| 377 |
+
)
|
| 378 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
# Supabase aggregation (computed in Python)
|
| 380 |
stats: Dict[str, Dict[str, Any]] = {}
|
| 381 |
for row in rows:
|
|
|
|
| 413 |
limit: int = 50,
|
| 414 |
since_timestamp: Optional[int] = None
|
| 415 |
) -> List[Dict[str, Any]]:
|
| 416 |
+
"""Get recent red-flag violations for a tenant from Supabase."""
|
| 417 |
+
if not self.supabase_client:
|
| 418 |
+
raise RuntimeError("Supabase client not initialized. Cannot get redflag violations.")
|
| 419 |
+
|
| 420 |
+
return self._supabase_simple_select(
|
| 421 |
+
self.table_names["redflags"],
|
| 422 |
+
tenant_id,
|
| 423 |
+
since_timestamp=since_timestamp,
|
| 424 |
+
limit=limit,
|
| 425 |
+
order_desc=True,
|
| 426 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
|
| 428 |
def get_activity_summary(
|
| 429 |
self,
|
| 430 |
tenant_id: str,
|
| 431 |
since_timestamp: Optional[int] = None
|
| 432 |
) -> Dict[str, Any]:
|
| 433 |
+
"""Get activity summary for a tenant from Supabase."""
|
| 434 |
+
if not self.supabase_client:
|
| 435 |
+
raise RuntimeError("Supabase client not initialized. Cannot get activity summary.")
|
| 436 |
+
|
| 437 |
+
queries = self._supabase_fetch_all(
|
| 438 |
+
self.table_names["agent_query"], tenant_id, since_timestamp
|
| 439 |
+
)
|
| 440 |
+
redflags = self._supabase_fetch_all(
|
| 441 |
+
self.table_names["redflags"], tenant_id, since_timestamp
|
| 442 |
+
)
|
|
|
|
|
|
|
|
|
|
| 443 |
|
| 444 |
+
total_queries = len(queries)
|
| 445 |
+
active_users = len({row["user_id"] for row in queries if row.get("user_id")})
|
| 446 |
+
last_query_ts = max((row.get("timestamp") or 0) for row in queries) if queries else None
|
| 447 |
+
redflag_count = len(redflags)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
|
| 449 |
+
return {
|
| 450 |
+
"total_queries": total_queries,
|
| 451 |
+
"active_users": active_users,
|
| 452 |
+
"redflag_count": redflag_count,
|
| 453 |
+
"last_query": datetime.fromtimestamp(last_query_ts).isoformat()
|
| 454 |
+
if last_query_ts
|
| 455 |
+
else None,
|
| 456 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 457 |
|
| 458 |
def get_rag_quality_metrics(
|
| 459 |
self,
|
| 460 |
tenant_id: str,
|
| 461 |
since_timestamp: Optional[int] = None
|
| 462 |
) -> Dict[str, Any]:
|
| 463 |
+
"""Get RAG quality metrics (recall/precision indicators) from Supabase."""
|
| 464 |
+
if not self.supabase_client:
|
| 465 |
+
raise RuntimeError("Supabase client not initialized. Cannot get RAG quality metrics.")
|
| 466 |
+
|
| 467 |
+
rows = self._supabase_fetch_all(
|
| 468 |
+
self.table_names["rag_search"], tenant_id, since_timestamp
|
| 469 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
|
| 471 |
+
if not rows:
|
| 472 |
return {
|
| 473 |
+
"total_searches": 0,
|
| 474 |
+
"avg_hits_per_search": 0,
|
| 475 |
+
"avg_score": 0,
|
| 476 |
+
"avg_top_score": 0,
|
| 477 |
+
"avg_latency_ms": 0,
|
| 478 |
}
|
| 479 |
|
| 480 |
+
total_searches = len(rows)
|
| 481 |
+
avg_hits = sum(row.get("hits_count") or 0 for row in rows) / total_searches
|
| 482 |
+
avg_avg_score = sum(row.get("avg_score") or 0 for row in rows) / total_searches
|
| 483 |
+
avg_top_score = sum(row.get("top_score") or 0 for row in rows) / total_searches
|
| 484 |
+
avg_latency = sum(row.get("latency_ms") or 0 for row in rows) / total_searches
|
| 485 |
+
|
| 486 |
+
return {
|
| 487 |
+
"total_searches": total_searches,
|
| 488 |
+
"avg_hits_per_search": round(avg_hits, 2),
|
| 489 |
+
"avg_score": round(avg_avg_score, 3),
|
| 490 |
+
"avg_top_score": round(avg_top_score, 3),
|
| 491 |
+
"avg_latency_ms": round(avg_latency, 2),
|
| 492 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 493 |
|
check_env.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Simple script to check Supabase environment variables
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
|
| 11 |
+
# Load .env file
|
| 12 |
+
load_dotenv()
|
| 13 |
+
|
| 14 |
+
print("=" * 70)
|
| 15 |
+
print("Supabase Environment Variables Check")
|
| 16 |
+
print("=" * 70)
|
| 17 |
+
print()
|
| 18 |
+
|
| 19 |
+
# Check SUPABASE_URL
|
| 20 |
+
supabase_url = os.getenv("SUPABASE_URL")
|
| 21 |
+
if supabase_url:
|
| 22 |
+
print(f"[OK] SUPABASE_URL is set")
|
| 23 |
+
print(f" Value: {supabase_url}")
|
| 24 |
+
if not supabase_url.startswith("https://"):
|
| 25 |
+
print(f" [WARNING] URL should start with https://")
|
| 26 |
+
if ".supabase.co" not in supabase_url:
|
| 27 |
+
print(f" [WARNING] URL should contain .supabase.co")
|
| 28 |
+
else:
|
| 29 |
+
print("[ERROR] SUPABASE_URL is NOT set")
|
| 30 |
+
print(" Required for Supabase integration")
|
| 31 |
+
|
| 32 |
+
print()
|
| 33 |
+
|
| 34 |
+
# Check SUPABASE_SERVICE_KEY
|
| 35 |
+
supabase_key = os.getenv("SUPABASE_SERVICE_KEY")
|
| 36 |
+
if supabase_key:
|
| 37 |
+
key_length = len(supabase_key)
|
| 38 |
+
print(f"[OK] SUPABASE_SERVICE_KEY is set")
|
| 39 |
+
print(f" Length: {key_length} characters")
|
| 40 |
+
|
| 41 |
+
if key_length < 100:
|
| 42 |
+
print(f" [ERROR] Key is too short ({key_length} chars)")
|
| 43 |
+
print(f" Expected: 200+ characters")
|
| 44 |
+
print(f" This looks like an 'anon' key, not 'service_role' key!")
|
| 45 |
+
print(f" Get the correct key from:")
|
| 46 |
+
print(f" Supabase Dashboard -> Settings -> API -> service_role key")
|
| 47 |
+
elif key_length < 200:
|
| 48 |
+
print(f" [WARNING] Key might be incomplete ({key_length} chars)")
|
| 49 |
+
print(f" Expected: 200+ characters")
|
| 50 |
+
else:
|
| 51 |
+
print(f" [OK] Key length looks correct ({key_length} chars)")
|
| 52 |
+
|
| 53 |
+
# Check if it starts with eyJ (JWT token format)
|
| 54 |
+
if supabase_key.startswith("eyJ"):
|
| 55 |
+
print(f" [OK] Key format looks correct (JWT token)")
|
| 56 |
+
else:
|
| 57 |
+
print(f" [WARNING] Key doesn't start with 'eyJ' (unusual for JWT)")
|
| 58 |
+
|
| 59 |
+
# Show first and last few characters (masked)
|
| 60 |
+
if key_length > 20:
|
| 61 |
+
masked = supabase_key[:10] + "..." + supabase_key[-10:]
|
| 62 |
+
print(f" Preview: {masked}")
|
| 63 |
+
else:
|
| 64 |
+
print("[ERROR] SUPABASE_SERVICE_KEY is NOT set")
|
| 65 |
+
print(" Required for Supabase integration")
|
| 66 |
+
print(" Get it from: Supabase Dashboard -> Settings -> API -> service_role key")
|
| 67 |
+
|
| 68 |
+
print()
|
| 69 |
+
|
| 70 |
+
# Check POSTGRESQL_URL (optional)
|
| 71 |
+
postgres_url = os.getenv("POSTGRESQL_URL")
|
| 72 |
+
if postgres_url:
|
| 73 |
+
print(f"[INFO] POSTGRESQL_URL is set (optional, for migrations)")
|
| 74 |
+
if len(postgres_url) > 50:
|
| 75 |
+
masked = postgres_url[:30] + "..." + postgres_url[-20:]
|
| 76 |
+
print(f" Value: {masked}")
|
| 77 |
+
else:
|
| 78 |
+
print(f" Value: {postgres_url}")
|
| 79 |
+
else:
|
| 80 |
+
print("[INFO] POSTGRESQL_URL is not set (optional, only needed for migrations)")
|
| 81 |
+
|
| 82 |
+
print()
|
| 83 |
+
print("=" * 70)
|
| 84 |
+
print("Summary")
|
| 85 |
+
print("=" * 70)
|
| 86 |
+
|
| 87 |
+
has_url = bool(supabase_url)
|
| 88 |
+
has_key = bool(supabase_key)
|
| 89 |
+
key_valid = has_key and len(supabase_key) >= 200
|
| 90 |
+
|
| 91 |
+
if has_url and has_key and key_valid:
|
| 92 |
+
print("[SUCCESS] Supabase environment variables are correctly configured!")
|
| 93 |
+
print(" Your data should upload to Supabase automatically.")
|
| 94 |
+
elif has_url and has_key:
|
| 95 |
+
print("[WARNING] Supabase URL and key are set, but key appears invalid.")
|
| 96 |
+
print(" Check that you're using the 'service_role' key (not 'anon' key).")
|
| 97 |
+
elif has_url or has_key:
|
| 98 |
+
print("[ERROR] Supabase configuration is incomplete.")
|
| 99 |
+
print(" Both SUPABASE_URL and SUPABASE_SERVICE_KEY must be set.")
|
| 100 |
+
else:
|
| 101 |
+
print("[ERROR] Supabase is not configured.")
|
| 102 |
+
print(" Set SUPABASE_URL and SUPABASE_SERVICE_KEY in your .env file.")
|
| 103 |
+
|
| 104 |
+
print()
|
| 105 |
+
print("=" * 70)
|
| 106 |
+
|
data/analytics.db
CHANGED
|
Binary files a/data/analytics.db and b/data/analytics.db differ
|
|
|
test_key.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
|
| 4 |
+
load_dotenv()
|
| 5 |
+
|
| 6 |
+
key = os.getenv("SUPABASE_SERVICE_KEY")
|
| 7 |
+
url = os.getenv("SUPABASE_URL")
|
| 8 |
+
|
| 9 |
+
print("Checking Supabase Configuration:")
|
| 10 |
+
print("=" * 50)
|
| 11 |
+
|
| 12 |
+
if key:
|
| 13 |
+
print(f"SUPABASE_SERVICE_KEY:")
|
| 14 |
+
print(f" Length: {len(key)} characters")
|
| 15 |
+
print(f" Starts with 'eyJ': {key.startswith('eyJ')}")
|
| 16 |
+
print(f" First 30 chars: {key[:30]}...")
|
| 17 |
+
print(f" Last 30 chars: ...{key[-30:]}")
|
| 18 |
+
|
| 19 |
+
if len(key) >= 200:
|
| 20 |
+
print(f" [OK] Key length is correct")
|
| 21 |
+
else:
|
| 22 |
+
print(f" [WARNING] Key might be too short (expected 200+)")
|
| 23 |
+
|
| 24 |
+
if key.startswith("eyJ"):
|
| 25 |
+
print(f" [OK] Key format looks correct (JWT)")
|
| 26 |
+
else:
|
| 27 |
+
print(f" [WARNING] Key doesn't start with 'eyJ'")
|
| 28 |
+
else:
|
| 29 |
+
print("SUPABASE_SERVICE_KEY: NOT SET")
|
| 30 |
+
|
| 31 |
+
print()
|
| 32 |
+
|
| 33 |
+
if url:
|
| 34 |
+
print(f"SUPABASE_URL:")
|
| 35 |
+
print(f" Value: {url}")
|
| 36 |
+
if url.startswith("https://") and ".supabase.co" in url:
|
| 37 |
+
print(f" [OK] URL format looks correct")
|
| 38 |
+
else:
|
| 39 |
+
print(f" [WARNING] URL format might be incorrect")
|
| 40 |
+
else:
|
| 41 |
+
print("SUPABASE_URL: NOT SET")
|
| 42 |
+
|
| 43 |
+
print()
|
| 44 |
+
print("=" * 50)
|
| 45 |
+
|
test_supabase_connection.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Test Supabase connection directly"""
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
|
| 7 |
+
load_dotenv()
|
| 8 |
+
|
| 9 |
+
try:
|
| 10 |
+
from supabase import create_client
|
| 11 |
+
|
| 12 |
+
supabase_url = os.getenv("SUPABASE_URL")
|
| 13 |
+
supabase_key = os.getenv("SUPABASE_SERVICE_KEY")
|
| 14 |
+
|
| 15 |
+
print("Testing Supabase Connection:")
|
| 16 |
+
print("=" * 50)
|
| 17 |
+
print(f"URL: {supabase_url}")
|
| 18 |
+
print(f"Key length: {len(supabase_key) if supabase_key else 0}")
|
| 19 |
+
print()
|
| 20 |
+
|
| 21 |
+
if not supabase_url or not supabase_key:
|
| 22 |
+
print("ERROR: Missing Supabase credentials")
|
| 23 |
+
exit(1)
|
| 24 |
+
|
| 25 |
+
print("Creating Supabase client...")
|
| 26 |
+
client = create_client(supabase_url, supabase_key)
|
| 27 |
+
print("[OK] Client created successfully")
|
| 28 |
+
|
| 29 |
+
print()
|
| 30 |
+
print("Testing table access...")
|
| 31 |
+
tables = ["tool_usage_events", "redflag_violations", "rag_search_events", "agent_query_events"]
|
| 32 |
+
|
| 33 |
+
for table in tables:
|
| 34 |
+
try:
|
| 35 |
+
result = client.table(table).select("id").limit(1).execute()
|
| 36 |
+
print(f"[OK] Table '{table}' is accessible")
|
| 37 |
+
except Exception as e:
|
| 38 |
+
error_msg = str(e)
|
| 39 |
+
if "does not exist" in error_msg.lower() or "relation" in error_msg.lower():
|
| 40 |
+
print(f"[ERROR] Table '{table}' does NOT exist")
|
| 41 |
+
print(f" Solution: Run supabase_analytics_tables.sql in Supabase SQL Editor")
|
| 42 |
+
elif "401" in error_msg or "Invalid API key" in error_msg:
|
| 43 |
+
print(f"[ERROR] Table '{table}' access denied - Invalid API key")
|
| 44 |
+
print(f" Error: {error_msg[:100]}")
|
| 45 |
+
else:
|
| 46 |
+
print(f"[ERROR] Table '{table}' error: {error_msg[:100]}")
|
| 47 |
+
|
| 48 |
+
print()
|
| 49 |
+
print("Testing insert...")
|
| 50 |
+
try:
|
| 51 |
+
test_payload = {
|
| 52 |
+
"tenant_id": "test_connection",
|
| 53 |
+
"tool_name": "connection_test",
|
| 54 |
+
"timestamp": 1234567890,
|
| 55 |
+
"success": True
|
| 56 |
+
}
|
| 57 |
+
result = client.table("tool_usage_events").insert(test_payload).execute()
|
| 58 |
+
print("[OK] Test insert successful!")
|
| 59 |
+
print(f" Inserted {len(result.data) if result.data else 1} row(s)")
|
| 60 |
+
except Exception as e:
|
| 61 |
+
error_msg = str(e)
|
| 62 |
+
print(f"[ERROR] Test insert failed: {error_msg[:200]}")
|
| 63 |
+
if "401" in error_msg or "Invalid API key" in error_msg:
|
| 64 |
+
print(" This indicates an invalid API key")
|
| 65 |
+
elif "does not exist" in error_msg.lower():
|
| 66 |
+
print(" This indicates the table doesn't exist")
|
| 67 |
+
elif "RLS" in error_msg or "policy" in error_msg.lower():
|
| 68 |
+
print(" This indicates RLS policy blocking the insert")
|
| 69 |
+
|
| 70 |
+
print()
|
| 71 |
+
print("=" * 50)
|
| 72 |
+
print("Connection test complete!")
|
| 73 |
+
|
| 74 |
+
except ImportError:
|
| 75 |
+
print("ERROR: supabase-py package not installed")
|
| 76 |
+
print("Install it with: pip install supabase")
|
| 77 |
+
except Exception as e:
|
| 78 |
+
print(f"ERROR: {e}")
|
| 79 |
+
import traceback
|
| 80 |
+
traceback.print_exc()
|
| 81 |
+
|
verify_supabase_setup.py
CHANGED
|
@@ -48,7 +48,10 @@ def main():
|
|
| 48 |
if len(supabase_key) > 100:
|
| 49 |
print(f" β
SUPABASE_SERVICE_KEY is set: {supabase_key[:20]}... ({len(supabase_key)} chars)")
|
| 50 |
else:
|
| 51 |
-
print(f"
|
|
|
|
|
|
|
|
|
|
| 52 |
else:
|
| 53 |
print(" β SUPABASE_SERVICE_KEY is not set")
|
| 54 |
|
|
@@ -74,11 +77,52 @@ def main():
|
|
| 74 |
|
| 75 |
# Check AnalyticsStore
|
| 76 |
print("3. Checking AnalyticsStore Configuration:")
|
|
|
|
| 77 |
try:
|
| 78 |
analytics_store = AnalyticsStore()
|
| 79 |
if analytics_store.use_supabase:
|
| 80 |
print(" β
AnalyticsStore is using Supabase")
|
| 81 |
print(f" π¦ Backend: Supabase (REST API)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
else:
|
| 83 |
print(" β AnalyticsStore is using SQLite (not Supabase)")
|
| 84 |
print(" β οΈ Future analytics will be saved to SQLite, not Supabase!")
|
|
|
|
| 48 |
if len(supabase_key) > 100:
|
| 49 |
print(f" β
SUPABASE_SERVICE_KEY is set: {supabase_key[:20]}... ({len(supabase_key)} chars)")
|
| 50 |
else:
|
| 51 |
+
print(f" β SUPABASE_SERVICE_KEY seems incomplete ({len(supabase_key)} chars, expected 200+)")
|
| 52 |
+
print(" β οΈ This looks like an 'anon' key, not a 'service_role' key!")
|
| 53 |
+
print(" π‘ You need the SERVICE_ROLE key (not anon key) for backend operations")
|
| 54 |
+
print(" π‘ Get it from: Supabase Dashboard β Settings β API β service_role key")
|
| 55 |
else:
|
| 56 |
print(" β SUPABASE_SERVICE_KEY is not set")
|
| 57 |
|
|
|
|
| 77 |
|
| 78 |
# Check AnalyticsStore
|
| 79 |
print("3. Checking AnalyticsStore Configuration:")
|
| 80 |
+
analytics_store = None
|
| 81 |
try:
|
| 82 |
analytics_store = AnalyticsStore()
|
| 83 |
if analytics_store.use_supabase:
|
| 84 |
print(" β
AnalyticsStore is using Supabase")
|
| 85 |
print(f" π¦ Backend: Supabase (REST API)")
|
| 86 |
+
|
| 87 |
+
# Test table verification
|
| 88 |
+
if analytics_store._tables_verified:
|
| 89 |
+
print(" β
Analytics tables verified and accessible")
|
| 90 |
+
else:
|
| 91 |
+
print(" β οΈ Analytics tables not verified")
|
| 92 |
+
print(" β οΈ This may cause inserts to fail!")
|
| 93 |
+
print(" π‘ Solution: Run supabase_analytics_tables.sql in Supabase SQL Editor")
|
| 94 |
+
|
| 95 |
+
# Test actual insert
|
| 96 |
+
print()
|
| 97 |
+
print(" π§ͺ Testing actual insert to Supabase...")
|
| 98 |
+
try:
|
| 99 |
+
test_tenant = "test_verification"
|
| 100 |
+
analytics_store.log_tool_usage(
|
| 101 |
+
tenant_id=test_tenant,
|
| 102 |
+
tool_name="verification_test",
|
| 103 |
+
latency_ms=1,
|
| 104 |
+
success=True
|
| 105 |
+
)
|
| 106 |
+
print(" β
Test insert successful! Data is being saved to Supabase.")
|
| 107 |
+
except Exception as insert_error:
|
| 108 |
+
error_str = str(insert_error)
|
| 109 |
+
print(f" β Test insert failed: {insert_error}")
|
| 110 |
+
print(" π‘ This indicates:")
|
| 111 |
+
|
| 112 |
+
# Check for specific error types
|
| 113 |
+
if "Invalid API key" in error_str or "401" in error_str:
|
| 114 |
+
print(" β INVALID API KEY - This is the main issue!")
|
| 115 |
+
print(" π‘ Your SUPABASE_SERVICE_KEY is incorrect or incomplete")
|
| 116 |
+
print(" π‘ Get the correct key from: Supabase Dashboard β Settings β API")
|
| 117 |
+
print(" π‘ Make sure you're using the 'service_role' key (not 'anon' key)")
|
| 118 |
+
print(" π‘ The service_role key should be 200+ characters long")
|
| 119 |
+
elif "does not exist" in error_str.lower() or "relation" in error_str.lower():
|
| 120 |
+
print(" - Tables may not exist (run supabase_analytics_tables.sql)")
|
| 121 |
+
elif "RLS" in error_str or "policy" in error_str.lower():
|
| 122 |
+
print(" - RLS policies may be blocking inserts")
|
| 123 |
+
else:
|
| 124 |
+
print(" - Schema mismatch between code and database")
|
| 125 |
+
print(" - Check Supabase logs for more details")
|
| 126 |
else:
|
| 127 |
print(" β AnalyticsStore is using SQLite (not Supabase)")
|
| 128 |
print(" β οΈ Future analytics will be saved to SQLite, not Supabase!")
|