nothingworry commited on
Commit
d74c0dc
Β·
1 Parent(s): 80ebded

refactor(analytics): Remove SQLite fallback, require Supabase exclusively

Browse files
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 dual-backend support.
32
 
33
- - Uses Supabase when SUPABASE_URL/SUPABASE_SERVICE_KEY are configured
34
- - Falls back to local SQLite (data/analytics.db) otherwise
 
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
- self.use_supabase = use_supabase
 
 
 
 
 
 
 
 
 
 
44
  self._tables_verified = False
45
  self.supabase_client: Optional[Client] = None
46
-
47
- if self.use_supabase is None:
48
- supabase_url = os.getenv("SUPABASE_URL")
49
- supabase_key = os.getenv("SUPABASE_SERVICE_KEY")
50
- self.use_supabase = bool(
51
- supabase_url and supabase_key and SUPABASE_AVAILABLE
 
 
 
52
  )
53
-
54
- if self.use_supabase:
55
- self._init_supabase(auto_create_tables)
56
- else:
57
- self._init_sqlite(db_path)
58
-
59
- def _init_sqlite(self, db_path: Optional[str]):
60
- """Initialize SQLite database path + schema."""
61
- if db_path is None:
62
- root_dir = Path(__file__).resolve().parents[3]
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
- print("⚠️ Supabase credentials missing. Falling back to SQLite for analytics.")
77
- self.use_supabase = False
78
- self._init_sqlite(None)
79
- return
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
- except Exception as exc: # pragma: no cover - defensive logging
95
- print(f"⚠️ Failed to initialize Supabase client for analytics: {exc}")
96
- self.use_supabase = False
97
- self._init_sqlite(None)
 
 
 
 
 
 
 
 
 
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
- return
 
 
 
 
 
 
 
 
252
  try:
253
- self.supabase_client.table(table).insert(payload).execute()
254
- except Exception as exc: # pragma: no cover - logging only
255
- print(f"❌ Supabase insert failed for {table}: {exc}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 self.use_supabase and self.supabase_client:
326
- payload = {
327
- "tenant_id": tenant_id,
328
- "user_id": user_id,
329
- "tool_name": tool_name,
330
- "timestamp": self._now_ts(),
331
- "latency_ms": latency_ms,
332
- "tokens_used": tokens_used,
333
- "success": success,
334
- "error_message": error_message,
335
- "metadata": metadata,
336
- }
337
- self._supabase_insert(self.table_names["tool_usage"], payload)
338
- return
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
- if self.use_supabase and self.supabase_client:
376
- payload = {
377
- "tenant_id": tenant_id,
378
- "user_id": user_id,
379
- "rule_id": rule_id,
380
- "rule_pattern": rule_pattern,
381
- "severity": severity,
382
- "matched_text": matched_text,
383
- "confidence": confidence,
384
- "message_preview": truncated_message,
385
- "timestamp": self._now_ts(),
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
- if self.use_supabase and self.supabase_client:
423
- payload = {
424
- "tenant_id": tenant_id,
425
- "query": trimmed_query,
426
- "hits_count": hits_count,
427
- "avg_score": avg_score,
428
- "top_score": top_score,
429
- "timestamp": self._now_ts(),
430
- "latency_ms": latency_ms,
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
- serialized_tools = self._serialize_tools(tools_used)
468
-
469
- if self.use_supabase and self.supabase_client:
470
- payload = {
471
- "tenant_id": tenant_id,
472
- "user_id": user_id,
473
- "message_preview": truncated_message,
474
- "intent": intent,
475
- "tools_used": tools_used,
476
- "total_tokens": total_tokens,
477
- "total_latency_ms": total_latency_ms,
478
- "success": success,
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 self.use_supabase and self.supabase_client:
512
- rows = self._supabase_fetch_all(
513
- self.table_names["tool_usage"], tenant_id, since_timestamp
514
- )
515
- else:
516
- with sqlite3.connect(self.db_path) as conn:
517
- conn.row_factory = sqlite3.Row
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 self.use_supabase and self.supabase_client:
592
- rows = self._supabase_simple_select(
593
- self.table_names["redflags"],
594
- tenant_id,
595
- since_timestamp=since_timestamp,
596
- limit=limit,
597
- order_desc=True,
598
- )
599
- return rows
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 self.use_supabase and self.supabase_client:
629
- queries = self._supabase_fetch_all(
630
- self.table_names["agent_query"], tenant_id, since_timestamp
631
- )
632
- redflags = self._supabase_fetch_all(
633
- self.table_names["redflags"], tenant_id, since_timestamp
634
- )
635
-
636
- total_queries = len(queries)
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
- return {
642
- "total_queries": total_queries,
643
- "active_users": active_users,
644
- "redflag_count": redflag_count,
645
- "last_query": datetime.fromtimestamp(last_query_ts).isoformat()
646
- if last_query_ts
647
- else None,
648
- }
649
 
650
- with sqlite3.connect(self.db_path) as conn:
651
- conn.row_factory = sqlite3.Row
652
-
653
- # Total queries
654
- query = "SELECT COUNT(*) as total FROM agent_query_events WHERE tenant_id = ?"
655
- params = [tenant_id]
656
- if since_timestamp:
657
- query += " AND timestamp >= ?"
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 self.use_supabase and self.supabase_client:
708
- rows = self._supabase_fetch_all(
709
- self.table_names["rag_search"], tenant_id, since_timestamp
710
- )
711
-
712
- if not rows:
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": total_searches,
729
- "avg_hits_per_search": round(avg_hits, 2),
730
- "avg_score": round(avg_avg_score, 3),
731
- "avg_top_score": round(avg_top_score, 3),
732
- "avg_latency_ms": round(avg_latency, 2),
733
  }
734
 
735
- with sqlite3.connect(self.db_path) as conn:
736
- conn.row_factory = sqlite3.Row
737
-
738
- query = """
739
- SELECT
740
- COUNT(*) as total_searches,
741
- AVG(hits_count) as avg_hits,
742
- AVG(avg_score) as avg_avg_score,
743
- AVG(top_score) as avg_top_score,
744
- AVG(latency_ms) as avg_latency_ms
745
- FROM rag_search_events
746
- WHERE tenant_id = ?
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" ⚠️ SUPABASE_SERVICE_KEY seems incomplete ({len(supabase_key)} chars, expected 200+)")
 
 
 
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!")