feat(stats): add user statistics to Prometheus metrics and Grafana

- Add user metrics to mana-core-auth MetricsService:
  - auth_users_total: Total registered users
  - auth_users_verified: Email-verified users
  - auth_users_created_today/this_week/this_month
- Create Grafana user-statistics dashboard with:
  - User overview stats (total, verified, verification rate, new today)
  - Registration period breakdown (today/week/month)
  - User growth trends over time
- Enhance telegram-stats-bot /users command:
  - Add yesterday comparison with trends
  - Add week-over-week comparison
  - Add mini bar chart for last 7 days registration
- Include user stats in daily Telegram report
This commit is contained in:
Till-JS 2026-01-26 10:53:57 +01:00
parent 9fedb7cfdd
commit 0cd2bc858a
6 changed files with 798 additions and 13 deletions

View file

@ -1,9 +1,10 @@
import { Module } from '@nestjs/common';
import { UmamiModule } from '../umami/umami.module';
import { UsersModule } from '../users/users.module';
import { AnalyticsService } from './analytics.service';
@Module({
imports: [UmamiModule],
imports: [UmamiModule, UsersModule],
providers: [AnalyticsService],
exports: [AnalyticsService],
})

View file

@ -1,17 +1,22 @@
import { Injectable, Logger } from '@nestjs/common';
import { UmamiService, UmamiStats } from '../umami/umami.service';
import { UsersService, UserStats } from '../users/users.service';
import {
formatDailyReport,
formatWeeklyReport,
formatRealtimeReport,
formatStatsOverview,
formatUsersReportCompact,
} from './formatters';
@Injectable()
export class AnalyticsService {
private readonly logger = new Logger(AnalyticsService.name);
constructor(private readonly umamiService: UmamiService) {}
constructor(
private readonly umamiService: UmamiService,
private readonly usersService: UsersService
) {}
private getStartOfDay(date: Date = new Date()): Date {
const start = new Date(date);
@ -75,7 +80,15 @@ export class AnalyticsService {
async generateDailyReport(): Promise<string> {
try {
const stats = await this.getTodayStats();
return formatDailyReport(stats, new Date());
let report = formatDailyReport(stats, new Date());
// Add user stats to daily report
const userStats = await this.usersService.getUserStats();
if (userStats) {
report += formatUsersReportCompact(userStats);
}
return report;
} catch (error) {
this.logger.error('Failed to generate daily report:', error);
return '❌ Fehler beim Erstellen des Daily Reports';

View file

@ -203,27 +203,103 @@ Verfügbare Befehle:
Weekly: Jeden Montag um 9:00`;
}
export interface DailyRegistration {
date: string;
count: number;
}
export interface UserStats {
totalUsers: number;
verifiedUsers: number;
todayNewUsers: number;
yesterdayNewUsers: number;
weekNewUsers: number;
lastWeekNewUsers: number;
monthNewUsers: number;
dailyRegistrations: DailyRegistration[];
}
function createMiniBarChart(dailyRegistrations: DailyRegistration[]): string[] {
if (dailyRegistrations.length === 0) return [];
const maxCount = Math.max(...dailyRegistrations.map((d) => d.count), 1);
const barChars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
// Fill in missing days and sort
const last7Days: DailyRegistration[] = [];
for (let i = 6; i >= 0; i--) {
const date = new Date();
date.setDate(date.getDate() - i);
const dateStr = date.toISOString().split('T')[0];
const found = dailyRegistrations.find((d) => d.date === dateStr);
last7Days.push({ date: dateStr, count: found?.count || 0 });
}
const bars = last7Days.map((d) => {
const index = Math.floor((d.count / maxCount) * (barChars.length - 1));
return barChars[Math.max(0, index)];
});
const dayLabels = last7Days.map((d) => {
const date = new Date(d.date);
return ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'][date.getDay()];
});
return [`<code>${bars.join('')}</code>`, `<code>${dayLabels.join('')}</code>`];
}
export function formatUsersReport(stats: UserStats): string {
const verificationRate =
stats.totalUsers > 0 ? Math.round((stats.verifiedUsers / stats.totalUsers) * 100) : 0;
// Calculate trends
const dailyTrend =
stats.yesterdayNewUsers > 0
? ((stats.todayNewUsers - stats.yesterdayNewUsers) / stats.yesterdayNewUsers) * 100
: stats.todayNewUsers > 0
? 100
: 0;
const weeklyTrend =
stats.lastWeekNewUsers > 0
? ((stats.weekNewUsers - stats.lastWeekNewUsers) / stats.lastWeekNewUsers) * 100
: stats.weekNewUsers > 0
? 100
: 0;
const lines: string[] = [
'👥 <b>ManaCore User Statistics</b>',
'━━━━━━━━━━━━━━━━━━━━',
'',
`👤 <b>Gesamt:</b> ${formatNumber(stats.totalUsers)}`,
`✅ <b>Verifiziert:</b> ${formatNumber(stats.verifiedUsers)}`,
'<b>📊 Übersicht</b>',
` 👤 Gesamt: <b>${formatNumber(stats.totalUsers)}</b>`,
` ✅ Verifiziert: ${formatNumber(stats.verifiedUsers)} (${verificationRate}%)`,
'',
'<b>📊 Neue Registrierungen:</b>',
` Heute: +${formatNumber(stats.todayNewUsers)}`,
` Diese Woche: +${formatNumber(stats.weekNewUsers)}`,
'<b>📈 Neue Registrierungen</b>',
` Heute: <b>+${formatNumber(stats.todayNewUsers)}</b> ${formatChangeEmoji(dailyTrend)}`,
` Gestern: +${formatNumber(stats.yesterdayNewUsers)}`,
` Diese Woche: +${formatNumber(stats.weekNewUsers)} ${formatChange(weeklyTrend)} ${formatChangeEmoji(weeklyTrend)}`,
` Dieser Monat: +${formatNumber(stats.monthNewUsers)}`,
];
// Add mini bar chart for last 7 days
if (stats.dailyRegistrations.length > 0) {
lines.push('');
lines.push('<b>📅 Letzte 7 Tage</b>');
lines.push(...createMiniBarChart(stats.dailyRegistrations));
}
return lines.join('\n');
}
export function formatUsersReportCompact(stats: UserStats): string {
const verificationRate =
stats.totalUsers > 0 ? Math.round((stats.verifiedUsers / stats.totalUsers) * 100) : 0;
return [
'',
'<b>👥 Registrierte User</b>',
` Gesamt: <b>${formatNumber(stats.totalUsers)}</b> (${verificationRate}% verifiziert)`,
` Heute: +${formatNumber(stats.todayNewUsers)} | Woche: +${formatNumber(stats.weekNewUsers)} | Monat: +${formatNumber(stats.monthNewUsers)}`,
].join('\n');
}