WasabiDrop Claude commited on
Commit
ff331a9
·
1 Parent(s): 7e1c5bd

🔧 Fix user view template and enhance user analytics

Browse files

Bug Fix:
- Fixed Jinja template error in user view page (dict vs attribute access)
- User statistics now properly display without crashing

Enhanced User Analytics:
- Added detailed user analytics with cost tracking
- Comprehensive model usage statistics per user
- Success rate, response time, and error tracking
- Per-model cost and token breakdown
- Daily and hourly usage distribution

Features:
- Enhanced user view with 8 key metrics
- Detailed model usage table with costs
- Better error handling for missing data
- Backward compatibility with existing stats

Technical Implementation:
- New get_user_analytics() method in structured logger
- Combined data from both event loggers
- Proper dictionary access in templates
- Enhanced dummy logger for Firebase-less deployments

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

pages/admin/view_user.html CHANGED
@@ -105,15 +105,85 @@
105
  {% if user_data.usage_stats %}
106
  <div class="usage-stats">
107
  <div class="stat-item">
108
- <strong>Total Requests:</strong> {{ user_data.usage_stats.total_requests }}
109
  </div>
110
  <div class="stat-item">
111
- <strong>Total Tokens:</strong> {{ user_data.usage_stats.total_tokens }}
112
  </div>
113
  <div class="stat-item">
114
- <strong>Total Cost:</strong> ${{ "%.4f"|format(user_data.usage_stats.total_cost) }}
115
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  </div>
 
117
  {% else %}
118
  <p><em>No usage statistics available</em></p>
119
  {% endif %}
 
105
  {% if user_data.usage_stats %}
106
  <div class="usage-stats">
107
  <div class="stat-item">
108
+ <strong>Total Requests:</strong> {{ user_data.usage_stats.get('total_requests', 0) }}
109
  </div>
110
  <div class="stat-item">
111
+ <strong>Total Tokens:</strong> {{ user_data.usage_stats.get('total_tokens', 0) }}
112
  </div>
113
  <div class="stat-item">
114
+ <strong>Input Tokens:</strong> {{ user_data.usage_stats.get('input_tokens', 0) }}
115
  </div>
116
+ <div class="stat-item">
117
+ <strong>Output Tokens:</strong> {{ user_data.usage_stats.get('output_tokens', 0) }}
118
+ </div>
119
+ <div class="stat-item">
120
+ <strong>Chat Completions:</strong> {{ user_data.usage_stats.get('chat_completions', 0) }}
121
+ </div>
122
+ <div class="stat-item">
123
+ <strong>API Errors:</strong> {{ user_data.usage_stats.get('api_errors', 0) }}
124
+ </div>
125
+ <div class="stat-item">
126
+ <strong>Total Cost:</strong> ${{ "%.4f"|format(user_data.usage_stats.get('total_cost', 0)) }}
127
+ </div>
128
+ <div class="stat-item">
129
+ <strong>Success Rate:</strong> {{ "%.1f"|format(user_data.usage_stats.get('success_rate', 0)) }}%
130
+ </div>
131
+ <div class="stat-item">
132
+ <strong>Avg Response Time:</strong> {{ "%.0f"|format(user_data.usage_stats.get('avg_response_time', 0)) }}ms
133
+ </div>
134
+ </div>
135
+
136
+ {% if user_data.usage_stats.get('model_usage') %}
137
+ <div class="model-usage-stats">
138
+ <h3>Model Usage Details</h3>
139
+ <table class="usage-table">
140
+ <thead>
141
+ <tr>
142
+ <th>Model</th>
143
+ <th>Requests</th>
144
+ <th>Tokens</th>
145
+ <th>Cost</th>
146
+ </tr>
147
+ </thead>
148
+ <tbody>
149
+ {% for model, stats in user_data.usage_stats.get('model_usage', {}).items() %}
150
+ <tr>
151
+ <td>{{ model }}</td>
152
+ <td>{{ stats.get('requests', 0) }}</td>
153
+ <td>{{ stats.get('tokens', 0) }}</td>
154
+ <td>${{ "%.4f"|format(stats.get('cost', 0)) }}</td>
155
+ </tr>
156
+ {% endfor %}
157
+ </tbody>
158
+ </table>
159
+ </div>
160
+ {% elif user_data.usage_stats.get('model_families') %}
161
+ <div class="model-families-stats">
162
+ <h3>Model Family Usage</h3>
163
+ <table class="usage-table">
164
+ <thead>
165
+ <tr>
166
+ <th>Model Family</th>
167
+ <th>Requests</th>
168
+ <th>Input Tokens</th>
169
+ <th>Output Tokens</th>
170
+ <th>Total Tokens</th>
171
+ </tr>
172
+ </thead>
173
+ <tbody>
174
+ {% for family, stats in user_data.usage_stats.get('model_families', {}).items() %}
175
+ <tr>
176
+ <td>{{ family }}</td>
177
+ <td>{{ stats.get('requests', 0) }}</td>
178
+ <td>{{ stats.get('input_tokens', 0) }}</td>
179
+ <td>{{ stats.get('output_tokens', 0) }}</td>
180
+ <td>{{ stats.get('total_tokens', 0) }}</td>
181
+ </tr>
182
+ {% endfor %}
183
+ </tbody>
184
+ </table>
185
  </div>
186
+ {% endif %}
187
  {% else %}
188
  <p><em>No usage statistics available</em></p>
189
  {% endif %}
src/routes/admin_web.py CHANGED
@@ -251,9 +251,23 @@ def view_user(token: str):
251
  flash('User not found', 'error')
252
  return redirect(url_for('admin.list_users'))
253
 
254
- # Add detailed statistics
255
  usage_stats = event_logger.get_user_stats(token)
256
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  # Add recent events (Firebase logger doesn't have this method yet)
258
  recent_events = [] # TODO: Implement get_events method in FirebaseEventLogger
259
 
 
251
  flash('User not found', 'error')
252
  return redirect(url_for('admin.list_users'))
253
 
254
+ # Add detailed statistics from both loggers
255
  usage_stats = event_logger.get_user_stats(token)
256
 
257
+ # Get enhanced stats from structured logger
258
+ enhanced_stats = structured_logger.get_user_analytics(token)
259
+
260
+ # Combine stats for better reporting
261
+ if enhanced_stats:
262
+ usage_stats.update({
263
+ 'total_cost': enhanced_stats.get('total_cost', 0),
264
+ 'avg_response_time': enhanced_stats.get('avg_response_time', 0),
265
+ 'success_rate': enhanced_stats.get('success_rate', 100),
266
+ 'models_used': enhanced_stats.get('models_used', []),
267
+ 'hourly_distribution': enhanced_stats.get('hourly_distribution', {}),
268
+ 'daily_usage': enhanced_stats.get('daily_usage', {})
269
+ })
270
+
271
  # Add recent events (Firebase logger doesn't have this method yet)
272
  recent_events = [] # TODO: Implement get_events method in FirebaseEventLogger
273
 
src/services/firebase_logger.py CHANGED
@@ -351,6 +351,113 @@ class FirebaseStructuredLogger:
351
  except Exception as e:
352
  print(f"Failed to get analytics: {e}")
353
  return {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
 
355
  # Global logger instances
356
  firebase_event_logger = FirebaseEventLogger()
@@ -367,7 +474,9 @@ else:
367
  def log_event(self, *args, **kwargs): return False
368
  def log_chat_completion(self, *args, **kwargs): return False
369
  def log_api_request(self, *args, **kwargs): return False
 
370
  def get_user_stats(self, *args, **kwargs): return {}
 
371
  def cleanup_old_events(self, *args, **kwargs): return False
372
  def get_analytics(self, *args, **kwargs): return {}
373
 
 
351
  except Exception as e:
352
  print(f"Failed to get analytics: {e}")
353
  return {}
354
+
355
+ def get_user_analytics(self, user_token: str, days: int = 30) -> Dict[str, Any]:
356
+ """Get detailed analytics for a specific user"""
357
+ if not self.firebase_available:
358
+ return {}
359
+
360
+ try:
361
+ # Get events from last N days
362
+ cutoff_time = datetime.now() - timedelta(days=days)
363
+ cutoff_timestamp = int(cutoff_time.timestamp() * 1000)
364
+
365
+ events = self.db_ref.order_by_key().start_at(str(cutoff_timestamp)).get()
366
+
367
+ if not events:
368
+ return {}
369
+
370
+ # Filter events for this user
371
+ user_events = {}
372
+ for event_key, event_data in events.items():
373
+ if event_data.get("user_token") == user_token:
374
+ user_events[event_key] = event_data
375
+
376
+ if not user_events:
377
+ return {}
378
+
379
+ analytics = {
380
+ "total_requests": len(user_events),
381
+ "total_tokens": 0,
382
+ "total_cost": 0.0,
383
+ "avg_response_time": 0.0,
384
+ "success_rate": 0.0,
385
+ "models_used": [],
386
+ "model_usage": {},
387
+ "hourly_distribution": {},
388
+ "daily_usage": {},
389
+ "input_tokens": 0,
390
+ "output_tokens": 0,
391
+ "chat_completions": 0,
392
+ "api_errors": 0
393
+ }
394
+
395
+ successful_requests = 0
396
+ total_response_time = 0
397
+ models_set = set()
398
+
399
+ for event_key, event_data in user_events.items():
400
+ if event_data.get("event_type") == "chat_completion":
401
+ analytics["chat_completions"] += 1
402
+ analytics["total_tokens"] += event_data.get("total_tokens", 0)
403
+ analytics["input_tokens"] += event_data.get("input_tokens", 0)
404
+ analytics["output_tokens"] += event_data.get("output_tokens", 0)
405
+ analytics["total_cost"] += event_data.get("cost_usd", 0)
406
+
407
+ if event_data.get("success", False):
408
+ successful_requests += 1
409
+ total_response_time += event_data.get("response_time_ms", 0)
410
+ else:
411
+ analytics["api_errors"] += 1
412
+
413
+ # Model usage
414
+ model = event_data.get("model_name", "unknown")
415
+ models_set.add(model)
416
+ if model not in analytics["model_usage"]:
417
+ analytics["model_usage"][model] = {
418
+ "requests": 0,
419
+ "tokens": 0,
420
+ "cost": 0.0
421
+ }
422
+ analytics["model_usage"][model]["requests"] += 1
423
+ analytics["model_usage"][model]["tokens"] += event_data.get("total_tokens", 0)
424
+ analytics["model_usage"][model]["cost"] += event_data.get("cost_usd", 0)
425
+
426
+ # Hourly distribution
427
+ timestamp = event_data.get("timestamp", "")
428
+ if timestamp:
429
+ try:
430
+ dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
431
+ hour = dt.hour
432
+ date_str = dt.strftime("%Y-%m-%d")
433
+
434
+ if hour not in analytics["hourly_distribution"]:
435
+ analytics["hourly_distribution"][hour] = 0
436
+ analytics["hourly_distribution"][hour] += 1
437
+
438
+ if date_str not in analytics["daily_usage"]:
439
+ analytics["daily_usage"][date_str] = {
440
+ "requests": 0,
441
+ "tokens": 0,
442
+ "cost": 0.0
443
+ }
444
+ analytics["daily_usage"][date_str]["requests"] += 1
445
+ analytics["daily_usage"][date_str]["tokens"] += event_data.get("total_tokens", 0)
446
+ analytics["daily_usage"][date_str]["cost"] += event_data.get("cost_usd", 0)
447
+ except:
448
+ pass
449
+
450
+ # Calculate averages and final stats
451
+ analytics["models_used"] = list(models_set)
452
+ if successful_requests > 0:
453
+ analytics["avg_response_time"] = total_response_time / successful_requests
454
+ analytics["success_rate"] = (successful_requests / len(user_events)) * 100
455
+
456
+ return analytics
457
+
458
+ except Exception as e:
459
+ print(f"Failed to get user analytics: {e}")
460
+ return {}
461
 
462
  # Global logger instances
463
  firebase_event_logger = FirebaseEventLogger()
 
474
  def log_event(self, *args, **kwargs): return False
475
  def log_chat_completion(self, *args, **kwargs): return False
476
  def log_api_request(self, *args, **kwargs): return False
477
+ def log_user_action(self, *args, **kwargs): return False
478
  def get_user_stats(self, *args, **kwargs): return {}
479
+ def get_user_analytics(self, *args, **kwargs): return {}
480
  def cleanup_old_events(self, *args, **kwargs): return False
481
  def get_analytics(self, *args, **kwargs): return {}
482