Skip to content

Commit 54e4c74

Browse files
Merge pull request #167 from geturbackend/copilot/fix-cohort-accuracy-and-memory
Harden admin metrics cohorts/usage aggregation and enforce router-level admin guard
2 parents 4cc7eb9 + ba79db2 commit 54e4c74

3 files changed

Lines changed: 37 additions & 31 deletions

File tree

‎apps/dashboard-api/src/controllers/admin.metrics.controller.js‎

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,20 @@ const { Developer, Project, Log, ApiAnalytics, PlatformEvent, DeveloperActivity
55
* Checked upstream via the isAdmin flag on the JWT payload,
66
* but we double-check here for defence in depth.
77
*/
8-
function requireAdmin(req, res) {
8+
function requireAdmin(req, res, next) {
99
if (!req.user?.isAdmin) {
10-
res.status(403).json({ success: false, data: {}, message: 'Admin access required.' });
11-
return false;
10+
return res.status(403).json({ success: false, data: {}, message: 'Admin access required.' });
1211
}
13-
return true;
12+
return next();
1413
}
1514

15+
module.exports.requireAdmin = requireAdmin;
16+
1617
// ---------------------------------------------------------------------------
1718
// GET /api/admin/metrics/overview
1819
// Platform-wide snapshot: signups, verified devs, active projects, total calls.
1920
// ---------------------------------------------------------------------------
2021
module.exports.getOverview = async (req, res) => {
21-
if (!requireAdmin(req, res)) return;
2222
try {
2323
const sevenDaysAgo = new Date();
2424
sevenDaysAgo.setUTCDate(sevenDaysAgo.getUTCDate() - 7);
@@ -62,7 +62,6 @@ module.exports.getOverview = async (req, res) => {
6262
// Platform-wide funnel: count of devs who completed each activation step.
6363
// ---------------------------------------------------------------------------
6464
module.exports.getActivationFunnel = async (req, res) => {
65-
if (!requireAdmin(req, res)) return;
6665
try {
6766
const FUNNEL_STEPS = [
6867
'signup_completed',
@@ -117,7 +116,6 @@ module.exports.getActivationFunnel = async (req, res) => {
117116
// D1/D7/D30 retention for developers who signed up in a given month.
118117
// ---------------------------------------------------------------------------
119118
module.exports.getCohorts = async (req, res) => {
120-
if (!requireAdmin(req, res)) return;
121119
try {
122120
const { month } = req.query; // e.g. "2026-05"
123121
if (!month || !/^\d{4}-\d{2}$/.test(month)) {
@@ -237,32 +235,40 @@ module.exports.getCohorts = async (req, res) => {
237235
// Platform-wide feature breakdown from DeveloperActivity (last 30 days).
238236
// ---------------------------------------------------------------------------
239237
module.exports.getFeatureUsage = async (req, res) => {
240-
if (!requireAdmin(req, res)) return;
241238
try {
242239
const thirtyDaysAgo = new Date();
243240
thirtyDaysAgo.setUTCDate(thirtyDaysAgo.getUTCDate() - 30);
244241

245242
const agg = await DeveloperActivity.aggregate([
246243
{ $match: { date: { $gte: thirtyDaysAgo } } },
247244
{
248-
$group: {
249-
_id: null,
250-
totalApiCalls: { $sum: '$apiCallCount' },
251-
totalMailSent: { $sum: '$mailSentCount' },
252-
totalStorageUploads: { $sum: '$storageUploadsCount' },
253-
totalWebhooksFired: { $sum: '$webhookTriggeredCount' },
254-
activeDevelopers: { $addToSet: '$developerId' },
245+
$facet: {
246+
totals: [
247+
{
248+
$group: {
249+
_id: null,
250+
totalApiCalls: { $sum: '$apiCallCount' },
251+
totalMailSent: { $sum: '$mailSentCount' },
252+
totalStorageUploads: { $sum: '$storageUploadsCount' },
253+
totalWebhooksFired: { $sum: '$webhookTriggeredCount' },
254+
},
255+
},
256+
],
257+
activeDevelopers: [
258+
{ $group: { _id: '$developerId' } },
259+
{ $count: 'count' },
260+
],
255261
},
256262
},
257263
]);
258264

259-
const result = agg[0] || {
265+
const result = agg[0]?.totals?.[0] || {
260266
totalApiCalls: 0,
261267
totalMailSent: 0,
262268
totalStorageUploads: 0,
263269
totalWebhooksFired: 0,
264-
activeDevelopers: [],
265270
};
271+
const activeDeveloperCount = agg[0]?.activeDevelopers?.[0]?.count || 0;
266272

267273
return res.json({
268274
success: true,
@@ -272,7 +278,7 @@ module.exports.getFeatureUsage = async (req, res) => {
272278
totalMailSent: result.totalMailSent,
273279
totalStorageUploads: result.totalStorageUploads,
274280
totalWebhooksFired: result.totalWebhooksFired,
275-
activeDevelopers: result.activeDevelopers.length,
281+
activeDevelopers: activeDeveloperCount,
276282
},
277283
message: '',
278284
});
@@ -287,7 +293,6 @@ module.exports.getFeatureUsage = async (req, res) => {
287293
// Global error rate and latency across all projects (last 24h from ApiAnalytics).
288294
// ---------------------------------------------------------------------------
289295
module.exports.getReliability = async (req, res) => {
290-
if (!requireAdmin(req, res)) return;
291296
try {
292297
const { ApiAnalytics } = require('@urbackend/common');
293298
const since = new Date(Date.now() - 24 * 60 * 60 * 1000);
@@ -332,7 +337,6 @@ module.exports.getReliability = async (req, res) => {
332337
// Most active projects by API calls in the last 7 days.
333338
// ---------------------------------------------------------------------------
334339
module.exports.getTopProjects = async (req, res) => {
335-
if (!requireAdmin(req, res)) return;
336340
try {
337341
const sevenDaysAgo = new Date();
338342
sevenDaysAgo.setUTCDate(sevenDaysAgo.getUTCDate() - 7);
@@ -372,7 +376,6 @@ module.exports.getTopProjects = async (req, res) => {
372376
// Projects with zero API calls in the last 14 days that had prior activity.
373377
// ---------------------------------------------------------------------------
374378
module.exports.getChurnSignals = async (req, res) => {
375-
if (!requireAdmin(req, res)) return;
376379
try {
377380
const fourteenDaysAgo = new Date();
378381
fourteenDaysAgo.setUTCDate(fourteenDaysAgo.getUTCDate() - 14);

‎apps/dashboard-api/src/routes/admin.metrics.js‎

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const express = require('express');
22
const router = express.Router();
33
const authMiddleware = require('../middlewares/authMiddleware');
44
const {
5+
requireAdmin,
56
getOverview,
67
getActivationFunnel,
78
getCohorts,
@@ -11,14 +12,16 @@ const {
1112
getChurnSignals,
1213
} = require('../controllers/admin.metrics.controller');
1314

14-
// All admin routes require a valid dashboard session.
15-
// The controller's requireAdmin() guard enforces isAdmin=true.
16-
router.get('/overview', authMiddleware, getOverview);
17-
router.get('/activation-funnel', authMiddleware, getActivationFunnel);
18-
router.get('/cohorts', authMiddleware, getCohorts);
19-
router.get('/feature-usage', authMiddleware, getFeatureUsage);
20-
router.get('/reliability', authMiddleware, getReliability);
21-
router.get('/top-projects', authMiddleware, getTopProjects);
22-
router.get('/churn-signals', authMiddleware, getChurnSignals);
15+
// All admin routes require a valid dashboard session and isAdmin=true.
16+
router.use(authMiddleware);
17+
router.use(requireAdmin);
18+
19+
router.get('/overview', getOverview);
20+
router.get('/activation-funnel', getActivationFunnel);
21+
router.get('/cohorts', getCohorts);
22+
router.get('/feature-usage', getFeatureUsage);
23+
router.get('/reliability', getReliability);
24+
router.get('/top-projects', getTopProjects);
25+
router.get('/churn-signals', getChurnSignals);
2326

2427
module.exports = router;

‎packages/common/src/queues/reliabilityAlertQueue.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ async function scheduleReliabilityAlert() {
3232
/**
3333
* Run the reliability check.
3434
* Looks at the last 15 minutes of ApiAnalytics.
35-
* If a project has >=20 total requests and >5% error rate (using >= 500 for true platform errors),
35+
* If a project has >=20 total requests and >=5% error rate (using >= 500 for true platform errors),
3636
* it writes a PlatformEvent 'reliability_spike'.
3737
*/
3838
async function runReliabilityCheck() {

0 commit comments

Comments
 (0)