mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 04:06:43 +02:00
✨ 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:
parent
9fedb7cfdd
commit
0cd2bc858a
6 changed files with 798 additions and 13 deletions
542
docker/grafana/dashboards/user-statistics.json
Normal file
542
docker/grafana/dashboards/user-statistics.json
Normal file
|
|
@ -0,0 +1,542 @@
|
||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"editable": true,
|
||||||
|
"fiscalYearStartMonth": 0,
|
||||||
|
"graphTooltip": 0,
|
||||||
|
"id": null,
|
||||||
|
"links": [],
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"collapsed": false,
|
||||||
|
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 },
|
||||||
|
"id": 1,
|
||||||
|
"panels": [],
|
||||||
|
"title": "User Overview",
|
||||||
|
"type": "row"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": { "mode": "thresholds" },
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [{ "color": "blue", "value": null }]
|
||||||
|
},
|
||||||
|
"unit": "short"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": { "h": 5, "w": 6, "x": 0, "y": 1 },
|
||||||
|
"id": 2,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "area",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "horizontal",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["lastNotNull"],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto"
|
||||||
|
},
|
||||||
|
"pluginVersion": "10.0.0",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||||
|
"expr": "auth_users_total",
|
||||||
|
"legendFormat": "Total Users",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Total Users",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": { "mode": "thresholds" },
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [{ "color": "green", "value": null }]
|
||||||
|
},
|
||||||
|
"unit": "short"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": { "h": 5, "w": 6, "x": 6, "y": 1 },
|
||||||
|
"id": 3,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "area",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "horizontal",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["lastNotNull"],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto"
|
||||||
|
},
|
||||||
|
"pluginVersion": "10.0.0",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||||
|
"expr": "auth_users_verified",
|
||||||
|
"legendFormat": "Verified",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Verified Users",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": { "mode": "thresholds" },
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [{ "color": "yellow", "value": null }]
|
||||||
|
},
|
||||||
|
"unit": "percentunit"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": { "h": 5, "w": 6, "x": 12, "y": 1 },
|
||||||
|
"id": 4,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "area",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "horizontal",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["lastNotNull"],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto"
|
||||||
|
},
|
||||||
|
"pluginVersion": "10.0.0",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||||
|
"expr": "auth_users_verified / auth_users_total",
|
||||||
|
"legendFormat": "Verification Rate",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Verification Rate",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": { "mode": "thresholds" },
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [{ "color": "orange", "value": null }]
|
||||||
|
},
|
||||||
|
"unit": "short"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": { "h": 5, "w": 6, "x": 18, "y": 1 },
|
||||||
|
"id": 5,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "area",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "horizontal",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["lastNotNull"],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto"
|
||||||
|
},
|
||||||
|
"pluginVersion": "10.0.0",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||||
|
"expr": "auth_users_created_today",
|
||||||
|
"legendFormat": "Today",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "New Users Today",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsed": false,
|
||||||
|
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 6 },
|
||||||
|
"id": 6,
|
||||||
|
"panels": [],
|
||||||
|
"title": "Registration Periods",
|
||||||
|
"type": "row"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": { "mode": "thresholds" },
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [{ "color": "purple", "value": null }]
|
||||||
|
},
|
||||||
|
"unit": "short"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": { "h": 4, "w": 8, "x": 0, "y": 7 },
|
||||||
|
"id": 7,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "horizontal",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["lastNotNull"],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto"
|
||||||
|
},
|
||||||
|
"pluginVersion": "10.0.0",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||||
|
"expr": "auth_users_created_today",
|
||||||
|
"legendFormat": "Today",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "New Users Today",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": { "mode": "thresholds" },
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [{ "color": "purple", "value": null }]
|
||||||
|
},
|
||||||
|
"unit": "short"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": { "h": 4, "w": 8, "x": 8, "y": 7 },
|
||||||
|
"id": 8,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "horizontal",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["lastNotNull"],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto"
|
||||||
|
},
|
||||||
|
"pluginVersion": "10.0.0",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||||
|
"expr": "auth_users_created_this_week",
|
||||||
|
"legendFormat": "This Week",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "New Users This Week",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": { "mode": "thresholds" },
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [{ "color": "purple", "value": null }]
|
||||||
|
},
|
||||||
|
"unit": "short"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": { "h": 4, "w": 8, "x": 16, "y": 7 },
|
||||||
|
"id": 9,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "horizontal",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["lastNotNull"],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto"
|
||||||
|
},
|
||||||
|
"pluginVersion": "10.0.0",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||||
|
"expr": "auth_users_created_this_month",
|
||||||
|
"legendFormat": "This Month",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "New Users This Month",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsed": false,
|
||||||
|
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 11 },
|
||||||
|
"id": 10,
|
||||||
|
"panels": [],
|
||||||
|
"title": "User Growth Trends",
|
||||||
|
"type": "row"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": { "mode": "palette-classic" },
|
||||||
|
"custom": {
|
||||||
|
"axisBorderShow": false,
|
||||||
|
"axisCenteredZero": false,
|
||||||
|
"axisColorMode": "text",
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"drawStyle": "line",
|
||||||
|
"fillOpacity": 20,
|
||||||
|
"gradientMode": "opacity",
|
||||||
|
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
|
||||||
|
"insertNulls": false,
|
||||||
|
"lineInterpolation": "smooth",
|
||||||
|
"lineWidth": 2,
|
||||||
|
"pointSize": 5,
|
||||||
|
"scaleDistribution": { "type": "linear" },
|
||||||
|
"showPoints": "auto",
|
||||||
|
"spanNulls": false,
|
||||||
|
"stacking": { "group": "A", "mode": "none" },
|
||||||
|
"thresholdsStyle": { "mode": "off" }
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [{ "color": "green", "value": null }]
|
||||||
|
},
|
||||||
|
"unit": "short"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 12 },
|
||||||
|
"id": 11,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": ["lastNotNull", "max"],
|
||||||
|
"displayMode": "table",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": true
|
||||||
|
},
|
||||||
|
"tooltip": { "mode": "single", "sort": "none" }
|
||||||
|
},
|
||||||
|
"pluginVersion": "10.0.0",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||||
|
"expr": "auth_users_total",
|
||||||
|
"legendFormat": "Total Users",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Total Users Over Time",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": { "mode": "palette-classic" },
|
||||||
|
"custom": {
|
||||||
|
"axisBorderShow": false,
|
||||||
|
"axisCenteredZero": false,
|
||||||
|
"axisColorMode": "text",
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"drawStyle": "line",
|
||||||
|
"fillOpacity": 20,
|
||||||
|
"gradientMode": "opacity",
|
||||||
|
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
|
||||||
|
"insertNulls": false,
|
||||||
|
"lineInterpolation": "smooth",
|
||||||
|
"lineWidth": 2,
|
||||||
|
"pointSize": 5,
|
||||||
|
"scaleDistribution": { "type": "linear" },
|
||||||
|
"showPoints": "auto",
|
||||||
|
"spanNulls": false,
|
||||||
|
"stacking": { "group": "A", "mode": "none" },
|
||||||
|
"thresholdsStyle": { "mode": "off" }
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [{ "color": "green", "value": null }]
|
||||||
|
},
|
||||||
|
"unit": "percentunit"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 12 },
|
||||||
|
"id": 12,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": ["lastNotNull", "mean"],
|
||||||
|
"displayMode": "table",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": true
|
||||||
|
},
|
||||||
|
"tooltip": { "mode": "single", "sort": "none" }
|
||||||
|
},
|
||||||
|
"pluginVersion": "10.0.0",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||||
|
"expr": "auth_users_verified / auth_users_total",
|
||||||
|
"legendFormat": "Verification Rate",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Verification Rate Over Time",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": { "mode": "palette-classic" },
|
||||||
|
"custom": {
|
||||||
|
"axisBorderShow": false,
|
||||||
|
"axisCenteredZero": false,
|
||||||
|
"axisColorMode": "text",
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"drawStyle": "bars",
|
||||||
|
"fillOpacity": 80,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
|
||||||
|
"insertNulls": false,
|
||||||
|
"lineInterpolation": "linear",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"pointSize": 5,
|
||||||
|
"scaleDistribution": { "type": "linear" },
|
||||||
|
"showPoints": "never",
|
||||||
|
"spanNulls": false,
|
||||||
|
"stacking": { "group": "A", "mode": "none" },
|
||||||
|
"thresholdsStyle": { "mode": "off" }
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [{ "color": "green", "value": null }]
|
||||||
|
},
|
||||||
|
"unit": "short"
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": { "id": "byName", "options": "Today" },
|
||||||
|
"properties": [{ "id": "color", "value": { "fixedColor": "orange", "mode": "fixed" } }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": { "id": "byName", "options": "This Week" },
|
||||||
|
"properties": [{ "id": "color", "value": { "fixedColor": "blue", "mode": "fixed" } }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": { "id": "byName", "options": "This Month" },
|
||||||
|
"properties": [{ "id": "color", "value": { "fixedColor": "purple", "mode": "fixed" } }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 20 },
|
||||||
|
"id": 13,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": ["lastNotNull"],
|
||||||
|
"displayMode": "table",
|
||||||
|
"placement": "right",
|
||||||
|
"showLegend": true
|
||||||
|
},
|
||||||
|
"tooltip": { "mode": "multi", "sort": "none" }
|
||||||
|
},
|
||||||
|
"pluginVersion": "10.0.0",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||||
|
"expr": "auth_users_created_today",
|
||||||
|
"legendFormat": "Today",
|
||||||
|
"refId": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||||
|
"expr": "auth_users_created_this_week",
|
||||||
|
"legendFormat": "This Week",
|
||||||
|
"refId": "B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||||
|
"expr": "auth_users_created_this_month",
|
||||||
|
"legendFormat": "This Month",
|
||||||
|
"refId": "C"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "New Registrations by Period",
|
||||||
|
"type": "timeseries"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"refresh": "30s",
|
||||||
|
"schemaVersion": 38,
|
||||||
|
"tags": ["manacore", "users", "auth"],
|
||||||
|
"templating": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"current": { "selected": false, "text": "Prometheus", "value": "Prometheus" },
|
||||||
|
"hide": 0,
|
||||||
|
"includeAll": false,
|
||||||
|
"multi": false,
|
||||||
|
"name": "datasource",
|
||||||
|
"options": [],
|
||||||
|
"query": "prometheus",
|
||||||
|
"refresh": 1,
|
||||||
|
"regex": "",
|
||||||
|
"skipUrlSync": false,
|
||||||
|
"type": "datasource"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"time": { "from": "now-7d", "to": "now" },
|
||||||
|
"timepicker": {},
|
||||||
|
"timezone": "browser",
|
||||||
|
"title": "User Statistics",
|
||||||
|
"uid": "user-statistics",
|
||||||
|
"version": 1,
|
||||||
|
"weekStart": ""
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,28 @@
|
||||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
import * as client from 'prom-client';
|
import * as client from 'prom-client';
|
||||||
|
import { count, eq, gte, and, isNull, sql } from 'drizzle-orm';
|
||||||
|
import { getDb } from '../db/connection';
|
||||||
|
import { users } from '../db/schema';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MetricsService implements OnModuleInit {
|
export class MetricsService implements OnModuleInit, OnModuleDestroy {
|
||||||
|
private readonly logger = new Logger(MetricsService.name);
|
||||||
private readonly register: client.Registry;
|
private readonly register: client.Registry;
|
||||||
|
private updateInterval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
// HTTP metrics
|
// HTTP metrics
|
||||||
readonly httpRequestsTotal: client.Counter<string>;
|
readonly httpRequestsTotal: client.Counter<string>;
|
||||||
readonly httpRequestDuration: client.Histogram<string>;
|
readonly httpRequestDuration: client.Histogram<string>;
|
||||||
|
|
||||||
constructor() {
|
// User metrics
|
||||||
|
readonly usersTotal: client.Gauge<string>;
|
||||||
|
readonly usersVerified: client.Gauge<string>;
|
||||||
|
readonly usersCreatedToday: client.Gauge<string>;
|
||||||
|
readonly usersCreatedThisWeek: client.Gauge<string>;
|
||||||
|
readonly usersCreatedThisMonth: client.Gauge<string>;
|
||||||
|
|
||||||
|
constructor(private readonly configService: ConfigService) {
|
||||||
this.register = new client.Registry();
|
this.register = new client.Registry();
|
||||||
|
|
||||||
// Add default metrics (CPU, memory, event loop, etc.)
|
// Add default metrics (CPU, memory, event loop, etc.)
|
||||||
|
|
@ -34,10 +47,113 @@ export class MetricsService implements OnModuleInit {
|
||||||
buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 2, 5],
|
buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 2, 5],
|
||||||
registers: [this.register],
|
registers: [this.register],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// User statistics gauges
|
||||||
|
this.usersTotal = new client.Gauge({
|
||||||
|
name: 'auth_users_total',
|
||||||
|
help: 'Total number of registered users',
|
||||||
|
registers: [this.register],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.usersVerified = new client.Gauge({
|
||||||
|
name: 'auth_users_verified',
|
||||||
|
help: 'Number of email-verified users',
|
||||||
|
registers: [this.register],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.usersCreatedToday = new client.Gauge({
|
||||||
|
name: 'auth_users_created_today',
|
||||||
|
help: 'Number of users created today',
|
||||||
|
registers: [this.register],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.usersCreatedThisWeek = new client.Gauge({
|
||||||
|
name: 'auth_users_created_this_week',
|
||||||
|
help: 'Number of users created this week',
|
||||||
|
registers: [this.register],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.usersCreatedThisMonth = new client.Gauge({
|
||||||
|
name: 'auth_users_created_this_month',
|
||||||
|
help: 'Number of users created this month',
|
||||||
|
registers: [this.register],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onModuleInit() {
|
async onModuleInit() {
|
||||||
// Metrics are ready
|
// Update user metrics immediately and then every 60 seconds
|
||||||
|
await this.updateUserMetrics();
|
||||||
|
this.updateInterval = setInterval(() => this.updateUserMetrics(), 60000);
|
||||||
|
}
|
||||||
|
|
||||||
|
onModuleDestroy() {
|
||||||
|
if (this.updateInterval) {
|
||||||
|
clearInterval(this.updateInterval);
|
||||||
|
this.updateInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateUserMetrics() {
|
||||||
|
const databaseUrl = this.configService.get<string>('DATABASE_URL');
|
||||||
|
if (!databaseUrl) {
|
||||||
|
this.logger.warn('DATABASE_URL not configured, user metrics unavailable');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = getDb(databaseUrl);
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
// Start of today (midnight)
|
||||||
|
const startOfToday = new Date(now);
|
||||||
|
startOfToday.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
// Start of week (Monday)
|
||||||
|
const startOfWeek = new Date(now);
|
||||||
|
const day = startOfWeek.getDay();
|
||||||
|
const diff = startOfWeek.getDate() - day + (day === 0 ? -6 : 1);
|
||||||
|
startOfWeek.setDate(diff);
|
||||||
|
startOfWeek.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
// Start of month
|
||||||
|
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||||
|
|
||||||
|
// Query all metrics in parallel
|
||||||
|
const [totalResult, verifiedResult, todayResult, weekResult, monthResult] = await Promise.all(
|
||||||
|
[
|
||||||
|
// Total users
|
||||||
|
db.select({ count: count() }).from(users).where(isNull(users.deletedAt)),
|
||||||
|
// Verified users
|
||||||
|
db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(users)
|
||||||
|
.where(and(isNull(users.deletedAt), eq(users.emailVerified, true))),
|
||||||
|
// Users created today
|
||||||
|
db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(users)
|
||||||
|
.where(and(isNull(users.deletedAt), gte(users.createdAt, startOfToday))),
|
||||||
|
// Users created this week
|
||||||
|
db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(users)
|
||||||
|
.where(and(isNull(users.deletedAt), gte(users.createdAt, startOfWeek))),
|
||||||
|
// Users created this month
|
||||||
|
db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(users)
|
||||||
|
.where(and(isNull(users.deletedAt), gte(users.createdAt, startOfMonth))),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
this.usersTotal.set(totalResult[0].count);
|
||||||
|
this.usersVerified.set(verifiedResult[0].count);
|
||||||
|
this.usersCreatedToday.set(todayResult[0].count);
|
||||||
|
this.usersCreatedThisWeek.set(weekResult[0].count);
|
||||||
|
this.usersCreatedThisMonth.set(monthResult[0].count);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Failed to update user metrics:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMetrics(): Promise<string> {
|
async getMetrics(): Promise<string> {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { UmamiModule } from '../umami/umami.module';
|
import { UmamiModule } from '../umami/umami.module';
|
||||||
|
import { UsersModule } from '../users/users.module';
|
||||||
import { AnalyticsService } from './analytics.service';
|
import { AnalyticsService } from './analytics.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [UmamiModule],
|
imports: [UmamiModule, UsersModule],
|
||||||
providers: [AnalyticsService],
|
providers: [AnalyticsService],
|
||||||
exports: [AnalyticsService],
|
exports: [AnalyticsService],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,22 @@
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { UmamiService, UmamiStats } from '../umami/umami.service';
|
import { UmamiService, UmamiStats } from '../umami/umami.service';
|
||||||
|
import { UsersService, UserStats } from '../users/users.service';
|
||||||
import {
|
import {
|
||||||
formatDailyReport,
|
formatDailyReport,
|
||||||
formatWeeklyReport,
|
formatWeeklyReport,
|
||||||
formatRealtimeReport,
|
formatRealtimeReport,
|
||||||
formatStatsOverview,
|
formatStatsOverview,
|
||||||
|
formatUsersReportCompact,
|
||||||
} from './formatters';
|
} from './formatters';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AnalyticsService {
|
export class AnalyticsService {
|
||||||
private readonly logger = new Logger(AnalyticsService.name);
|
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 {
|
private getStartOfDay(date: Date = new Date()): Date {
|
||||||
const start = new Date(date);
|
const start = new Date(date);
|
||||||
|
|
@ -75,7 +80,15 @@ export class AnalyticsService {
|
||||||
async generateDailyReport(): Promise<string> {
|
async generateDailyReport(): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const stats = await this.getTodayStats();
|
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) {
|
} catch (error) {
|
||||||
this.logger.error('Failed to generate daily report:', error);
|
this.logger.error('Failed to generate daily report:', error);
|
||||||
return '❌ Fehler beim Erstellen des Daily Reports';
|
return '❌ Fehler beim Erstellen des Daily Reports';
|
||||||
|
|
|
||||||
|
|
@ -203,27 +203,103 @@ Verfügbare Befehle:
|
||||||
• Weekly: Jeden Montag um 9:00`;
|
• Weekly: Jeden Montag um 9:00`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DailyRegistration {
|
||||||
|
date: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UserStats {
|
export interface UserStats {
|
||||||
totalUsers: number;
|
totalUsers: number;
|
||||||
verifiedUsers: number;
|
verifiedUsers: number;
|
||||||
todayNewUsers: number;
|
todayNewUsers: number;
|
||||||
|
yesterdayNewUsers: number;
|
||||||
weekNewUsers: number;
|
weekNewUsers: number;
|
||||||
|
lastWeekNewUsers: number;
|
||||||
monthNewUsers: 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 {
|
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[] = [
|
const lines: string[] = [
|
||||||
'👥 <b>ManaCore User Statistics</b>',
|
'👥 <b>ManaCore User Statistics</b>',
|
||||||
'━━━━━━━━━━━━━━━━━━━━',
|
'━━━━━━━━━━━━━━━━━━━━',
|
||||||
'',
|
'',
|
||||||
`👤 <b>Gesamt:</b> ${formatNumber(stats.totalUsers)}`,
|
'<b>📊 Übersicht</b>',
|
||||||
`✅ <b>Verifiziert:</b> ${formatNumber(stats.verifiedUsers)}`,
|
` 👤 Gesamt: <b>${formatNumber(stats.totalUsers)}</b>`,
|
||||||
|
` ✅ Verifiziert: ${formatNumber(stats.verifiedUsers)} (${verificationRate}%)`,
|
||||||
'',
|
'',
|
||||||
'<b>📊 Neue Registrierungen:</b>',
|
'<b>📈 Neue Registrierungen</b>',
|
||||||
` Heute: +${formatNumber(stats.todayNewUsers)}`,
|
` Heute: <b>+${formatNumber(stats.todayNewUsers)}</b> ${formatChangeEmoji(dailyTrend)}`,
|
||||||
` Diese Woche: +${formatNumber(stats.weekNewUsers)}`,
|
` Gestern: +${formatNumber(stats.yesterdayNewUsers)}`,
|
||||||
|
` Diese Woche: +${formatNumber(stats.weekNewUsers)} ${formatChange(weeklyTrend)} ${formatChangeEmoji(weeklyTrend)}`,
|
||||||
` Dieser Monat: +${formatNumber(stats.monthNewUsers)}`,
|
` 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');
|
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');
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,14 @@ export interface UserStats {
|
||||||
todayNewUsers: number;
|
todayNewUsers: number;
|
||||||
weekNewUsers: number;
|
weekNewUsers: number;
|
||||||
monthNewUsers: number;
|
monthNewUsers: number;
|
||||||
|
yesterdayNewUsers: number;
|
||||||
|
lastWeekNewUsers: number;
|
||||||
|
dailyRegistrations: DailyRegistration[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DailyRegistration {
|
||||||
|
date: string;
|
||||||
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|
@ -43,31 +51,60 @@ export class UsersService implements OnModuleInit {
|
||||||
const startOfToday = new Date(now);
|
const startOfToday = new Date(now);
|
||||||
startOfToday.setHours(0, 0, 0, 0);
|
startOfToday.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
const startOfYesterday = new Date(startOfToday);
|
||||||
|
startOfYesterday.setDate(startOfYesterday.getDate() - 1);
|
||||||
|
|
||||||
const startOfWeek = new Date(now);
|
const startOfWeek = new Date(now);
|
||||||
const day = startOfWeek.getDay();
|
const day = startOfWeek.getDay();
|
||||||
const diff = startOfWeek.getDate() - day + (day === 0 ? -6 : 1);
|
const diff = startOfWeek.getDate() - day + (day === 0 ? -6 : 1);
|
||||||
startOfWeek.setDate(diff);
|
startOfWeek.setDate(diff);
|
||||||
startOfWeek.setHours(0, 0, 0, 0);
|
startOfWeek.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
const startOfLastWeek = new Date(startOfWeek);
|
||||||
|
startOfLastWeek.setDate(startOfLastWeek.getDate() - 7);
|
||||||
|
|
||||||
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||||
|
|
||||||
|
// Main stats query
|
||||||
const [result] = await this.sql`
|
const [result] = await this.sql`
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) as total_users,
|
COUNT(*) as total_users,
|
||||||
COUNT(*) FILTER (WHERE email_verified = true) as verified_users,
|
COUNT(*) FILTER (WHERE email_verified = true) as verified_users,
|
||||||
COUNT(*) FILTER (WHERE created_at >= ${startOfToday.toISOString()}) as today_new_users,
|
COUNT(*) FILTER (WHERE created_at >= ${startOfToday.toISOString()}) as today_new_users,
|
||||||
|
COUNT(*) FILTER (WHERE created_at >= ${startOfYesterday.toISOString()} AND created_at < ${startOfToday.toISOString()}) as yesterday_new_users,
|
||||||
COUNT(*) FILTER (WHERE created_at >= ${startOfWeek.toISOString()}) as week_new_users,
|
COUNT(*) FILTER (WHERE created_at >= ${startOfWeek.toISOString()}) as week_new_users,
|
||||||
|
COUNT(*) FILTER (WHERE created_at >= ${startOfLastWeek.toISOString()} AND created_at < ${startOfWeek.toISOString()}) as last_week_new_users,
|
||||||
COUNT(*) FILTER (WHERE created_at >= ${startOfMonth.toISOString()}) as month_new_users
|
COUNT(*) FILTER (WHERE created_at >= ${startOfMonth.toISOString()}) as month_new_users
|
||||||
FROM auth.users
|
FROM auth.users
|
||||||
WHERE deleted_at IS NULL
|
WHERE deleted_at IS NULL
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Get daily registrations for last 7 days
|
||||||
|
const dailyStats = await this.sql`
|
||||||
|
SELECT
|
||||||
|
DATE(created_at) as date,
|
||||||
|
COUNT(*) as count
|
||||||
|
FROM auth.users
|
||||||
|
WHERE deleted_at IS NULL
|
||||||
|
AND created_at >= ${new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString()}
|
||||||
|
GROUP BY DATE(created_at)
|
||||||
|
ORDER BY date DESC
|
||||||
|
`;
|
||||||
|
|
||||||
|
const dailyRegistrations: DailyRegistration[] = dailyStats.map((row) => ({
|
||||||
|
date: new Date(row.date).toISOString().split('T')[0],
|
||||||
|
count: Number(row.count),
|
||||||
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalUsers: Number(result.total_users),
|
totalUsers: Number(result.total_users),
|
||||||
verifiedUsers: Number(result.verified_users),
|
verifiedUsers: Number(result.verified_users),
|
||||||
todayNewUsers: Number(result.today_new_users),
|
todayNewUsers: Number(result.today_new_users),
|
||||||
|
yesterdayNewUsers: Number(result.yesterday_new_users),
|
||||||
weekNewUsers: Number(result.week_new_users),
|
weekNewUsers: Number(result.week_new_users),
|
||||||
|
lastWeekNewUsers: Number(result.last_week_new_users),
|
||||||
monthNewUsers: Number(result.month_new_users),
|
monthNewUsers: Number(result.month_new_users),
|
||||||
|
dailyRegistrations,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Failed to fetch user stats:', error);
|
this.logger.error('Failed to fetch user stats:', error);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue