diff --git a/apps/dashboard-api/src/__tests__/analytics.controller.test.js b/apps/dashboard-api/src/__tests__/analytics.controller.test.js index 6f89a2a94..e8edc519a 100644 --- a/apps/dashboard-api/src/__tests__/analytics.controller.test.js +++ b/apps/dashboard-api/src/__tests__/analytics.controller.test.js @@ -249,159 +249,5 @@ describe("Analytics Controller", () => { }); }); - describe("getActivationFunnel", () => { - it("should return status of activation funnel steps", async () => { - mockPlatformEventFind.mockReturnValue({ - sort: jest.fn().mockReturnThis(), - select: jest.fn().mockReturnThis(), - lean: jest.fn().mockResolvedValue([ - { event: "signup_completed", timestamp: "2026-06-10T00:00:00Z" }, - { event: "email_verified", timestamp: "2026-06-10T00:05:00Z" }, - ]), - }); - - await controller.getActivationFunnel(req, res, next); - - expect(res.json).toHaveBeenCalledTimes(1); - const responseData = res.json.mock.calls[0][0]; - expect(responseData.success).toBe(true); - const steps = responseData.data.steps; - expect(steps[0]).toEqual({ - step: "signup_completed", - order: 1, - completed: true, - completedAt: "2026-06-10T00:00:00Z", - }); - expect(steps[1]).toEqual({ - step: "email_verified", - order: 2, - completed: true, - completedAt: "2026-06-10T00:05:00Z", - }); - expect(steps[2]).toEqual({ - step: "project_created", - order: 3, - completed: false, - completedAt: null, - }); - }); - }); - - describe("getRetention", () => { - it("should return d1/d7/d30 retention flags", async () => { - mockPlatformEventFindOne.mockReturnValue({ - sort: jest.fn().mockReturnThis(), - lean: jest.fn().mockResolvedValue({ - timestamp: "2026-06-10T12:00:00Z", - }), - }); - - mockDeveloperActivityFindOne.mockImplementation(({ date }) => { - return { - lean: jest.fn().mockResolvedValue({ date }), - }; - }); - - await controller.getRetention(req, res, next); - - expect(res.json).toHaveBeenCalledTimes(1); - const responseData = res.json.mock.calls[0][0]; - expect(responseData.success).toBe(true); - expect(responseData.data.d1).toBe(true); - expect(responseData.data.d7).toBe(true); - expect(responseData.data.d30).toBe(true); - }); - - it("should return false flags if no signup event exists", async () => { - mockPlatformEventFindOne.mockReturnValue({ - sort: jest.fn().mockReturnThis(), - lean: jest.fn().mockResolvedValue(null), - }); - - await controller.getRetention(req, res, next); - - expect(res.json).toHaveBeenCalledTimes(1); - const responseData = res.json.mock.calls[0][0]; - expect(responseData.data).toEqual({ - d1: false, - d7: false, - d30: false, - signupDate: null, - }); - }); - }); - - describe("getEngagement", () => { - it("should return aggregated engagement metrics", async () => { - mockDeveloperActivityAggregate.mockResolvedValueOnce([ - { - totalApiCalls: 500, - totalMailSent: 50, - totalStorageUploads: 10, - totalWebhooksFired: 5, - activeDays: 3, - allProjectIds: [["proj1"], ["proj2", "proj1"]], - }, - ]); - - await controller.getEngagement(req, res, next); - - expect(res.json).toHaveBeenCalledTimes(1); - const responseData = res.json.mock.calls[0][0]; - expect(responseData.success).toBe(true); - expect(responseData.data).toEqual({ - window: "30d", - totalApiCalls: 500, - totalMailSent: 50, - totalStorageUploads: 10, - totalWebhooksFired: 5, - activeDays: 3, - uniqueActiveProjects: 2, - }); - }); - }); - - describe("getNorthStar", () => { - it("should return north star metrics", async () => { - mockProjectFind.mockReturnValue({ - select: jest.fn().mockReturnValue({ - lean: jest.fn().mockResolvedValue([ - { _id: "proj1", name: "Proj 1" }, - { _id: "proj2", name: "Proj 2" }, - ]), - }), - }); - - mockLogDistinct.mockResolvedValueOnce(["proj1"]); - - await controller.getNorthStar(req, res, next); - - expect(res.json).toHaveBeenCalledTimes(1); - const responseData = res.json.mock.calls[0][0]; - expect(responseData.success).toBe(true); - expect(responseData.data).toEqual({ - activeProjects: 1, - totalProjects: 2, - percentage: 50, - }); - }); - - it("should return 0 metrics if developer has no projects", async () => { - mockProjectFind.mockReturnValue({ - select: jest.fn().mockReturnValue({ - lean: jest.fn().mockResolvedValue([]), - }), - }); - - await controller.getNorthStar(req, res, next); - - expect(res.json).toHaveBeenCalledTimes(1); - const responseData = res.json.mock.calls[0][0]; - expect(responseData.data).toEqual({ - activeProjects: 0, - totalProjects: 0, - percentage: 0, - }); - }); - }); }); + diff --git a/apps/dashboard-api/src/controllers/analytics.controller.js b/apps/dashboard-api/src/controllers/analytics.controller.js index c1d1128ae..4e424f16e 100644 --- a/apps/dashboard-api/src/controllers/analytics.controller.js +++ b/apps/dashboard-api/src/controllers/analytics.controller.js @@ -145,206 +145,3 @@ module.exports.getRecentActivity = async (req, res, next) => { } }; -// --------------------------------------------------------------------------- -// ACTIVATION FUNNEL -// Returns step-by-step conversion rates for the current developer. -// --------------------------------------------------------------------------- -module.exports.getActivationFunnel = async (req, res, next) => { - try { - const developerId = req.user._id; - - const FUNNEL_STEPS = [ - "signup_completed", - "email_verified", - "project_created", - "collection_created", - "first_api_success", - ]; - const EVENT_ALIASES = { - first_api_success: 'first_api_call', - }; - - // Fetch one event per step (we only need existence, not count) - const events = await PlatformEvent.find({ - developerId, - event: { $in: [...FUNNEL_STEPS, ...Object.keys(EVENT_ALIASES)] }, - }) - .sort({ timestamp: 1 }) - .select("event timestamp") - .lean(); - - const completed = {}; - for (const e of events) { - const step = EVENT_ALIASES[e.event] || e.event; - if (!completed[step]) completed[step] = e.timestamp; - } - - const steps = FUNNEL_STEPS.map((step, i) => ({ - step, - order: i + 1, - completed: !!completed[step], - completedAt: completed[step] || null, - })); - - return new ApiResponse({ steps }).send(res); - } catch (err) { - next(err); - } -}; - -// --------------------------------------------------------------------------- -// RETENTION (D1 / D7 / D30) -// Checks whether the developer was active on Day+1, Day+7, Day+30 after signup. -// --------------------------------------------------------------------------- -module.exports.getRetention = async (req, res, next) => { - try { - const developerId = req.user._id; - - // Find signup event to anchor the cohort start date - const signupEvent = await PlatformEvent.findOne({ - developerId, - event: "signup_completed", - }) - .sort({ timestamp: 1 }) - .lean(); - - if (!signupEvent) { - return new ApiResponse({ - d1: false, - d7: false, - d30: false, - signupDate: null, - }).send(res); - } - - const signupDate = new Date(signupEvent.timestamp); - signupDate.setUTCHours(0, 0, 0, 0); - - const checkDay = async (daysAfter) => { - const targetDate = new Date(signupDate); - targetDate.setUTCDate(targetDate.getUTCDate() + daysAfter); - const nextDate = new Date(targetDate); - nextDate.setUTCDate(nextDate.getUTCDate() + 1); - - const activity = await DeveloperActivity.findOne({ - developerId, - date: { $gte: targetDate, $lt: nextDate }, - }).lean(); - return !!activity; - }; - - const [d1, d7, d30] = await Promise.all([ - checkDay(1), - checkDay(7), - checkDay(30), - ]); - - return new ApiResponse({ d1, d7, d30, signupDate }).send(res); - } catch (err) { - next(err); - } -}; - -// --------------------------------------------------------------------------- -// FEATURE ENGAGEMENT (trailing 30 days) -// Returns per-feature usage totals across all projects for the developer. -// --------------------------------------------------------------------------- -module.exports.getEngagement = async (req, res, next) => { - try { - const developerId = req.user._id; - - const thirtyDaysAgo = new Date(); - thirtyDaysAgo.setUTCDate(thirtyDaysAgo.getUTCDate() - 30); - - const agg = await DeveloperActivity.aggregate([ - { - $match: { - developerId: new mongoose.Types.ObjectId(developerId), - date: { $gte: thirtyDaysAgo }, - }, - }, - { - $group: { - _id: null, - totalApiCalls: { $sum: "$apiCallCount" }, - totalMailSent: { $sum: "$mailSentCount" }, - totalStorageUploads: { $sum: "$storageUploadsCount" }, - totalWebhooksFired: { $sum: "$webhookTriggeredCount" }, - activeDays: { $sum: 1 }, - allProjectIds: { $push: "$activeProjectIds" }, - }, - }, - ]); - - const result = agg[0] || { - totalApiCalls: 0, - totalMailSent: 0, - totalStorageUploads: 0, - totalWebhooksFired: 0, - activeDays: 0, - }; - - // Unique active projects in the 30-day window - const flatProjectIds = (result.allProjectIds || []).flat(); - const uniqueActiveProjects = new Set(flatProjectIds.map(String)).size; - - return new ApiResponse({ - window: "30d", - totalApiCalls: result.totalApiCalls, - totalMailSent: result.totalMailSent, - totalStorageUploads: result.totalStorageUploads, - totalWebhooksFired: result.totalWebhooksFired, - activeDays: result.activeDays, - uniqueActiveProjects, - }).send(res); - } catch (err) { - next(err); - } -}; - -// --------------------------------------------------------------------------- -// NORTH STAR METRIC -// "Projects making successful API calls in the last 7 days" -// --------------------------------------------------------------------------- -module.exports.getNorthStar = async (req, res, next) => { - try { - const developerId = req.user._id; - - const sevenDaysAgo = new Date(); - sevenDaysAgo.setUTCDate(sevenDaysAgo.getUTCDate() - 7); - - // Projects owned by this developer (North Star should be owner-only) - const allProjects = await Project.find({ owner: developerId }) - .select("_id name") - .lean(); - const projectIds = allProjects.map((p) => p._id); - const totalProjects = projectIds.length; - - if (totalProjects === 0) { - return new ApiResponse({ - activeProjects: 0, - totalProjects: 0, - percentage: 0, - }).send(res); - } - - // Projects with at least one 2xx log in the last 7 days - const activeProjectIds = await Log.distinct("projectId", { - projectId: { $in: projectIds }, - status: { $gte: 200, $lt: 300 }, - timestamp: { $gte: sevenDaysAgo }, - }); - - const activeProjects = activeProjectIds.length; - const percentage = - totalProjects > 0 - ? Math.round((activeProjects / totalProjects) * 100) - : 0; - - return new ApiResponse({ activeProjects, totalProjects, percentage }).send( - res, - ); - } catch (err) { - next(err); - } -}; diff --git a/apps/dashboard-api/src/routes/analytics.js b/apps/dashboard-api/src/routes/analytics.js index fe5ce223b..28fe206ae 100644 --- a/apps/dashboard-api/src/routes/analytics.js +++ b/apps/dashboard-api/src/routes/analytics.js @@ -1,16 +1,9 @@ const express = require("express"); const router = express.Router(); const analyticsController = require("../controllers/analytics.controller"); -const authMiddleware = require("../middlewares/authMiddleware"); const authFlexible = require("../middlewares/authFlexible"); router.get("/stats", authFlexible, analyticsController.getGlobalStats); router.get("/activity", authFlexible, analyticsController.getRecentActivity); -// --- Metrics Stack --- -router.get("/funnel", authMiddleware, analyticsController.getActivationFunnel); -router.get("/retention", authMiddleware, analyticsController.getRetention); -router.get("/engagement", authMiddleware, analyticsController.getEngagement); -router.get("/north-star", authMiddleware, analyticsController.getNorthStar); - module.exports = router; diff --git a/apps/web-dashboard/src/components/Dashboard/DashboardHeader.jsx b/apps/web-dashboard/src/components/Dashboard/DashboardHeader.jsx index 6c9f74491..ed1762fe7 100644 --- a/apps/web-dashboard/src/components/Dashboard/DashboardHeader.jsx +++ b/apps/web-dashboard/src/components/Dashboard/DashboardHeader.jsx @@ -1,22 +1,18 @@ import React from 'react'; import { Plus } from 'lucide-react'; -const DashboardHeader = ({ title = "Overview", subtitle = "Manage and monitor your projects in real-time.", onCreateProject }) => { +const DashboardHeader = ({ title = "Overview", subtitle = "Manage your projects.", onCreateProject }) => { return ( -
+
{subtitle}