From 6cc3c50be37b1bf170a0ceb3f60c1b995d89de29 Mon Sep 17 00:00:00 2001 From: phucd5 Date: Sat, 2 Dec 2023 15:13:32 -0500 Subject: [PATCH 1/2] added metrics --- API-Documentation.md | 41 +++++ server/__tests__/controllers/metricsTest.js | 174 ++++++++++++++++++++ server/controllers/metrics.js | 80 +++++++++ server/models/Metrics.js | 35 ++++ server/routes/metrics.js | 29 ++++ 5 files changed, 359 insertions(+) create mode 100644 server/__tests__/controllers/metricsTest.js create mode 100644 server/controllers/metrics.js create mode 100644 server/models/Metrics.js create mode 100644 server/routes/metrics.js diff --git a/API-Documentation.md b/API-Documentation.md index facbe0c..5887e11 100644 --- a/API-Documentation.md +++ b/API-Documentation.md @@ -280,4 +280,45 @@ Add a reply to a message. "message_id": "653ea4a35b858b2542ea4f13", "content": "this is a test reply to a test message" } +``` + +## Metrics Endpoints + +### Create Metrics + +Creates a new metrics record. + +- **Endpoint:** `POST /metrics/create-metrics` +- **Body:** + +```json +{ + "total_distribution": 50 +} +``` + +### Increment Clicks + +Increments the click count of a specified metrics record by 1. + +- **Endpoint:** `POST /metrics/increment-clicks` +- **Body:** + +```json +{ + "metricsName": "DailyUserVisits" +} +``` + +### Get Metrics by Name + +Retrieves a metrics record by its name. + +- **Endpoint:** `POST /metrics/get-metrics` +- **Body:** + +```json +{ + "metricsName": "DailyUserVisits" +} ``` \ No newline at end of file diff --git a/server/__tests__/controllers/metricsTest.js b/server/__tests__/controllers/metricsTest.js new file mode 100644 index 0000000..c6dabba --- /dev/null +++ b/server/__tests__/controllers/metricsTest.js @@ -0,0 +1,174 @@ +import { jest } from "@jest/globals"; +import { + createMetrics, + incrementClicks, + getMetricsByName, +} from "../../controllers/metrics.js"; +import MetricsModel from "../../models/Metrics.js"; +import httpMocks from "node-mocks-http"; + +jest.mock("../../models/Metrics"); + +describe("createMetrics", () => { + it("should successfully create a metrics record", async () => { + const mockMetricsData = { + clicks: 0, + total_distribution: 50, + }; + // Mock the save function to return an object with the input data + MetricsModel.prototype.save = jest.fn().mockImplementation(function () { + return { ...mockMetricsData, _id: this._id }; + }); + + const req = httpMocks.createRequest({ + body: { total_distribution: 50 }, + }); + const res = httpMocks.createResponse(); + + await createMetrics(req, res); + + const responseData = JSON.parse(res._getData()); + + expect(MetricsModel.prototype.save).toHaveBeenCalled(); + expect(res.statusCode).toBe(200); + expect(responseData.clicks).toBe(mockMetricsData.clicks); + expect(responseData.total_distribution).toBe( + mockMetricsData.total_distribution + ); + expect(responseData).toHaveProperty("_id"); + }); + it("should return 500 on server errors", async () => { + MetricsModel.prototype.save = jest.fn().mockImplementation(() => { + throw new Error("Internal Server Error"); + }); + + const req = httpMocks.createRequest({ + body: { total_distribution: 50 }, + }); + const res = httpMocks.createResponse(); + + await createMetrics(req, res); + + expect(res.statusCode).toBe(500); + expect(res._getData()).toContain("Internal Server Error"); + }); +}); + +describe("incrementClicks", () => { + it("should successfully increment the clicks of a metrics record", async () => { + const mockMetricsData = { + _id: "someMetricsId", + metrics_name: "TestMetric", + clicks: 1, + }; + MetricsModel.findOneAndUpdate = jest + .fn() + .mockResolvedValue(mockMetricsData); + + const req = httpMocks.createRequest({ + body: { metricsName: "TestMetric" }, + }); + const res = httpMocks.createResponse(); + + await incrementClicks(req, res); + + expect(MetricsModel.findOneAndUpdate).toHaveBeenCalledWith( + { metrics_name: "TestMetric" }, + { $inc: { clicks: 1 } }, + { new: true } + ); + expect(res.statusCode).toBe(200); + expect(JSON.parse(res._getData())).toEqual(mockMetricsData); + }); + it("should return 404 if the metrics record is not found", async () => { + MetricsModel.findOneAndUpdate = jest.fn().mockResolvedValue(null); + + const req = httpMocks.createRequest({ + body: { metricsName: "NonexistentMetric" }, + }); + const res = httpMocks.createResponse(); + + await incrementClicks(req, res); + + expect(MetricsModel.findOneAndUpdate).toHaveBeenCalledWith( + { metrics_name: "NonexistentMetric" }, + { $inc: { clicks: 1 } }, + { new: true } + ); + expect(res.statusCode).toBe(404); + expect(res._getData()).toContain("Metrics record not found"); + }); + it("should handle server errors", async () => { + MetricsModel.findOneAndUpdate.mockImplementationOnce(() => { + throw new Error("Internal Server Error"); + }); + + const req = httpMocks.createRequest({ + body: { metricsName: "TestMetric" }, + }); + const res = httpMocks.createResponse(); + + await incrementClicks(req, res); + + expect(res.statusCode).toBe(500); + expect(res._getData()).toContain("Internal Server Error"); + }); +}); + +describe("getMetricsByName", () => { + it("should successfully retrieve a metrics record by name", async () => { + const mockMetricsData = { + _id: "someMetricsId", + metrics_name: "TestMetric", + clicks: 10, + total_distribution: 50, + }; + MetricsModel.findOne = jest.fn().mockResolvedValue(mockMetricsData); + + const req = httpMocks.createRequest({ + body: { metricsName: "TestMetric" }, + }); + const res = httpMocks.createResponse(); + + await getMetricsByName(req, res); + + expect(MetricsModel.findOne).toHaveBeenCalledWith({ + metrics_name: "TestMetric", + }); + expect(res.statusCode).toBe(200); + expect(JSON.parse(res._getData())).toEqual(mockMetricsData); + }); + + it("should return 404 if the metrics record is not found", async () => { + MetricsModel.findOne = jest.fn().mockResolvedValue(null); + + const req = httpMocks.createRequest({ + body: { metricsName: "NonexistentMetric" }, + }); + const res = httpMocks.createResponse(); + + await getMetricsByName(req, res); + + expect(MetricsModel.findOne).toHaveBeenCalledWith({ + metrics_name: "NonexistentMetric", + }); + expect(res.statusCode).toBe(404); + expect(res._getData()).toContain("Metrics record not found"); + }); + + it("should handle server errors", async () => { + MetricsModel.findOne.mockImplementationOnce(() => { + throw new Error("Internal Server Error"); + }); + + const req = httpMocks.createRequest({ + body: { metricsName: "TestMetric" }, + }); + const res = httpMocks.createResponse(); + + await getMetricsByName(req, res); + + expect(res.statusCode).toBe(500); + expect(res._getData()).toContain("Internal Server Error"); + }); +}); diff --git a/server/controllers/metrics.js b/server/controllers/metrics.js new file mode 100644 index 0000000..fdc7b43 --- /dev/null +++ b/server/controllers/metrics.js @@ -0,0 +1,80 @@ +import MetricsModel from "../models/Metrics.js"; +import { + handleServerError, + handleSuccess, + handleNotFound, +} from "../utils/handlers.js"; + +/** + * Creates a new metrics record. + * + * Initializes a new metrics record with default clicks (0) and a specified total_distribution from the request body. + * + * @param {Object} req - The request object containing the total_distribution value. + * @param {Object} res - The response object to send back the created metrics data or an error message. + */ +export const createMetrics = async (req, res) => { + try { + const totalDistribution = req.body.total_distribution || 50; // Default to 50 if not provided + const metrics = new MetricsModel({ + clicks: 0, + total_distribution: totalDistribution, + }); + await metrics.save(); + handleSuccess(res, metrics); + } catch (err) { + handleServerError(res, err); + } +}; + +/** + * Increments the click counter of a metrics record. + * + * Accepts a metrics name from the request body and increments its clicks counter by 1. + * + * @param {Object} req - Request object containing the metricsName in the body. + * @param {Object} res - Response object to send back the updated metrics data or an error message. + */ +export const incrementClicks = async (req, res) => { + try { + const { metricsName } = req.body; + const metrics = await MetricsModel.findOneAndUpdate( + { metrics_name: metricsName }, + { $inc: { clicks: 1 } }, + { new: true } + ); + + if (!metrics) { + return handleNotFound(res, "Metrics record not found"); + } + + handleSuccess(res, metrics); + } catch (err) { + handleServerError(res, err); + } +}; + +/** + * Retrieves a metrics record by its name. + * + * Fetches a metrics record from the database using its name. + * + * @param {Object} req - Request object containing the metricsName in the body. + * @param {Object} res - Response object to return the metrics data or an error message. + */ +export const getMetricsByName = async (req, res) => { + try { + const { metricsName } = req.body; + const metrics = await MetricsModel.findOne({ + metrics_name: metricsName, + }); + + if (!metrics) { + return handleNotFound(res, "Metrics record not found"); + } + + handleSuccess(res, metrics); + } catch (err) { + handleServerError(res, err); + } +}; diff --git a/server/models/Metrics.js b/server/models/Metrics.js new file mode 100644 index 0000000..3f23bf0 --- /dev/null +++ b/server/models/Metrics.js @@ -0,0 +1,35 @@ +/** + * Metrics Model Schema + * + * Defines the Mongoose schema for metrics records in the application. It includes fields for metrics name, + * click count, and total distribution. The metrics name is unique for each record, ensuring no duplicates. + * The schema is used to for A/B testing of the application. + * + * Schema Fields: + * - metrics_name: Unique name identifier for the metrics record. + * - clicks: Count of clicks, used for tracking user interactions. + * - total_distribution: Represents the distribution value, defaulting to 50. + * + */ + +import mongoose from "mongoose"; +const { Schema } = mongoose; + +export const MetricsSchema = new Schema({ + clicks: { + type: Number, + default: 0, + }, + total_distribution: { + type: Number, + default: 50, + }, + metrics_name: { + type: String, + required: true, + unique: true, + }, +}); + +const MetricsModel = mongoose.model("Metrics", MetricsSchema); +export default MetricsModel; diff --git a/server/routes/metrics.js b/server/routes/metrics.js new file mode 100644 index 0000000..5bac26d --- /dev/null +++ b/server/routes/metrics.js @@ -0,0 +1,29 @@ +/** + * Metrics Routes + * + * Express routes for operations related to metrics records. Includes routes for + * creating, incrementing the click count of a metric, and retrieving metric data by name. + * The request handling is delegated to the metrics controller. + * + * Routes: + * - POST /create-metrics: Handles the creation of a new metrics record. + * - POST /increment-clicks: Handles incrementing the click count of a metric. + * - POST /get-metrics: Retrieves a metrics record by its name. + */ + +import express from "express"; +import { + createMetrics, + incrementClicks, + getMetricsByName, +} from "../controllers/metrics.js"; + +const router = express.Router(); + +router.post("/create-metrics", createMetrics); + +router.post("/increment-clicks", incrementClicks); + +router.post("/get-metrics", getMetricsByName); + +export default router; From 7a93ef73a02f95e4ced8ed12b8e17e319a6b1480 Mon Sep 17 00:00:00 2001 From: phucd5 Date: Sat, 2 Dec 2023 15:32:53 -0500 Subject: [PATCH 2/2] fixed metrics --- server/__tests__/controllers/metricsTest.js | 16 +++++------ server/controllers/metrics.js | 31 ++++++++++++++++----- server/server.js | 2 ++ 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/server/__tests__/controllers/metricsTest.js b/server/__tests__/controllers/metricsTest.js index c6dabba..92acd2d 100644 --- a/server/__tests__/controllers/metricsTest.js +++ b/server/__tests__/controllers/metricsTest.js @@ -14,14 +14,14 @@ describe("createMetrics", () => { const mockMetricsData = { clicks: 0, total_distribution: 50, + metrics_name: "TestMetric", }; - // Mock the save function to return an object with the input data MetricsModel.prototype.save = jest.fn().mockImplementation(function () { return { ...mockMetricsData, _id: this._id }; }); const req = httpMocks.createRequest({ - body: { total_distribution: 50 }, + body: { total_distribution: 50, metrics_name: "TestMetric" }, }); const res = httpMocks.createResponse(); @@ -31,19 +31,17 @@ describe("createMetrics", () => { expect(MetricsModel.prototype.save).toHaveBeenCalled(); expect(res.statusCode).toBe(200); - expect(responseData.clicks).toBe(mockMetricsData.clicks); - expect(responseData.total_distribution).toBe( - mockMetricsData.total_distribution - ); + expect(responseData).toMatchObject(mockMetricsData); expect(responseData).toHaveProperty("_id"); }); + it("should return 500 on server errors", async () => { MetricsModel.prototype.save = jest.fn().mockImplementation(() => { throw new Error("Internal Server Error"); }); const req = httpMocks.createRequest({ - body: { total_distribution: 50 }, + body: { total_distribution: 50, metrics_name: "TestMetric" }, }); const res = httpMocks.createResponse(); @@ -78,7 +76,9 @@ describe("incrementClicks", () => { { new: true } ); expect(res.statusCode).toBe(200); - expect(JSON.parse(res._getData())).toEqual(mockMetricsData); + expect(JSON.parse(res._getData())).toEqual({ + message: "Metrics incremented sucessfully", + }); }); it("should return 404 if the metrics record is not found", async () => { MetricsModel.findOneAndUpdate = jest.fn().mockResolvedValue(null); diff --git a/server/controllers/metrics.js b/server/controllers/metrics.js index fdc7b43..32677db 100644 --- a/server/controllers/metrics.js +++ b/server/controllers/metrics.js @@ -1,3 +1,16 @@ +/** + * Metrics Controller + * + * This file serves as the controller for metrics-related operations in the API. + * It includes functionalities for managing metrics records such as creating new records, + * incrementing click counts, and retrieving metrics data by name. + * + * Dependencies: + * - MetricsModel: The Mongoose model used for metrics data interactions with the MongoDB database. + * - Handlers: Utility functions for handling various HTTP response scenarios, such as server errors, + * successful responses, and resource not found errors. + */ + import MetricsModel from "../models/Metrics.js"; import { handleServerError, @@ -15,11 +28,14 @@ import { */ export const createMetrics = async (req, res) => { try { - const totalDistribution = req.body.total_distribution || 50; // Default to 50 if not provided + const { total_distribution = 50, metrics_name } = req.body; + const metrics = new MetricsModel({ clicks: 0, - total_distribution: totalDistribution, + total_distribution, + metrics_name, }); + await metrics.save(); handleSuccess(res, metrics); } catch (err) { @@ -37,9 +53,10 @@ export const createMetrics = async (req, res) => { */ export const incrementClicks = async (req, res) => { try { - const { metricsName } = req.body; + const { metrics_name } = req.body; + console.log(metrics_name); const metrics = await MetricsModel.findOneAndUpdate( - { metrics_name: metricsName }, + { metrics_name: metrics_name }, { $inc: { clicks: 1 } }, { new: true } ); @@ -48,7 +65,7 @@ export const incrementClicks = async (req, res) => { return handleNotFound(res, "Metrics record not found"); } - handleSuccess(res, metrics); + handleSuccess(res, { message: "Metrics incremented sucessfully" }); } catch (err) { handleServerError(res, err); } @@ -64,9 +81,9 @@ export const incrementClicks = async (req, res) => { */ export const getMetricsByName = async (req, res) => { try { - const { metricsName } = req.body; + const { metrics_name } = req.body; const metrics = await MetricsModel.findOne({ - metrics_name: metricsName, + metrics_name: metrics_name, }); if (!metrics) { diff --git a/server/server.js b/server/server.js index e3cec36..b2578da 100644 --- a/server/server.js +++ b/server/server.js @@ -17,6 +17,7 @@ import authRoutes from "./routes/auth.js"; import messageRoutes from "./routes/message.js"; import replyRoutes from "./routes/reply.js"; import userRoutes from "./routes/user.js"; +import metricRoutes from "./routes/metrics.js"; // Load environment variables from .env file dotenv.config(); @@ -56,6 +57,7 @@ app.use("/auth", authRoutes); app.use("/message", messageRoutes); app.use("/reply", replyRoutes); app.use("/user", userRoutes); +app.use("/metrics", metricRoutes); const server = app.listen(PORT, console.log(`Server running on port ${PORT}`));