diff --git a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx
index c45fa1b72..2fefd3daa 100644
--- a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx
+++ b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx
@@ -17,9 +17,11 @@ import "./index.css";
const CustomToolTip = ({ active, payload, label }) => {
const uiTimezone = useSelector((state) => state.ui.timezone);
-
const theme = useTheme();
if (active && payload && payload.length) {
+ const responseTime = payload[0]?.payload?.originalAvgResponseTime
+ ? payload[0]?.payload?.originalAvgResponseTime
+ : (payload[0]?.payload?.avgResponseTime ?? 0);
return (
{
Response Time
{" "}
- {payload[0].payload.originalResponseTime}
+ {Math.floor(responseTime)}
{
return null;
};
+CustomToolTip.propTypes = {
+ active: PropTypes.bool,
+ payload: PropTypes.arrayOf(
+ PropTypes.shape({
+ value: PropTypes.number,
+ payload: PropTypes.shape({
+ _id: PropTypes.string,
+ avgResponseTime: PropTypes.number,
+ originalAvgResponseTime: PropTypes.number,
+ }),
+ })
+ ),
+ label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+};
const CustomTick = ({ x, y, payload, index }) => {
const theme = useTheme();
const uiTimezone = useSelector((state) => state.ui.timezone);
-
// Render nothing for the first tick
if (index === 0) return null;
return (
@@ -168,7 +183,7 @@ const MonitorDetailsAreaChart = ({ checks }) => {
}
minTickGap={0}
axisLine={false}
@@ -183,7 +198,7 @@ const MonitorDetailsAreaChart = ({ checks }) => {
/>
{
+ const uiTimezone = useSelector((state) => state.ui.timezone);
+ const dateFormat = type === "day" ? "MMM D, h:mm A" : "MMM D";
+
+ return (
+ <>
+
+ {formatDateWithTz(firstDataPoint._id, dateFormat, uiTimezone)}
+
+
+ {formatDateWithTz(lastDataPoint._id, dateFormat, uiTimezone)}
+
+ >
+ );
+};
+
+CustomLabels.propTypes = {
+ x: PropTypes.number.isRequired,
+ width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ firstDataPoint: PropTypes.object.isRequired,
+ lastDataPoint: PropTypes.object.isRequired,
+ type: PropTypes.string.isRequired,
+};
+
+export default CustomLabels;
diff --git a/Client/src/Pages/Uptime/Details/Charts/DownBarChart.jsx b/Client/src/Pages/Uptime/Details/Charts/DownBarChart.jsx
new file mode 100644
index 000000000..a851a93ea
--- /dev/null
+++ b/Client/src/Pages/Uptime/Details/Charts/DownBarChart.jsx
@@ -0,0 +1,89 @@
+import { memo, useState } from "react";
+import { useTheme } from "@mui/material";
+import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts";
+import PropTypes from "prop-types";
+import CustomLabels from "./CustomLabels";
+
+const DownBarChart = memo(({ stats, type, onBarHover }) => {
+ const theme = useTheme();
+
+ const [chartHovered, setChartHovered] = useState(false);
+ const [hoveredBarIndex, setHoveredBarIndex] = useState(null);
+
+ return (
+
+ {
+ setChartHovered(true);
+ onBarHover({ time: null, totalChecks: 0 });
+ }}
+ onMouseLeave={() => {
+ setChartHovered(false);
+ setHoveredBarIndex(null);
+ onBarHover(null);
+ }}
+ >
+
+ }
+ />
+
+ {stats.downChecks.map((entry, index) => (
+ | {
+ setHoveredBarIndex(index);
+ onBarHover(entry);
+ }}
+ onMouseLeave={() => {
+ setHoveredBarIndex(null);
+ onBarHover({ time: null, totalChecks: 0 });
+ }}
+ />
+ ))}
+ |
+
+
+ );
+});
+
+DownBarChart.displayName = "DownBarChart";
+DownBarChart.propTypes = {
+ stats: PropTypes.shape({
+ downChecks: PropTypes.arrayOf(PropTypes.object),
+ downChecksAggregate: PropTypes.object,
+ }),
+ type: PropTypes.string,
+ onBarHover: PropTypes.func,
+};
+export default DownBarChart;
diff --git a/Client/src/Pages/Uptime/Details/Charts/ResponseGaugeChart.jsx b/Client/src/Pages/Uptime/Details/Charts/ResponseGaugeChart.jsx
new file mode 100644
index 000000000..29e0cc8bb
--- /dev/null
+++ b/Client/src/Pages/Uptime/Details/Charts/ResponseGaugeChart.jsx
@@ -0,0 +1,117 @@
+import PropTypes from "prop-types";
+import { useTheme } from "@mui/material";
+import { ResponsiveContainer, RadialBarChart, RadialBar, Cell } from "recharts";
+
+const ResponseGaugeChart = ({ avgResponseTime }) => {
+ const theme = useTheme();
+
+ let max = 1000; // max ms
+
+ const data = [
+ { response: max, fill: "transparent", background: false },
+ { response: avgResponseTime, background: true },
+ ];
+ let responseTime = Math.floor(avgResponseTime);
+ let responseProps =
+ responseTime <= 200
+ ? {
+ category: "Excellent",
+ main: theme.palette.success.main,
+ bg: theme.palette.success.contrastText,
+ }
+ : responseTime <= 500
+ ? {
+ category: "Fair",
+ main: theme.palette.success.main,
+ bg: theme.palette.success.contrastText,
+ }
+ : responseTime <= 600
+ ? {
+ category: "Acceptable",
+ main: theme.palette.warning.main,
+ bg: theme.palette.warning.dark,
+ }
+ : {
+ category: "Poor",
+ main: theme.palette.error.main,
+ bg: theme.palette.error.light,
+ };
+
+ return (
+
+
+
+ low
+
+
+ high
+
+
+ {responseProps.category}
+
+
+ {responseTime} ms
+
+
+ |
+ |
+
+
+
+ );
+};
+
+ResponseGaugeChart.propTypes = {
+ avgResponseTime: PropTypes.number.isRequired,
+};
+
+export default ResponseGaugeChart;
diff --git a/Client/src/Pages/Uptime/Details/Charts/UpBarChart.jsx b/Client/src/Pages/Uptime/Details/Charts/UpBarChart.jsx
new file mode 100644
index 000000000..08c8d778e
--- /dev/null
+++ b/Client/src/Pages/Uptime/Details/Charts/UpBarChart.jsx
@@ -0,0 +1,100 @@
+import { memo, useState } from "react";
+import { useTheme } from "@mui/material";
+import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts";
+import PropTypes from "prop-types";
+import CustomLabels from "./CustomLabels";
+
+const UpBarChart = memo(({ stats, type, onBarHover }) => {
+ const theme = useTheme();
+ const [chartHovered, setChartHovered] = useState(false);
+ const [hoveredBarIndex, setHoveredBarIndex] = useState(null);
+
+ const getColorRange = (responseTime) => {
+ return responseTime < 200
+ ? { main: theme.palette.success.main, light: theme.palette.success.light }
+ : responseTime < 300
+ ? { main: theme.palette.warning.main, light: theme.palette.warning.light }
+ : { main: theme.palette.error.main, light: theme.palette.error.light };
+ };
+
+ return (
+
+ {
+ setChartHovered(true);
+ onBarHover({ time: null, totalChecks: 0, avgResponseTime: 0 });
+ }}
+ onMouseLeave={() => {
+ setChartHovered(false);
+ setHoveredBarIndex(null);
+ onBarHover(null);
+ }}
+ >
+
+ }
+ />
+
+ {stats.upChecks.map((entry, index) => {
+ let { main, light } = getColorRange(entry.avgResponseTime);
+ return (
+ {
+ setHoveredBarIndex(index);
+ onBarHover(entry);
+ }}
+ onMouseLeave={() => {
+ setHoveredBarIndex(null);
+ onBarHover({
+ time: null,
+ totalChecks: 0,
+ groupUptimePercentage: 0,
+ });
+ }}
+ />
+ );
+ })}
+ |
+
+
+ );
+});
+
+// Add display name for the component
+UpBarChart.displayName = "UpBarChart";
+
+// Validate props using PropTypes
+UpBarChart.propTypes = {
+ stats: PropTypes.shape({
+ upChecks: PropTypes.array,
+ upChecksAggregate: PropTypes.object,
+ }),
+ type: PropTypes.string,
+ onBarHover: PropTypes.func,
+};
+export default UpBarChart;
diff --git a/Client/src/Pages/Uptime/Details/Charts/index.jsx b/Client/src/Pages/Uptime/Details/Charts/index.jsx
deleted file mode 100644
index 7c5adc9e2..000000000
--- a/Client/src/Pages/Uptime/Details/Charts/index.jsx
+++ /dev/null
@@ -1,346 +0,0 @@
-import { useTheme } from "@emotion/react";
-import PropTypes from "prop-types";
-import {
- BarChart,
- Bar,
- XAxis,
- ResponsiveContainer,
- Cell,
- RadialBarChart,
- RadialBar,
-} from "recharts";
-import { memo, useMemo, useState } from "react";
-import { useSelector } from "react-redux";
-import { formatDateWithTz } from "../../../../Utils/timeUtils";
-
-const CustomLabels = ({ x, width, height, firstDataPoint, lastDataPoint, type }) => {
- const uiTimezone = useSelector((state) => state.ui.timezone);
- const dateFormat = type === "day" ? "MMM D, h:mm A" : "MMM D";
-
- return (
- <>
-
- {formatDateWithTz(new Date(firstDataPoint.time), dateFormat, uiTimezone)}
-
-
- {formatDateWithTz(new Date(lastDataPoint.time), dateFormat, uiTimezone)}
-
- >
- );
-};
-
-CustomLabels.propTypes = {
- x: PropTypes.number.isRequired,
- width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
- height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
- firstDataPoint: PropTypes.object.isRequired,
- lastDataPoint: PropTypes.object.isRequired,
- type: PropTypes.string.isRequired,
-};
-
-const UpBarChart = memo(({ data, type, onBarHover }) => {
- const theme = useTheme();
-
- const [chartHovered, setChartHovered] = useState(false);
- const [hoveredBarIndex, setHoveredBarIndex] = useState(null);
-
- const getColorRange = (uptime) => {
- return uptime > 80
- ? { main: theme.palette.success.main, light: theme.palette.success.light }
- : uptime > 50
- ? { main: theme.palette.warning.main, light: theme.palette.warning.light }
- : { main: theme.palette.error.contrastText, light: theme.palette.error.light };
- };
-
- // TODO - REMOVE THIS LATER
- const reversedData = useMemo(() => [...data].reverse(), [data]);
-
- return (
-
- {
- setChartHovered(true);
- onBarHover({ time: null, totalChecks: 0, uptimePercentage: 0 });
- }}
- onMouseLeave={() => {
- setChartHovered(false);
- setHoveredBarIndex(null);
- onBarHover(null);
- }}
- >
-
- }
- />
-
- {reversedData.map((entry, index) => {
- let { main, light } = getColorRange(entry.uptimePercentage);
- return (
- {
- setHoveredBarIndex(index);
- onBarHover(entry);
- }}
- onMouseLeave={() => {
- setHoveredBarIndex(null);
- onBarHover({
- time: null,
- totalChecks: 0,
- uptimePercentage: 0,
- });
- }}
- />
- );
- })}
- |
-
-
- );
-});
-
-// Add display name for the component
-UpBarChart.displayName = "UpBarChart";
-
-// Validate props using PropTypes
-UpBarChart.propTypes = {
- data: PropTypes.arrayOf(PropTypes.object),
- type: PropTypes.string,
- onBarHover: PropTypes.func,
-};
-export { UpBarChart };
-
-const DownBarChart = memo(({ data, type, onBarHover }) => {
- const theme = useTheme();
-
- const [chartHovered, setChartHovered] = useState(false);
- const [hoveredBarIndex, setHoveredBarIndex] = useState(null);
-
- // TODO - REMOVE THIS LATER
- const reversedData = useMemo(() => [...data].reverse(), [data]);
-
- return (
-
- {
- setChartHovered(true);
- onBarHover({ time: null, totalIncidents: 0 });
- }}
- onMouseLeave={() => {
- setChartHovered(false);
- setHoveredBarIndex(null);
- onBarHover(null);
- }}
- >
-
- }
- />
-
- {reversedData.map((entry, index) => (
- {
- setHoveredBarIndex(index);
- onBarHover(entry);
- }}
- onMouseLeave={() => {
- setHoveredBarIndex(null);
- onBarHover({ time: null, totalIncidents: 0 });
- }}
- />
- ))}
- |
-
-
- );
-});
-
-DownBarChart.displayName = "DownBarChart";
-DownBarChart.propTypes = {
- data: PropTypes.arrayOf(PropTypes.object),
- type: PropTypes.string,
- onBarHover: PropTypes.func,
-};
-export { DownBarChart };
-
-const ResponseGaugeChart = ({ data }) => {
- const theme = useTheme();
-
- let max = 1000; // max ms
-
- const memoizedData = useMemo(
- () => [{ response: max, fill: "transparent", background: false }, ...data],
- [data[0].response]
- );
-
- let responseTime = Math.floor(memoizedData[1].response);
- let responseProps =
- responseTime <= 200
- ? {
- category: "Excellent",
- main: theme.palette.success.main,
- bg: theme.palette.success.contrastText,
- }
- : responseTime <= 500
- ? {
- category: "Fair",
- main: theme.palette.success.main,
- bg: theme.palette.success.contrastText,
- }
- : responseTime <= 600
- ? {
- category: "Acceptable",
- main: theme.palette.warning.main,
- bg: theme.palette.warning.dark,
- }
- : {
- category: "Poor",
- main: theme.palette.error.light,
- bg: theme.palette.error.dark,
- };
-
- return (
-
-
-
- low
-
-
- high
-
-
- {responseProps.category}
-
-
- {responseTime} ms
-
-
- |
- |
-
-
-
- );
-};
-
-ResponseGaugeChart.propTypes = {
- data: PropTypes.arrayOf(PropTypes.object).isRequired,
-};
-
-export { ResponseGaugeChart };
diff --git a/Client/src/Pages/Uptime/Details/index.jsx b/Client/src/Pages/Uptime/Details/index.jsx
index 631667d08..7a2aa6756 100644
--- a/Client/src/Pages/Uptime/Details/index.jsx
+++ b/Client/src/Pages/Uptime/Details/index.jsx
@@ -5,7 +5,6 @@ import { useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import { networkService } from "../../../main";
import { logger } from "../../../Utils/Logger";
-import { formatDurationRounded, formatDurationSplit } from "../../../Utils/timeUtils";
import MonitorDetailsAreaChart from "../../../Components/Charts/MonitorDetailsAreaChart";
import ButtonGroup from "@mui/material/ButtonGroup";
import SettingsIcon from "../../../assets/icons/settings-bold.svg?react";
@@ -18,14 +17,16 @@ import PaginationTable from "./PaginationTable";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import PulseDot from "../../../Components/Animated/PulseDot";
import { ChartBox } from "./styled";
-import { DownBarChart, ResponseGaugeChart, UpBarChart } from "./Charts";
import SkeletonLayout from "./skeleton";
import "./index.css";
import useUtils from "../utils";
-import { formatDateWithTz } from "../../../Utils/timeUtils";
+import { formatDateWithTz, formatDurationSplit } from "../../../Utils/timeUtils";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import IconBox from "../../../Components/IconBox";
import StatBox from "../../../Components/StatBox";
+import UpBarChart from "./Charts/UpBarChart";
+import DownBarChart from "./Charts/DownBarChart";
+import ResponseGaugeChart from "./Charts/ResponseGaugeChart";
/**
* Details page component displaying monitor details and related information.
* @component
@@ -47,16 +48,12 @@ const DetailsPage = () => {
const fetchMonitor = useCallback(async () => {
try {
- const res = await networkService.getStatsByMonitorId({
+ const res = await networkService.getUptimeDetailsById({
authToken: authToken,
monitorId: monitorId,
- sortOrder: null,
- limit: null,
dateRange: dateRange,
- numToDisplay: 50,
normalize: true,
});
- console.log(res?.data?.data);
setMonitor(res?.data?.data ?? {});
} catch (error) {
logger.error(error);
@@ -185,7 +182,7 @@ const DetailsPage = () => {
},
}}
>
- Checking every {formatDurationRounded(monitor?.interval)}.
+ {/* Checking every {formatDurationRounded(monitor?.interval)}. */}
@@ -224,17 +221,17 @@ const DetailsPage = () => {
- {monitor?.latestResponseTime}
+ {monitor?.stats?.latestResponseTime}
{"ms"}
>
}
@@ -311,7 +308,7 @@ const DetailsPage = () => {
{hoveredUptimeData !== null
? hoveredUptimeData.totalChecks
- : monitor?.periodTotalChecks}
+ : (monitor.stats?.upChecksAggregate?.totalChecks ?? 0)}
{hoveredUptimeData !== null && hoveredUptimeData.time !== null && (
{
color={theme.palette.text.tertiary}
>
{formatDateWithTz(
- hoveredUptimeData.time,
+ hoveredUptimeData._id,
dateFormat,
uiTimezone
)}
@@ -330,17 +327,27 @@ const DetailsPage = () => {
)}
- Uptime Percentage
+
+ {hoveredUptimeData !== null
+ ? "Avg Response Time"
+ : "Uptime Percentage"}
+
{hoveredUptimeData !== null
- ? Math.floor(hoveredUptimeData.uptimePercentage * 10) / 10
- : Math.floor(monitor?.periodUptime * 10) / 10}
- %
+ ? Math.floor(hoveredUptimeData?.avgResponseTime ?? 0)
+ : Math.floor(
+ ((monitor?.stats?.upChecksAggregate?.totalChecks ?? 0) /
+ (monitor?.stats?.totalChecks ?? 1)) *
+ 100
+ )}
+
+ {hoveredUptimeData !== null ? " ms" : " %"}
+
@@ -356,8 +363,8 @@ const DetailsPage = () => {
Total Incidents
{hoveredIncidentsData !== null
- ? hoveredIncidentsData.totalIncidents
- : monitor?.periodIncidents}
+ ? hoveredIncidentsData.totalChecks
+ : (monitor.stats?.downChecksAggregate?.totalChecks ?? 0)}
{hoveredIncidentsData !== null &&
hoveredIncidentsData.time !== null && (
@@ -369,7 +376,7 @@ const DetailsPage = () => {
color={theme.palette.text.tertiary}
>
{formatDateWithTz(
- hoveredIncidentsData.time,
+ hoveredIncidentsData._id,
dateFormat,
uiTimezone
)}
@@ -377,7 +384,7 @@ const DetailsPage = () => {
)}
@@ -390,7 +397,7 @@ const DetailsPage = () => {
Average Response Time
@@ -403,7 +410,7 @@ const DetailsPage = () => {
Response Times
-
+
{
};
export const formatDateWithTz = (timestamp, format, timezone) => {
- const formattedDate = dayjs(timestamp, timezone).tz(timezone).format(format);
+ const formattedDate = dayjs(timestamp).tz(timezone).format(format);
return formattedDate;
};
diff --git a/README.md b/README.md
index 3b39ad91b..35e3f0fd5 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,9 @@
-**Need support or have a suggestion? Check our [Discord channel](https://discord.gg/NAb6H3UTjK) or [Discussions](https://github.com/bluewave-labs/checkmate/discussions) forum.**
-**Checkmate is on [GitHub trending](https://github.com/trending) and #1 trending for JavaScript apps on Fri Dec 13th & 15th**
+# **We're opening our $5000 grant funding announcement soon, powered by Checkmate and [UpRock](https://uprock.com) - check [our web page](https://checkmate.so) for preliminary details.**
-**If you would like to support us, please consider giving it a ⭐, and think about contributing or providing feedback.**
+**If you would like to support us, please consider giving it a ⭐, and think about contributing or providing feedback. Need support or have a suggestion? Check our [Discord channel](https://discord.gg/NAb6H3UTjK) or [Discussions](https://github.com/bluewave-labs/checkmate/discussions) forum.**
-
+

@@ -53,20 +52,21 @@ If you have any questions, suggestions or comments, please use our [Discord chan
- Infrastructure monitoring (memory, disk usage, CPU performance etc) - requires [Capture](https://github.com/bluewave-labs/capture)
- Docker monitoring
- Ping monitoring
+- SSL monitoring
- Incidents at a glance
- E-mail notifications
- Scheduled maintenance
-- Can monitor 1000+ servers at the same time on a moderate server
**Short term roadmap:**
-- Global (distributed) uptime checking on Solana network
-- Status pages
-- Better notification options (think Discord, Telegram, Slack)
+- Port monitoring (**complete**, waiting to be deployed to stable version)
+- Global (distributed) uptime checking on Solana network (**in progress**)
+- Status pages (**in progress**)
+- Better notification options (Webhooks, Discord, Telegram, Slack)
- More configuration options
+- Translations
+- Tagging/grouping monitors
- DNS monitoring
-- SSL monitoring
-- Port monitoring
## 🏗️ Screenshots
diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js
index e1e37aae0..5e16b3b18 100644
--- a/Server/controllers/monitorController.js
+++ b/Server/controllers/monitorController.js
@@ -76,6 +76,19 @@ class MonitorController {
}
};
+ getUptimeDetailsById = async (req, res, next) => {
+ try {
+ const monitor = await this.db.getUptimeDetailsById(req);
+ return res.status(200).json({
+ success: true,
+ msg: successMessages.MONITOR_GET_BY_ID,
+ data: monitor,
+ });
+ } catch (error) {
+ next(handleError(error, SERVICE_NAME, "getMonitorDetailsById"));
+ }
+ };
+
/**
* Returns monitor stats for monitor with matching ID
* @async
diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js
index 94ebf9352..1143b9eeb 100644
--- a/Server/db/mongo/modules/monitorModule.js
+++ b/Server/db/mongo/modules/monitorModule.js
@@ -4,7 +4,7 @@ import PageSpeedCheck from "../../models/PageSpeedCheck.js";
import HardwareCheck from "../../models/HardwareCheck.js";
import { errorMessages } from "../../../utils/messages.js";
import Notification from "../../models/Notification.js";
-import { NormalizeData } from "../../../utils/dataUtils.js";
+import { NormalizeData, NormalizeDataUptimeDetails } from "../../../utils/dataUtils.js";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
@@ -311,6 +311,329 @@ const calculateGroupStats = (group) => {
};
};
+/**
+ * Get uptime details by monitor ID
+ * @async
+ * @param {Express.Request} req
+ * @param {Express.Response} res
+ * @returns {Promise}
+ * @throws {Error}
+ */
+const getUptimeDetailsById = async (req) => {
+ try {
+ const { monitorId } = req.params;
+ const monitor = await Monitor.findById(monitorId);
+ if (monitor === null || monitor === undefined) {
+ throw new Error(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId));
+ }
+
+ const { dateRange, normalize } = req.query;
+ const dates = getDateRange(dateRange);
+ const formatLookup = {
+ day: "%Y-%m-%dT%H:00:00Z",
+ week: "%Y-%m-%dT%H:00:00Z",
+ month: "%Y-%m-%dT00:00:00Z",
+ };
+
+ const dateString = formatLookup[dateRange];
+ const monitorData = await Check.aggregate([
+ {
+ $match: {
+ monitorId: monitor._id,
+ },
+ },
+ {
+ $sort: {
+ createdAt: 1,
+ },
+ },
+ {
+ $facet: {
+ aggregateData: [
+ {
+ $group: {
+ _id: null,
+ avgResponseTime: {
+ $avg: "$responseTime",
+ },
+ firstCheck: {
+ $first: "$$ROOT",
+ },
+ lastCheck: {
+ $last: "$$ROOT",
+ },
+ totalChecks: {
+ $sum: 1,
+ },
+ },
+ },
+ ],
+ uptimeDuration: [
+ {
+ $match: {
+ status: false,
+ },
+ },
+ {
+ $sort: {
+ createdAt: 1,
+ },
+ },
+ {
+ $group: {
+ _id: null,
+ lastFalseCheck: {
+ $last: "$$ROOT",
+ },
+ },
+ },
+ ],
+ groupChecks: [
+ {
+ $match: {
+ createdAt: { $gte: dates.start, $lte: dates.end },
+ },
+ },
+ {
+ $group: {
+ _id: {
+ $dateToString: {
+ format: dateString,
+ date: "$createdAt",
+ },
+ },
+ avgResponseTime: {
+ $avg: "$responseTime",
+ },
+ totalChecks: {
+ $sum: 1,
+ },
+ },
+ },
+ {
+ $sort: {
+ _id: 1,
+ },
+ },
+ ],
+ groupAggregate: [
+ {
+ $match: {
+ createdAt: { $gte: dates.start, $lte: dates.end },
+ },
+ },
+ {
+ $group: {
+ _id: null,
+ avgResponseTime: {
+ $avg: "$responseTime",
+ },
+ },
+ },
+ ],
+ upChecksAggregate: [
+ {
+ $match: {
+ status: true,
+ },
+ },
+ {
+ $group: {
+ _id: null,
+ avgResponseTime: {
+ $avg: "$responseTime",
+ },
+ totalChecks: {
+ $sum: 1,
+ },
+ },
+ },
+ ],
+ upChecks: [
+ {
+ $match: {
+ status: true,
+ createdAt: { $gte: dates.start, $lte: dates.end },
+ },
+ },
+ {
+ $group: {
+ _id: {
+ $dateToString: {
+ format: dateString,
+ date: "$createdAt",
+ },
+ },
+ totalChecks: {
+ $sum: 1,
+ },
+ avgResponseTime: {
+ $avg: "$responseTime",
+ },
+ },
+ },
+ {
+ $sort: { _id: 1 },
+ },
+ ],
+ downChecksAggregate: [
+ {
+ $match: {
+ status: false,
+ },
+ },
+ {
+ $group: {
+ _id: null,
+ avgResponseTime: {
+ $avg: "$responseTime",
+ },
+ totalChecks: {
+ $sum: 1,
+ },
+ },
+ },
+ ],
+ downChecks: [
+ {
+ $match: {
+ status: false,
+ createdAt: { $gte: dates.start, $lte: dates.end },
+ },
+ },
+ {
+ $group: {
+ _id: {
+ $dateToString: {
+ format: dateString,
+ date: "$createdAt",
+ },
+ },
+ totalChecks: {
+ $sum: 1,
+ },
+ avgResponseTime: {
+ $avg: "$responseTime",
+ },
+ },
+ },
+ {
+ $sort: { _id: 1 },
+ },
+ ],
+ },
+ },
+ {
+ $project: {
+ avgResponseTime: {
+ $arrayElemAt: ["$aggregateData.avgResponseTime", 0],
+ },
+ totalChecks: {
+ $arrayElemAt: ["$aggregateData.totalChecks", 0],
+ },
+ latestResponseTime: {
+ $arrayElemAt: ["$aggregateData.lastCheck.responseTime", 0],
+ },
+ timeSinceLastCheck: {
+ $let: {
+ vars: {
+ lastCheck: {
+ $arrayElemAt: ["$aggregateData.lastCheck", 0],
+ },
+ },
+ in: {
+ $cond: [
+ {
+ $ifNull: ["$$lastCheck", false],
+ },
+ {
+ $subtract: [new Date(), "$$lastCheck.createdAt"],
+ },
+ 0,
+ ],
+ },
+ },
+ },
+ timeSinceLastFalseCheck: {
+ $let: {
+ vars: {
+ lastFalseCheck: {
+ $arrayElemAt: ["$uptimeDuration.lastFalseCheck", 0],
+ },
+ firstCheck: {
+ $arrayElemAt: ["$aggregateData.firstCheck", 0],
+ },
+ },
+ in: {
+ $cond: [
+ {
+ $ifNull: ["$$lastFalseCheck", false],
+ },
+ {
+ $subtract: [new Date(), "$$lastFalseCheck.createdAt"],
+ },
+ {
+ $cond: [
+ {
+ $ifNull: ["$$firstCheck", false],
+ },
+ {
+ $subtract: [new Date(), "$$firstCheck.createdAt"],
+ },
+ 0,
+ ],
+ },
+ ],
+ },
+ },
+ },
+ groupChecks: "$groupChecks",
+ groupAggregate: {
+ $arrayElemAt: ["$groupAggregate", 0],
+ },
+ upChecksAggregate: {
+ $arrayElemAt: ["$upChecksAggregate", 0],
+ },
+ upChecks: "$upChecks",
+ downChecksAggregate: {
+ $arrayElemAt: ["$downChecksAggregate", 0],
+ },
+ downChecks: "$downChecks",
+ },
+ },
+ ]);
+
+ const normalizedGroupChecks = NormalizeDataUptimeDetails(
+ monitorData[0].groupChecks,
+ 10,
+ 100
+ );
+
+ const monitorStats = {
+ ...monitor.toObject(),
+ stats: {
+ avgResponseTime: monitorData[0].avgResponseTime,
+ totalChecks: monitorData[0].totalChecks,
+ timeSinceLastCheck: monitorData[0].timeSinceLastCheck,
+ timeSinceLastFalseCheck: monitorData[0].timeSinceLastFalseCheck,
+ latestResponseTime: monitorData[0].latestResponseTime,
+ groupChecks: normalizedGroupChecks,
+ groupAggregate: monitorData[0].groupAggregate,
+ upChecksAggregate: monitorData[0].upChecksAggregate,
+ upChecks: monitorData[0].upChecks,
+ downChecksAggregate: monitorData[0].downChecksAggregate,
+ downChecks: monitorData[0].downChecks,
+ },
+ };
+
+ return monitorStats;
+ } catch (error) {
+ error.service = SERVICE_NAME;
+ error.method = "getUptimeDetailsById";
+ throw error;
+ }
+};
+
/**
* Get stats by monitor ID
* @async
@@ -939,6 +1262,7 @@ export {
getAllMonitorsWithUptimeStats,
getMonitorStatsById,
getMonitorById,
+ getUptimeDetailsById,
getMonitorsAndSummaryByTeamId,
getMonitorsByTeamId,
createMonitor,
diff --git a/Server/package-lock.json b/Server/package-lock.json
index e9dc276c4..3b7c616d2 100644
--- a/Server/package-lock.json
+++ b/Server/package-lock.json
@@ -11,7 +11,7 @@
"dependencies": {
"axios": "^1.7.2",
"bcrypt": "5.1.1",
- "bullmq": "5.34.5",
+ "bullmq": "5.34.6",
"cors": "^2.8.5",
"dockerode": "4.0.2",
"dotenv": "^16.4.5",
@@ -1541,9 +1541,9 @@
}
},
"node_modules/bullmq": {
- "version": "5.34.5",
- "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.34.5.tgz",
- "integrity": "sha512-MHho9EOhLCTY3ZF+dd0wHv0VlY2FtpBcopMRsvj0kPra4TAwBFh2pik/s4WbX56cIfCE+VzfHIHy4xvqp3g1+Q==",
+ "version": "5.34.6",
+ "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.34.6.tgz",
+ "integrity": "sha512-pRCYyO9RlkQWxdmKlrNnUthyFwurYXRYLVXD1YIx+nCCdhAOiHatD8FDHbsT/w2I31c0NWoMcfZiIGuipiF7Lg==",
"license": "MIT",
"dependencies": {
"cron-parser": "^4.9.0",
diff --git a/Server/package.json b/Server/package.json
index c8460aa03..894aca859 100644
--- a/Server/package.json
+++ b/Server/package.json
@@ -18,7 +18,7 @@
"dependencies": {
"axios": "^1.7.2",
"bcrypt": "5.1.1",
- "bullmq": "5.34.5",
+ "bullmq": "5.34.6",
"cors": "^2.8.5",
"dockerode": "4.0.2",
"dotenv": "^16.4.5",
diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js
index 9decd4339..d47351486 100644
--- a/Server/routes/monitorRoute.js
+++ b/Server/routes/monitorRoute.js
@@ -17,6 +17,10 @@ class MonitorRoutes {
"/hardware/details/:monitorId",
this.monitorController.getHardwareDetailsById
);
+ this.router.get(
+ "/uptime/details/:monitorId",
+ this.monitorController.getUptimeDetailsById
+ );
this.router.get("/certificate/:monitorId", (req, res, next) => {
this.monitorController.getMonitorCertificate(
req,
diff --git a/Server/utils/dataUtils.js b/Server/utils/dataUtils.js
index b0f8a2fd1..fd10a4257 100644
--- a/Server/utils/dataUtils.js
+++ b/Server/utils/dataUtils.js
@@ -8,12 +8,23 @@ const calculatePercentile = (arr, percentile) => {
return sorted[lower].responseTime * (1 - weight) + sorted[upper].responseTime * weight;
};
+const calculatePercentileUptimeDetails = (arr, percentile) => {
+ const sorted = arr.slice().sort((a, b) => a.avgResponseTime - b.avgResponseTime);
+ const index = (percentile / 100) * (sorted.length - 1);
+ const lower = Math.floor(index);
+ const upper = lower + 1;
+ const weight = index % 1;
+ if (upper >= sorted.length) return sorted[lower].avgResponseTime;
+ return (
+ sorted[lower].avgResponseTime * (1 - weight) + sorted[upper].avgResponseTime * weight
+ );
+};
+
const NormalizeData = (checks, rangeMin, rangeMax) => {
if (checks.length > 1) {
// Get the 5th and 95th percentile
const min = calculatePercentile(checks, 0);
const max = calculatePercentile(checks, 95);
-
const normalizedChecks = checks.map((check) => {
const originalResponseTime = check.responseTime;
// Normalize the response time between 1 and 100
@@ -40,5 +51,42 @@ const NormalizeData = (checks, rangeMin, rangeMax) => {
});
}
};
+const NormalizeDataUptimeDetails = (checks, rangeMin, rangeMax) => {
+ if (checks.length > 1) {
+ // Get the 5th and 95th percentile
+ const min = calculatePercentileUptimeDetails(checks, 0);
+ const max = calculatePercentileUptimeDetails(checks, 95);
-export { calculatePercentile, NormalizeData };
+ const normalizedChecks = checks.map((check) => {
+ const originalResponseTime = check.avgResponseTime;
+ // Normalize the response time between 1 and 100
+ let normalizedResponseTime =
+ rangeMin + ((check.avgResponseTime - min) * (rangeMax - rangeMin)) / (max - min);
+
+ // Put a floor on the response times so we don't have extreme outliers
+ // Better visuals
+ normalizedResponseTime = Math.max(
+ rangeMin,
+ Math.min(rangeMax, normalizedResponseTime)
+ );
+ return {
+ ...check,
+ avgResponseTime: normalizedResponseTime,
+ originalAvgResponseTime: originalResponseTime,
+ };
+ });
+
+ return normalizedChecks;
+ } else {
+ return checks.map((check) => {
+ return { ...check, originalResponseTime: check.responseTime };
+ });
+ }
+};
+
+export {
+ calculatePercentile,
+ NormalizeData,
+ calculatePercentileUptimeDetails,
+ NormalizeDataUptimeDetails,
+};