Skip to content

Commit da79159

Browse files
feat(dashboard): UI redesign & dead code cleanup (#341)
* feat(dashboard): vercel style redesign & remove dead metrics code * updated package-lock.json * fix(dashboard): restore focus-visible and fix empty state rendering
1 parent b789858 commit da79159

16 files changed

Lines changed: 230 additions & 694 deletions

apps/dashboard-api/src/__tests__/analytics.controller.test.js

Lines changed: 1 addition & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -249,159 +249,5 @@ describe("Analytics Controller", () => {
249249
});
250250
});
251251

252-
describe("getActivationFunnel", () => {
253-
it("should return status of activation funnel steps", async () => {
254-
mockPlatformEventFind.mockReturnValue({
255-
sort: jest.fn().mockReturnThis(),
256-
select: jest.fn().mockReturnThis(),
257-
lean: jest.fn().mockResolvedValue([
258-
{ event: "signup_completed", timestamp: "2026-06-10T00:00:00Z" },
259-
{ event: "email_verified", timestamp: "2026-06-10T00:05:00Z" },
260-
]),
261-
});
262-
263-
await controller.getActivationFunnel(req, res, next);
264-
265-
expect(res.json).toHaveBeenCalledTimes(1);
266-
const responseData = res.json.mock.calls[0][0];
267-
expect(responseData.success).toBe(true);
268-
const steps = responseData.data.steps;
269-
expect(steps[0]).toEqual({
270-
step: "signup_completed",
271-
order: 1,
272-
completed: true,
273-
completedAt: "2026-06-10T00:00:00Z",
274-
});
275-
expect(steps[1]).toEqual({
276-
step: "email_verified",
277-
order: 2,
278-
completed: true,
279-
completedAt: "2026-06-10T00:05:00Z",
280-
});
281-
expect(steps[2]).toEqual({
282-
step: "project_created",
283-
order: 3,
284-
completed: false,
285-
completedAt: null,
286-
});
287-
});
288-
});
289-
290-
describe("getRetention", () => {
291-
it("should return d1/d7/d30 retention flags", async () => {
292-
mockPlatformEventFindOne.mockReturnValue({
293-
sort: jest.fn().mockReturnThis(),
294-
lean: jest.fn().mockResolvedValue({
295-
timestamp: "2026-06-10T12:00:00Z",
296-
}),
297-
});
298-
299-
mockDeveloperActivityFindOne.mockImplementation(({ date }) => {
300-
return {
301-
lean: jest.fn().mockResolvedValue({ date }),
302-
};
303-
});
304-
305-
await controller.getRetention(req, res, next);
306-
307-
expect(res.json).toHaveBeenCalledTimes(1);
308-
const responseData = res.json.mock.calls[0][0];
309-
expect(responseData.success).toBe(true);
310-
expect(responseData.data.d1).toBe(true);
311-
expect(responseData.data.d7).toBe(true);
312-
expect(responseData.data.d30).toBe(true);
313-
});
314-
315-
it("should return false flags if no signup event exists", async () => {
316-
mockPlatformEventFindOne.mockReturnValue({
317-
sort: jest.fn().mockReturnThis(),
318-
lean: jest.fn().mockResolvedValue(null),
319-
});
320-
321-
await controller.getRetention(req, res, next);
322-
323-
expect(res.json).toHaveBeenCalledTimes(1);
324-
const responseData = res.json.mock.calls[0][0];
325-
expect(responseData.data).toEqual({
326-
d1: false,
327-
d7: false,
328-
d30: false,
329-
signupDate: null,
330-
});
331-
});
332-
});
333-
334-
describe("getEngagement", () => {
335-
it("should return aggregated engagement metrics", async () => {
336-
mockDeveloperActivityAggregate.mockResolvedValueOnce([
337-
{
338-
totalApiCalls: 500,
339-
totalMailSent: 50,
340-
totalStorageUploads: 10,
341-
totalWebhooksFired: 5,
342-
activeDays: 3,
343-
allProjectIds: [["proj1"], ["proj2", "proj1"]],
344-
},
345-
]);
346-
347-
await controller.getEngagement(req, res, next);
348-
349-
expect(res.json).toHaveBeenCalledTimes(1);
350-
const responseData = res.json.mock.calls[0][0];
351-
expect(responseData.success).toBe(true);
352-
expect(responseData.data).toEqual({
353-
window: "30d",
354-
totalApiCalls: 500,
355-
totalMailSent: 50,
356-
totalStorageUploads: 10,
357-
totalWebhooksFired: 5,
358-
activeDays: 3,
359-
uniqueActiveProjects: 2,
360-
});
361-
});
362-
});
363-
364-
describe("getNorthStar", () => {
365-
it("should return north star metrics", async () => {
366-
mockProjectFind.mockReturnValue({
367-
select: jest.fn().mockReturnValue({
368-
lean: jest.fn().mockResolvedValue([
369-
{ _id: "proj1", name: "Proj 1" },
370-
{ _id: "proj2", name: "Proj 2" },
371-
]),
372-
}),
373-
});
374-
375-
mockLogDistinct.mockResolvedValueOnce(["proj1"]);
376-
377-
await controller.getNorthStar(req, res, next);
378-
379-
expect(res.json).toHaveBeenCalledTimes(1);
380-
const responseData = res.json.mock.calls[0][0];
381-
expect(responseData.success).toBe(true);
382-
expect(responseData.data).toEqual({
383-
activeProjects: 1,
384-
totalProjects: 2,
385-
percentage: 50,
386-
});
387-
});
388-
389-
it("should return 0 metrics if developer has no projects", async () => {
390-
mockProjectFind.mockReturnValue({
391-
select: jest.fn().mockReturnValue({
392-
lean: jest.fn().mockResolvedValue([]),
393-
}),
394-
});
395-
396-
await controller.getNorthStar(req, res, next);
397-
398-
expect(res.json).toHaveBeenCalledTimes(1);
399-
const responseData = res.json.mock.calls[0][0];
400-
expect(responseData.data).toEqual({
401-
activeProjects: 0,
402-
totalProjects: 0,
403-
percentage: 0,
404-
});
405-
});
406-
});
407252
});
253+

apps/dashboard-api/src/controllers/analytics.controller.js

Lines changed: 0 additions & 203 deletions
Original file line numberDiff line numberDiff line change
@@ -145,206 +145,3 @@ module.exports.getRecentActivity = async (req, res, next) => {
145145
}
146146
};
147147

148-
// ---------------------------------------------------------------------------
149-
// ACTIVATION FUNNEL
150-
// Returns step-by-step conversion rates for the current developer.
151-
// ---------------------------------------------------------------------------
152-
module.exports.getActivationFunnel = async (req, res, next) => {
153-
try {
154-
const developerId = req.user._id;
155-
156-
const FUNNEL_STEPS = [
157-
"signup_completed",
158-
"email_verified",
159-
"project_created",
160-
"collection_created",
161-
"first_api_success",
162-
];
163-
const EVENT_ALIASES = {
164-
first_api_success: 'first_api_call',
165-
};
166-
167-
// Fetch one event per step (we only need existence, not count)
168-
const events = await PlatformEvent.find({
169-
developerId,
170-
event: { $in: [...FUNNEL_STEPS, ...Object.keys(EVENT_ALIASES)] },
171-
})
172-
.sort({ timestamp: 1 })
173-
.select("event timestamp")
174-
.lean();
175-
176-
const completed = {};
177-
for (const e of events) {
178-
const step = EVENT_ALIASES[e.event] || e.event;
179-
if (!completed[step]) completed[step] = e.timestamp;
180-
}
181-
182-
const steps = FUNNEL_STEPS.map((step, i) => ({
183-
step,
184-
order: i + 1,
185-
completed: !!completed[step],
186-
completedAt: completed[step] || null,
187-
}));
188-
189-
return new ApiResponse({ steps }).send(res);
190-
} catch (err) {
191-
next(err);
192-
}
193-
};
194-
195-
// ---------------------------------------------------------------------------
196-
// RETENTION (D1 / D7 / D30)
197-
// Checks whether the developer was active on Day+1, Day+7, Day+30 after signup.
198-
// ---------------------------------------------------------------------------
199-
module.exports.getRetention = async (req, res, next) => {
200-
try {
201-
const developerId = req.user._id;
202-
203-
// Find signup event to anchor the cohort start date
204-
const signupEvent = await PlatformEvent.findOne({
205-
developerId,
206-
event: "signup_completed",
207-
})
208-
.sort({ timestamp: 1 })
209-
.lean();
210-
211-
if (!signupEvent) {
212-
return new ApiResponse({
213-
d1: false,
214-
d7: false,
215-
d30: false,
216-
signupDate: null,
217-
}).send(res);
218-
}
219-
220-
const signupDate = new Date(signupEvent.timestamp);
221-
signupDate.setUTCHours(0, 0, 0, 0);
222-
223-
const checkDay = async (daysAfter) => {
224-
const targetDate = new Date(signupDate);
225-
targetDate.setUTCDate(targetDate.getUTCDate() + daysAfter);
226-
const nextDate = new Date(targetDate);
227-
nextDate.setUTCDate(nextDate.getUTCDate() + 1);
228-
229-
const activity = await DeveloperActivity.findOne({
230-
developerId,
231-
date: { $gte: targetDate, $lt: nextDate },
232-
}).lean();
233-
return !!activity;
234-
};
235-
236-
const [d1, d7, d30] = await Promise.all([
237-
checkDay(1),
238-
checkDay(7),
239-
checkDay(30),
240-
]);
241-
242-
return new ApiResponse({ d1, d7, d30, signupDate }).send(res);
243-
} catch (err) {
244-
next(err);
245-
}
246-
};
247-
248-
// ---------------------------------------------------------------------------
249-
// FEATURE ENGAGEMENT (trailing 30 days)
250-
// Returns per-feature usage totals across all projects for the developer.
251-
// ---------------------------------------------------------------------------
252-
module.exports.getEngagement = async (req, res, next) => {
253-
try {
254-
const developerId = req.user._id;
255-
256-
const thirtyDaysAgo = new Date();
257-
thirtyDaysAgo.setUTCDate(thirtyDaysAgo.getUTCDate() - 30);
258-
259-
const agg = await DeveloperActivity.aggregate([
260-
{
261-
$match: {
262-
developerId: new mongoose.Types.ObjectId(developerId),
263-
date: { $gte: thirtyDaysAgo },
264-
},
265-
},
266-
{
267-
$group: {
268-
_id: null,
269-
totalApiCalls: { $sum: "$apiCallCount" },
270-
totalMailSent: { $sum: "$mailSentCount" },
271-
totalStorageUploads: { $sum: "$storageUploadsCount" },
272-
totalWebhooksFired: { $sum: "$webhookTriggeredCount" },
273-
activeDays: { $sum: 1 },
274-
allProjectIds: { $push: "$activeProjectIds" },
275-
},
276-
},
277-
]);
278-
279-
const result = agg[0] || {
280-
totalApiCalls: 0,
281-
totalMailSent: 0,
282-
totalStorageUploads: 0,
283-
totalWebhooksFired: 0,
284-
activeDays: 0,
285-
};
286-
287-
// Unique active projects in the 30-day window
288-
const flatProjectIds = (result.allProjectIds || []).flat();
289-
const uniqueActiveProjects = new Set(flatProjectIds.map(String)).size;
290-
291-
return new ApiResponse({
292-
window: "30d",
293-
totalApiCalls: result.totalApiCalls,
294-
totalMailSent: result.totalMailSent,
295-
totalStorageUploads: result.totalStorageUploads,
296-
totalWebhooksFired: result.totalWebhooksFired,
297-
activeDays: result.activeDays,
298-
uniqueActiveProjects,
299-
}).send(res);
300-
} catch (err) {
301-
next(err);
302-
}
303-
};
304-
305-
// ---------------------------------------------------------------------------
306-
// NORTH STAR METRIC
307-
// "Projects making successful API calls in the last 7 days"
308-
// ---------------------------------------------------------------------------
309-
module.exports.getNorthStar = async (req, res, next) => {
310-
try {
311-
const developerId = req.user._id;
312-
313-
const sevenDaysAgo = new Date();
314-
sevenDaysAgo.setUTCDate(sevenDaysAgo.getUTCDate() - 7);
315-
316-
// Projects owned by this developer (North Star should be owner-only)
317-
const allProjects = await Project.find({ owner: developerId })
318-
.select("_id name")
319-
.lean();
320-
const projectIds = allProjects.map((p) => p._id);
321-
const totalProjects = projectIds.length;
322-
323-
if (totalProjects === 0) {
324-
return new ApiResponse({
325-
activeProjects: 0,
326-
totalProjects: 0,
327-
percentage: 0,
328-
}).send(res);
329-
}
330-
331-
// Projects with at least one 2xx log in the last 7 days
332-
const activeProjectIds = await Log.distinct("projectId", {
333-
projectId: { $in: projectIds },
334-
status: { $gte: 200, $lt: 300 },
335-
timestamp: { $gte: sevenDaysAgo },
336-
});
337-
338-
const activeProjects = activeProjectIds.length;
339-
const percentage =
340-
totalProjects > 0
341-
? Math.round((activeProjects / totalProjects) * 100)
342-
: 0;
343-
344-
return new ApiResponse({ activeProjects, totalProjects, percentage }).send(
345-
res,
346-
);
347-
} catch (err) {
348-
next(err);
349-
}
350-
};
Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,9 @@
11
const express = require("express");
22
const router = express.Router();
33
const analyticsController = require("../controllers/analytics.controller");
4-
const authMiddleware = require("../middlewares/authMiddleware");
54
const authFlexible = require("../middlewares/authFlexible");
65

76
router.get("/stats", authFlexible, analyticsController.getGlobalStats);
87
router.get("/activity", authFlexible, analyticsController.getRecentActivity);
98

10-
// --- Metrics Stack ---
11-
router.get("/funnel", authMiddleware, analyticsController.getActivationFunnel);
12-
router.get("/retention", authMiddleware, analyticsController.getRetention);
13-
router.get("/engagement", authMiddleware, analyticsController.getEngagement);
14-
router.get("/north-star", authMiddleware, analyticsController.getNorthStar);
15-
169
module.exports = router;

0 commit comments

Comments
 (0)