Merge pull request #3201 from bluewave-labs/develop

develop -> demo
This commit is contained in:
Alexander Holliday authored and GitHub committed 2026-01-27 12:52:48 -08:00
commit 37836e56b8
38 files changed
+330 -184

No files matched your search

@@ -1,6 +1,6 @@
import Box from "@mui/material/Box";
import { useTheme } from "@mui/material/styles";
import type { Check } from "@/Types/Check";
import type { CheckSnapshot } from "@/Types/Check";
import { getResponseColor } from "@/Utils/DataUtils";
import { HeatmapResponseTimeTooltip } from "@/Components/v2/common/charts/HeatmapResponseTimeTooltip";
import type { SxProps } from "@mui/material/styles";
@@ -13,7 +13,7 @@ interface PlaceholderCheck {
}
interface HeatmapResponseTimeProps {
checks: Check[];
checks: CheckSnapshot[];
gap?: ResponsiveStyleValue<number | string>;
availabilityCellSx?: SxProps;
responseCellSx?: SxProps;
@@ -34,7 +34,7 @@ export const HeatmapResponseTime = ({
const latestChecks = checks.slice(-25).reverse();
let data: Array<Check | PlaceholderCheck>;
let data: Array<CheckSnapshot | PlaceholderCheck>;
if (latestChecks.length !== 25) {
const placeholders = Array(25 - latestChecks.length).fill({
status: "placeholder" as const,
@@ -2,12 +2,14 @@ import Stack from "@mui/material/Stack";
import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography";
import { formatDateWithTz } from "@/Utils/TimeUtils";
import type { Check } from "@/Types/Check";
import type { CheckSnapshot } from "@/Types/Check";
import { useTheme } from "@mui/material/styles";
import { useSelector } from "react-redux";
import type { RootState } from "@/Types/state";
import { useTranslation } from "react-i18next";
type HeatmapCheck = Check | { status: "placeholder"; responseTime: 0; createdAt: "" };
type HeatmapCheck =
| CheckSnapshot
| { status: "placeholder"; responseTime: 0; createdAt: "" };
export const HeatmapResponseTimeTooltip = ({
children,
@@ -53,9 +55,9 @@ export const HeatmapResponseTimeTooltip = ({
<Typography>
{formatDateWithTz(check?.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone)}
</Typography>
{check?.originalResponseTime && (
{check?.responseTime && (
<Typography>
{t("common.labels.responseTime")}: {check.originalResponseTime.toFixed()} ms
{t("common.labels.responseTime")}: {check.responseTime.toFixed()} ms
</Typography>
)}
@@ -1,11 +1,11 @@
import Box from "@mui/material/Box";
import { useTheme } from "@mui/material/styles";
import type { ResponsiveStyleValue } from "@mui/system";
import type { Check } from "@/Types/Check";
import type { CheckSnapshot } from "@/Types/Check";
import { HeatmapResponseTimeTooltip } from "./HeatmapResponseTimeTooltip";
interface HistogramResponseTimeProps {
checks: Check[];
checks: CheckSnapshot[];
height?: ResponsiveStyleValue<number | string>;
gap?: ResponsiveStyleValue<number | string>;
}
@@ -2,16 +2,16 @@ import Stack from "@mui/material/Stack";
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts";
import { HistogramPageSpeedTooltip } from "@/Components/v2/monitors/charts/HistogramPageSpeedTooltip";
import { useTheme } from "@mui/material/styles";
import type { Check } from "@/Types/Check";
import type { CheckSnapshot } from "@/Types/Check";
import type { MonitorStatus } from "@/Types/Monitor";
import { getStatusColor } from "@/Utils/MonitorUtils";
const processData = (checks: Check[]) => {
const processData = (checks: CheckSnapshot[]) => {
if (checks.length === 0) return [];
const formattedData: { score: number; date: string }[] = [];
const calculateScore = (entry: Check) => {
const calculateScore = (entry: CheckSnapshot) => {
const accessibility = entry?.accessibility || 0;
const bestPractices = entry?.bestPractices || 0;
const performance = entry?.performance || 0;
@@ -32,7 +32,7 @@ export const HistogramPageSpeed = ({
checks,
status,
}: {
checks: Check[];
checks: CheckSnapshot[];
status: MonitorStatus;
}) => {
const theme = useTheme();
@@ -12,7 +12,7 @@ import {
import { HistogramPageSpeedScoresTooltip } from "@/Components/v2/monitors";
import { useTheme } from "@mui/material/styles";
import type { Check } from "@/Types/Check";
import type { CheckSnapshot } from "@/Types/Check";
import type { Palette } from "@mui/material/styles";
type PaletteColorKey = Extract<
keyof Palette,
@@ -52,7 +52,7 @@ export const HistogramPageSpeedDetails = ({
checks,
range,
}: {
checks: Check[];
checks: CheckSnapshot[];
range: string;
}) => {
const theme = useTheme();
@@ -1,6 +1,6 @@
import { BaseChart } from "@/Components/v2/design-elements";
import { FileText } from "lucide-react";
import type { Check, CheckAudits } from "@/Types/Check";
import type { CheckAudits, CheckSnapshot } from "@/Types/Check";
import { Pie, PieChart, ResponsiveContainer, Label } from "recharts";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
@@ -30,7 +30,7 @@ const CenterLabel = ({ viewBox, value }: any) => {
);
};
export const PiePageSpeed = ({ latestCheck }: { latestCheck?: Check }) => {
export const PiePageSpeed = ({ latestCheck }: { latestCheck?: CheckSnapshot }) => {
const { t } = useTranslation();
const theme = useTheme();
const [hoverTitle, setHoverTitle] = useState<string | null>(null);
@@ -4,7 +4,7 @@ import { BarChart3 } from "lucide-react";
import { BaseChart } from "@/Components/v2/design-elements";
import Typography from "@mui/material/Typography";
import type { Check } from "@/Types/Check";
import type { CheckSnapshot } from "@/Types/Check";
import { useTranslation } from "react-i18next";
import { getPageSpeedPalette } from "@/Utils/MonitorUtils";
import { useTheme } from "@mui/material/styles";
@@ -56,7 +56,7 @@ const MetricBox = ({
);
};
export const PiePageSpeedLegend = ({ latestCheck }: { latestCheck?: Check }) => {
export const PiePageSpeedLegend = ({ latestCheck }: { latestCheck?: CheckSnapshot }) => {
const theme = useTheme();
const { t } = useTranslation();
+3 -3
View File
@@ -137,7 +137,7 @@ const useFetchChecksByMonitor = ({
return [checks, checksCount, isLoading, networkError];
};
const useFetchChecksSummaryByTeamId = ({ updateTrigger } = {}) => {
const useFetchChecksSummaryByTeamId = ({ dateRange, updateTrigger } = {}) => {
const [summary, setSummary] = useState(undefined);
const [isLoading, setIsLoading] = useState(false);
const [networkError, setNetworkError] = useState(false);
@@ -147,7 +147,7 @@ const useFetchChecksSummaryByTeamId = ({ updateTrigger } = {}) => {
try {
setIsLoading(true);
const res = await networkService.getChecksAndSummaryByTeamId();
const res = await networkService.getChecksAndSummaryByTeamId({ dateRange });
setSummary(res.data.data);
} catch (error) {
setNetworkError(true);
@@ -158,7 +158,7 @@ const useFetchChecksSummaryByTeamId = ({ updateTrigger } = {}) => {
};
fetchSummary();
}, [updateTrigger]);
}, [dateRange, updateTrigger]);
return [summary, isLoading, networkError];
};
@@ -19,7 +19,7 @@ import { useState } from "react";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
import { useFetchChecksTeam, useFetchChecksByMonitor } from "@/Hooks/checkHooks.js";
import { Button, Typography, useTheme } from "@mui/material";
import { Typography, useTheme } from "@mui/material";
import { lighten } from "@mui/material/styles";
const GetTooltip = (row) => {
+2 -2
View File
@@ -5,7 +5,6 @@ import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
import IncidentTable from "./Components/IncidentTable/index.jsx";
import OptionsHeader from "./Components/OptionsHeader/index.jsx";
import StatusBoxes from "./Components/StatusBoxes/index.jsx";
import { Box, Button } from "@mui/material";
//Utils
import { useTheme } from "@emotion/react";
@@ -26,7 +25,7 @@ const Checks = () => {
// Local state
const [selectedMonitor, setSelectedMonitor] = useState("0");
const [filter, setFilter] = useState(undefined);
const [dateRange, setDateRange] = useState(undefined);
const [dateRange, setDateRange] = useState("hour");
const [monitorLookup, setMonitorLookup] = useState(undefined);
const [updateTrigger, setUpdateTrigger] = useState(false);
@@ -36,6 +35,7 @@ const Checks = () => {
const theme = useTheme();
const [monitors, , isLoading, networkError] = useFetchMonitorsByTeamId({});
const [summary, isLoadingSummary, networkErrorSummary] = useFetchChecksSummaryByTeamId({
dateRange,
updateTrigger,
});
const { monitorId } = useParams();
@@ -92,9 +92,7 @@ const CreateInfrastructureMonitor = () => {
initializeInfrastructureMonitorForUpdate(monitor);
const fetchLastCheck = async () => {
try {
const { stats } = monitor ?? {};
let latestCheck = stats?.aggregateData?.latestCheck;
let disks = latestCheck?.disk || [];
let disks = monitor?.recentChecks?.[0]?.disk || [];
if (disks.length > 0) {
setAvailableDisks(disks);
@@ -19,8 +19,7 @@ const Gauges = ({ isLoading = false, monitor }) => {
return <SkeletonLayout />;
}
const { stats } = monitor ?? {};
let latestCheck = stats?.aggregateData?.latestCheck;
const latestCheck = monitor?.recentChecks?.[0];
const memoryUsagePercent = latestCheck?.memory?.usage_percent ?? 0;
const memoryUsedBytes = latestCheck?.memory?.used_bytes ?? 0;
const memoryTotalBytes = latestCheck?.memory?.total_bytes ?? 0;
@@ -14,8 +14,7 @@ const InfraStatBoxes = ({ shouldRender, monitor }) => {
const { determineState } = useMonitorUtils();
const { t } = useTranslation();
const { stats } = monitor ?? {};
const latestCheck = stats?.aggregateData?.latestCheck;
const latestCheck = monitor?.recentChecks?.[0];
// Get data from latest check
const physicalCores = latestCheck?.cpu?.physical_core ?? 0;
@@ -127,7 +127,7 @@ const InfrastructureDetails = () => {
)}
{tab === "network" && (
<NetworkStats
net={monitor?.stats?.aggregateData?.latestCheck?.net || []}
net={monitor?.recentChecks?.[0]?.net || []}
isLoading={isLoading}
checks={monitor?.stats?.checks}
dateRange={dateRange}
@@ -102,10 +102,10 @@ const MonitorsTable = ({
const data = monitors?.map((monitor) => {
const processor =
((monitor.checks[0]?.cpu?.frequency ?? 0) / 1000).toFixed(2) + " GHz";
const cpu = (monitor?.checks[0]?.cpu.usage_percent ?? 0) * 100;
const mem = (monitor?.checks[0]?.memory.usage_percent ?? 0) * 100;
const disk = (monitor?.checks[0]?.disk[0]?.usage_percent ?? 0) * 100;
((monitor.recentChecks?.[0]?.cpu?.frequency ?? 0) / 1000).toFixed(2) + " GHz";
const cpu = (monitor?.recentChecks?.[0]?.cpu?.usage_percent ?? 0) * 100;
const mem = (monitor?.recentChecks?.[0]?.memory?.usage_percent ?? 0) * 100;
const disk = (monitor?.recentChecks?.[0]?.disk?.[0]?.usage_percent ?? 0) * 100;
const status = determineState(monitor);
const percentageColor =
monitor.uptimePercentage < 0.25
+3 -3
View File
@@ -48,15 +48,15 @@ const PageSpeedDetails = () => {
monitorStats={monitorStats}
/>
<HistogramPageSpeedDetails
checks={monitor?.checks || []}
checks={monitor?.recentChecks || []}
range="day"
/>
<Stack
direction={{ xs: "column", md: "row" }}
gap={theme.spacing(10)}
>
<PiePageSpeed latestCheck={monitor?.checks?.[0]} />
<PiePageSpeedLegend latestCheck={monitor?.checks?.[0]} />
<PiePageSpeed latestCheck={monitor?.recentChecks?.[0]} />
<PiePageSpeedLegend latestCheck={monitor?.recentChecks?.[0]} />
</Stack>
</BasePage>
);
@@ -208,7 +208,7 @@ export const PageSpeedMonitorsTable = ({
return (
<Stack alignItems={"center"}>
<HistogramPageSpeed
checks={row.checks}
checks={row.recentChecks}
status={row.status}
/>
</Stack>
+3 -11
View File
@@ -2,31 +2,23 @@ import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import { HeatmapResponseTime, HistogramResponseTime } from "@/Components/v2/common";
import type { Check } from "@/Types/Check";
import type { CheckSnapshot } from "@/Types/Check";
interface DummyChartProps {
chartType: string;
}
const generateDummyChecks = (): Check[] => {
const checks: Check[] = [];
const generateDummyChecks = (): CheckSnapshot[] => {
const checks: CheckSnapshot[] = [];
for (let i = 0; i < 25; i++) {
const isUp = Math.random() > 0.1;
checks.push({
id: `dummy-${i}`,
metadata: {
monitorId: "dummy-monitor",
teamId: "dummy-team",
type: "http",
},
status: isUp,
statusCode: isUp ? 200 : 500,
responseTime: Math.floor(Math.random() * 80) + 20,
message: "",
ack: false,
expiry: new Date(Date.now() + 86400000).toISOString(),
createdAt: new Date(Date.now() - i * 60000).toISOString(),
updatedAt: new Date(Date.now() - i * 60000).toISOString(),
});
}
return checks;
@@ -199,9 +199,9 @@ export const MonitorTable = ({
content: t("pages.uptime.table.headers.responseTime"),
render: (row) => {
if (chartType === "histogram") {
return <HistogramResponseTime checks={row.checks} />;
return <HistogramResponseTime checks={row.recentChecks} />;
} else {
return <HeatmapResponseTime checks={row.checks} />;
return <HeatmapResponseTime checks={row.recentChecks} />;
}
},
},
+101 -2
View File
@@ -1,7 +1,15 @@
export interface CheckMetadata {
monitorId: string;
teamId: string;
type: string;
type:
| "http"
| "ping"
| "pagespeed"
| "hardware"
| "docker"
| "port"
| "game"
| "unknown";
}
export interface CheckCpuInfo {
@@ -83,6 +91,7 @@ export interface CheckTimings {
lookup?: number;
connect?: number;
secureConnect?: number;
upload?: number;
response?: number;
end?: number;
abort?: number;
@@ -103,7 +112,6 @@ export interface Check {
id: string;
metadata: CheckMetadata;
status: boolean;
originalResponseTime?: number;
responseTime: number;
timings?: CheckTimings;
statusCode: number;
@@ -123,6 +131,7 @@ export interface Check {
seo?: number;
performance?: number;
audits?: CheckAudits;
__v: number;
createdAt: string;
updatedAt: string;
}
@@ -144,3 +153,93 @@ export interface ChecksResponse {
checks: Check[];
checksCount: number;
}
export type MonitorType =
| "http"
| "ping"
| "pagespeed"
| "hardware"
| "docker"
| "port"
| "game"
| "unknown";
export interface ChecksQueryResult {
checksCount: number;
checks: Check[];
}
export interface PageSpeedChecksResult {
monitorType: "pagespeed";
checks: Check[];
}
export interface HardwareChecksResult {
monitorType: "hardware";
aggregateData: {
totalChecks: number;
};
upChecks: {
totalChecks: number;
};
checks: Array<{
_id: string;
avgCpuUsage: number;
avgMemoryUsage: number;
avgTemperature: number[];
disks: Array<{
name: string;
readSpeed: number;
writeSpeed: number;
totalBytes: number;
freeBytes: number;
usagePercent: number;
}>;
net: Array<{
name: string;
bytesSentPerSecond: number;
deltaBytesRecv: number;
deltaPacketsSent: number;
deltaPacketsRecv: number;
deltaErrIn: number;
deltaErrOut: number;
deltaDropIn: number;
deltaDropOut: number;
deltaFifoIn: number;
deltaFifoOut: number;
}>;
}>;
}
export interface UptimeChecksResult {
monitorType: Exclude<MonitorType, "hardware" | "pagespeed">;
groupedChecks: GroupedCheck[];
groupedUpChecks: GroupedCheck[];
groupedDownChecks: GroupedCheck[];
uptimePercentage: number;
avgResponseTime: number;
}
export interface ChecksSummary {
totalChecks: number;
resolvedChecks: number;
downChecks: number;
cannotResolveChecks: number;
}
export type CheckSnapshot = Omit<
Check,
"metadata" | "ack" | "ackAt" | "expiry" | "__v" | "updatedAt"
>;
export interface HasResponseTime {
responseTime: number;
}
export type NormalizedCheck<T extends HasResponseTime = Check> = T & {
originalResponseTime: number;
};
export type NormalizedUptimeCheck<T extends GroupedCheck = GroupedCheck> = T & {
originalAvgResponseTime: number;
};
+45 -19
View File
@@ -1,4 +1,5 @@
import type { Check, GroupedCheck } from "@/Types/Check";
import type { GroupedCheck } from "@/Types/Check";
import type { CheckSnapshot } from "@/Types/Check";
export type MonitorStatus = boolean | undefined;
export const MonitorTypes = [
@@ -13,28 +14,53 @@ export const MonitorTypes = [
] as const;
export type MonitorType = (typeof MonitorTypes)[number];
export interface Monitor {
checks: Check[];
createdAt: string;
createdBy: string;
interval: number;
isActive: boolean;
latestChecks: Check[];
n: number;
name: string;
status: MonitorStatus;
type: string;
updatedAt: string;
updatedBy: string;
url: string;
id: string;
export interface MonitorThresholds {
usage_cpu?: number;
usage_memory?: number;
usage_disk?: number;
usage_temperature?: number;
}
export interface MonitorWithChecks extends Monitor {
checks: Check[];
export type MonitorMatchMethod = "equal" | "include" | "regex" | "";
export interface Monitor {
id: string;
userId: string;
teamId: string;
name: string;
description?: string;
status?: boolean;
statusWindow: boolean[];
statusWindowSize: number;
statusWindowThreshold: number;
type: MonitorType;
ignoreTlsErrors: boolean;
jsonPath?: string;
expectedValue?: string;
matchMethod?: MonitorMatchMethod;
url: string;
port?: number;
isActive: boolean;
interval: number;
uptimePercentage?: number;
notifications: string[];
secret?: string;
thresholds?: MonitorThresholds;
alertThreshold: number;
cpuAlertThreshold: number;
memoryAlertThreshold: number;
diskAlertThreshold: number;
tempAlertThreshold: number;
selectedDisks: string[];
gameId?: string;
group: string | null;
recentChecks: CheckSnapshot[];
createdAt: string;
updatedAt: string;
}
export type MonitorWithChecks = Monitor;
export interface MonitorsSummary {
totalMonitors: number;
upMonitors: number;
@@ -45,7 +71,7 @@ export interface MonitorsSummary {
export interface MonitorsWithChecksResponse {
count: number;
monitors: MonitorWithChecks[];
summary: MonitorsSummary;
summary: MonitorsSummary | null;
}
export interface MonitorStats {
+2 -2
View File
@@ -594,8 +594,8 @@ class NetworkService {
* @async
* @returns {Promise<AxiosResponse>} The response from the axios GET request.
*/
getChecksAndSummaryByTeamId = async () => {
return this.axiosInstance.get(`/checks/team/summary`);
getChecksAndSummaryByTeamId = async ({ dateRange }) => {
return this.axiosInstance.get(`/checks/team/summary?dateRange=${dateRange}`);
};
/**
+2 -2
View File
@@ -54,8 +54,8 @@ async function ensureMonitor({ monitorId, teamId, userId, type }) {
async function run() {
const mongoUri = process.env.MONGO_URI ?? "mongodb://localhost:27017/uptime_db";
const monitorId = parseObjectId(process.env.MONITOR_ID ?? DEFAULT_MONITOR_ID, DEFAULT_MONITOR_ID);
const teamId = parseObjectId("69648b0578209af45f9ffe30");
const userId = parseObjectId("69648b0678209af45f9ffe32");
const teamId = parseObjectId("6971546de1b2bc3de6498e6e");
const userId = parseObjectId("6971546de1b2bc3de6498e70");
const monitorType = process.env.MONITOR_TYPE ?? DEFAULT_MONITOR_TYPE;
const total = Number(process.env.CHECK_TOTAL ?? DEFAULT_TOTAL);
const batchSize = Number(process.env.CHECK_BATCH_SIZE ?? DEFAULT_BATCH_SIZE);
+1 -1
View File
@@ -176,7 +176,7 @@ export const initializeServices = async ({
const bufferService = new BufferService({ logger, checkService, settingsService });
const statusService = new StatusService({ db, logger, buffer: bufferService, monitorsRepository });
const statusService = new StatusService({ logger, buffer: bufferService, monitorsRepository });
const webhookProvider = new WebhookProvider(logger);
const slackProvider = new SlackProvider(logger);
+2 -1
View File
@@ -66,7 +66,8 @@ class CheckController {
getChecksSummaryByTeamId = async (req: Request, res: Response, next: NextFunction) => {
try {
const summary = await this.checkService.getChecksSummaryByTeamId({ teamId: req?.user?.teamId });
const dateRange = req.query?.dateRange || "hour";
const summary = await this.checkService.getChecksSummaryByTeamId({ teamId: req?.user?.teamId, dateRange });
return res.status(200).json({
success: true,
msg: "Checks summary retrieved successfully",
@@ -324,7 +324,6 @@ class MonitorController {
try {
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
await getMonitorsWithChecksQueryValidation.validateAsync(req.query);
const explain = optionalBoolean(req?.query?.explain, "explain");
const limit = optionalNumber(req?.query?.limit, "limit");
const page = optionalNumber(req?.query?.page, "page");
@@ -7,7 +7,6 @@ import { IStatusPageService } from "@/service/business/statusPageService.js";
import { IMonitorsRepository } from "@/repositories/index.js";
import { ISettingsService } from "@/service/system/settingsService.js";
import { ParseBoolean } from "@/utils/utils.js";
import { MonitorWithChecks } from "@/types/index.js";
import { NormalizeData } from "@/utils/dataUtils.js";
const SERVICE_NAME = "statusPageController";
@@ -84,7 +83,7 @@ class StatusPageController {
const monitors = await this.monitorsRepository.findByIdsWithChecks(statusPage.monitors);
const normalizedMonitors = monitors.map((monitor) => {
const normalizedChecks = NormalizeData(monitor.checks, 10, 100);
const normalizedChecks = NormalizeData(monitor.recentChecks, 10, 100);
if (!showURL) {
const { url, port, secret, notifications, ...rest } = monitor;
return { ...rest, checks: normalizedChecks };
+19 -3
View File
@@ -1,15 +1,18 @@
import { Schema, model, Types, type UpdateQuery } from "mongoose";
import type { Monitor, MonitorMatchMethod, MonitorThresholds } from "@/types/monitor.js";
import type { Monitor, MonitorMatchMethod, MonitorThresholds, CheckSnapshot } from "@/types/monitor.js";
import { MonitorTypes } from "@/types/monitor.js";
import Check from "./Check.js";
import MonitorStats from "./MonitorStats.js";
import StatusPage from "./StatusPage.js";
type CheckSnapshotDocument = Omit<CheckSnapshot, "createdAt"> & { createdAt: Date };
type MonitorDocumentBase = Omit<
Monitor,
"id" | "userId" | "teamId" | "notifications" | "selectedDisks" | "statusWindow" | "createdAt" | "updatedAt"
"id" | "userId" | "teamId" | "notifications" | "selectedDisks" | "statusWindow" | "recentChecks" | "createdAt" | "updatedAt"
> & {
statusWindow: boolean[];
recentChecks: CheckSnapshotDocument[];
notifications: Types.ObjectId[];
selectedDisks: string[];
matchMethod?: MonitorMatchMethod;
@@ -34,6 +37,15 @@ const thresholdsSchema = new Schema<MonitorThresholds>(
{ _id: false }
);
const checkSnapshotSchema = new Schema<CheckSnapshotDocument>(
{
id: { type: String, required: true },
status: { type: Boolean, required: true },
createdAt: { type: Date, required: true },
},
{ _id: false, strict: false }
);
const MonitorSchema = new Schema<MonitorDocument>(
{
userId: {
@@ -165,6 +177,10 @@ const MonitorSchema = new Schema<MonitorDocument>(
return value && value.trim() ? value.trim() : null;
},
},
recentChecks: {
type: [checkSnapshotSchema],
default: [],
},
},
{
timestamps: true,
@@ -202,6 +218,6 @@ MonitorSchema.index({ teamId: 1, type: 1 });
const MonitorModel = model<MonitorDocument>("Monitor", MonitorSchema);
export type { MonitorDocument };
export type { MonitorDocument, CheckSnapshotDocument };
export { MonitorModel };
export default MonitorModel;
@@ -33,7 +33,7 @@ export interface IChecksRepository {
dateString: string,
options?: { type?: MonitorType }
): Promise<UptimeChecksResult | HardwareChecksResult | PageSpeedChecksResult>;
findSummaryByTeamId(teamId: string): Promise<ChecksSummary>;
findSummaryByTeamId(teamId: string, dateRange: string): Promise<ChecksSummary>;
// update
//delete
deleteByMonitorId(monitorId: string): Promise<number>;
@@ -29,7 +29,6 @@ const dateRangeLookup: Record<string, Date | undefined> = {
export type LatestChecksMap = Record<string, Check[]>;
type DateRange = { start: Date; end: Date };
type HardwareAggregateData = { latestCheck: CheckDocument | null; totalChecks: number };
type HardwareUpChecks = { totalChecks: number };
class MongoChecksRepository implements IChecksRepository {
@@ -366,51 +365,30 @@ class MongoChecksRepository implements IChecksRepository {
return this.findUptimeDateRangeChecks(options?.type ?? "http", monitorObjectId, startDate, endDate, dateString);
};
findSummaryByTeamId = async (teamId: string) => {
findSummaryByTeamId = async (teamId: string, dateRange: string) => {
const matchStage = {
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
status: false,
...(dateRangeLookup[dateRange] && {
createdAt: {
$gte: dateRangeLookup[dateRange],
},
}),
};
const checks = await CheckModel.aggregate([
{ $match: matchStage },
{
$facet: {
summary: [
{
$group: {
_id: null,
totalChecks: { $sum: { $cond: [{ $eq: ["$status", false] }, 1, 0] } },
resolvedChecks: {
$sum: {
$cond: [{ $and: [{ $eq: ["$ack", true] }, { $eq: ["$status", false] }] }, 1, 0],
},
},
downChecks: {
$sum: {
$cond: [{ $and: [{ $eq: ["$ack", false] }, { $eq: ["$status", false] }] }, 1, 0],
},
},
cannotResolveChecks: {
$sum: {
$cond: [{ $eq: ["$statusCode", 5000] }, 1, 0],
},
},
},
},
{
$project: {
_id: 0,
},
},
],
},
},
{
$project: {
summary: { $arrayElemAt: ["$summary", 0] },
$group: {
_id: null,
totalChecks: { $sum: 1 },
resolvedChecks: { $sum: { $cond: [{ $eq: ["$ack", true] }, 1, 0] } },
downChecks: { $sum: { $cond: [{ $eq: ["$ack", false] }, 1, 0] } },
cannotResolveChecks: { $sum: { $cond: [{ $eq: ["$statusCode", 5000] }, 1, 0] } },
},
},
{ $project: { _id: 0 } },
]);
return checks[0].summary;
return checks[0] ?? { totalChecks: 0, resolvedChecks: 0, downChecks: 0, cannotResolveChecks: 0 };
};
deleteByMonitorId = async (monitorId: string): Promise<number> => {
@@ -526,14 +504,13 @@ class MongoChecksRepository implements IChecksRepository {
const monitorId = monitorObjectId.toHexString();
const dates = { start: startDate, end: endDate };
const [aggregateDataDoc, upChecksDoc, hardwareMetrics] = await Promise.all([
this.getHardwareAggregateData(monitorId, dates),
this.getHardwareTotalChecks(monitorId, dates),
this.getHardwareUpChecks(monitorId, dates),
this.getHardwareStats(monitorId, dates, dateString),
]);
const aggregateData = {
latestCheck: aggregateDataDoc?.latestCheck ? this.toEntity(aggregateDataDoc.latestCheck as CheckDocument) : null,
totalChecks: aggregateDataDoc?.totalChecks ?? 0,
totalChecks: aggregateDataDoc ?? 0,
};
const upChecks = {
@@ -589,25 +566,12 @@ class MongoChecksRepository implements IChecksRepository {
};
};
private getHardwareAggregateData = async (monitorId: string, dates: DateRange): Promise<HardwareAggregateData> => {
const result = await CheckModel.aggregate([
{
$match: {
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
"metadata.type": "hardware",
createdAt: { $gte: dates.start, $lte: dates.end },
},
},
{ $sort: { createdAt: -1 } },
{
$group: {
_id: null,
latestCheck: { $first: "$$ROOT" },
totalChecks: { $sum: 1 },
},
},
]);
return result[0] || { totalChecks: 0, latestCheck: null };
private getHardwareTotalChecks = async (monitorId: string, dates: DateRange): Promise<number> => {
return await CheckModel.countDocuments({
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
"metadata.type": "hardware",
createdAt: { $gte: dates.start, $lte: dates.end },
});
};
private getHardwareUpChecks = async (monitorId: string, dates: DateRange): Promise<HardwareUpChecks> => {
@@ -1,4 +1,4 @@
import { type MonitorType, type Monitor, type MonitorsSummary, type MonitorWithChecks } from "@/types/index.js";
import { type MonitorType, type Monitor, type MonitorsSummary } from "@/types/index.js";
export interface TeamQueryConfig {
limit?: number;
@@ -25,7 +25,7 @@ export interface IMonitorsRepository {
findAll(): Promise<Monitor[] | null>;
findByTeamId(teamId: string, config: TeamQueryConfig): Promise<Monitor[] | null>;
findByIds(monitorIds: string[]): Promise<Monitor[]>;
findByIdsWithChecks(monitorIds: string[], checksCount?: number): Promise<MonitorWithChecks[]>;
findByIdsWithChecks(monitorIds: string[], checksCount?: number): Promise<Monitor[]>;
// update
updateById(monitorId: string, teamId: string, updates: Partial<Monitor>): Promise<Monitor>;
@@ -1,6 +1,6 @@
import { MonitorModel } from "@/db/models/index.js";
import type { MonitorDocument } from "@/db/models/index.js";
import type { Monitor, MonitorsSummary, MonitorWithChecks, Check } from "@/types/index.js";
import type { MonitorDocument, CheckSnapshotDocument } from "@/db/models/index.js";
import type { Monitor, MonitorsSummary, CheckSnapshot, Check } from "@/types/index.js";
import mongoose, { type FilterQuery, type PipelineStage } from "mongoose";
import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js";
import { AppError } from "@/utils/AppError.js";
@@ -79,7 +79,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
return this.mapDocuments(monitors);
};
findByIdsWithChecks = async (monitorIds: string[], checksCount: number = 25): Promise<MonitorWithChecks[]> => {
findByIdsWithChecks = async (monitorIds: string[], checksCount: number = 25): Promise<Monitor[]> => {
if (!monitorIds.length) {
return [];
}
@@ -309,12 +309,13 @@ class MongoMonitorsRepository implements IMonitorsRepository {
selectedDisks: doc.selectedDisks ?? [],
gameId: doc.gameId ?? undefined,
group: doc.group ?? null,
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
createdAt: toDateString(doc.createdAt),
updatedAt: toDateString(doc.updatedAt),
};
};
private toEntityWithChecks = (doc: any): MonitorWithChecks => {
private toEntityWithChecks = (doc: any): Monitor => {
const toStringId = (value: unknown): string => {
if (value instanceof mongoose.Types.ObjectId) {
return value.toString();
@@ -392,9 +393,37 @@ class MongoMonitorsRepository implements IMonitorsRepository {
selectedDisks: doc.selectedDisks ?? [],
gameId: doc.gameId ?? undefined,
group: doc.group ?? null,
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
createdAt: toDateString(doc.createdAt),
updatedAt: toDateString(doc.updatedAt),
checks,
};
};
private toCheckSnapshot = (doc: CheckSnapshotDocument): CheckSnapshot => {
const toDateString = (value: Date | string): string => {
return value instanceof Date ? value.toISOString() : value;
};
return {
id: doc.id,
status: doc.status,
responseTime: doc.responseTime,
timings: doc.timings,
statusCode: doc.statusCode,
message: doc.message,
cpu: doc.cpu,
memory: doc.memory,
disk: doc.disk,
host: doc.host,
errors: doc.errors,
capture: doc.capture,
net: doc.net,
accessibility: doc.accessibility,
bestPractices: doc.bestPractices,
seo: doc.seo,
performance: doc.performance,
audits: doc.audits,
createdAt: toDateString(doc.createdAt),
};
};
}
+5 -2
View File
@@ -1,4 +1,5 @@
import { requireTeamId } from "@/controllers/controllerUtils.js";
import { Types } from "mongoose";
import { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js";
import type { MonitorType, MonitorStatusResponse, CheckErrorInfo, Check } from "@/types/index.js";
import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js";
@@ -39,6 +40,8 @@ class CheckService {
const { monitorId, teamId, type, status, responseTime, code, message, payload, timings } = statusResponse;
const check: Partial<Check> = {
id: new Types.ObjectId().toString(),
createdAt: new Date().toISOString(),
metadata: {
monitorId,
teamId,
@@ -142,11 +145,11 @@ class CheckService {
return checkData;
};
getChecksSummaryByTeamId = async ({ teamId }: { teamId: string }) => {
getChecksSummaryByTeamId = async ({ teamId, dateRange }: { teamId: string; dateRange: string }) => {
if (!teamId) {
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getChecksSummaryByTeamId", status: 400 });
}
const summary = await this.checksRepository.findSummaryByTeamId(teamId);
const summary = await this.checksRepository.findSummaryByTeamId(teamId, dateRange);
return summary;
};
+3 -12
View File
@@ -360,22 +360,13 @@ export class MonitorService implements IMonitorService {
const snapshotOnlyRequest =
requestedTypes.length > 0 && requestedTypes.every((requestedType) => snapshotTypes.includes(requestedType as MonitorType));
const limitPerMonitor = snapshotOnlyRequest ? 1 : 25;
const checksMap = await this.checksRepository.findLatestByMonitorIds(
monitorsList.map((monitor) => monitor.id),
{ limitPerMonitor }
);
const monitorsWithChecks = monitorsList.map((monitor: Monitor) => {
const rawChecks = checksMap[monitor.id] ?? [];
const rawChecks = monitor.recentChecks ?? [];
const isSnapshotType = snapshotOnlyRequest || snapshotTypes.includes(monitor.type);
const checks = isSnapshotType ? rawChecks.slice(0, 1) : NormalizeData(rawChecks, 10, 100);
return {
...monitor,
checks,
};
monitor.recentChecks = checks;
return monitor;
});
return { summary: summary ?? null, count, monitors: monitorsWithChecks };
};
@@ -9,6 +9,7 @@ import type {
Check,
HardwareStatusPayload,
PageSpeedStatusPayload,
CheckSnapshot,
} from "@/types/index.js";
const SERVICE_NAME = "StatusService";
@@ -24,13 +25,11 @@ export interface IStatusService {
export class StatusService implements IStatusService {
static SERVICE_NAME = SERVICE_NAME;
private db: any;
private logger: any;
private buffer: any;
private monitorsRepository: IMonitorsRepository;
constructor({ db, logger, buffer, monitorsRepository }: { db: any; logger: any; buffer: any; monitorsRepository: IMonitorsRepository }) {
this.db = db;
constructor({ logger, buffer, monitorsRepository }: { logger: any; buffer: any; monitorsRepository: IMonitorsRepository }) {
this.logger = logger;
this.buffer = buffer;
this.monitorsRepository = monitorsRepository;
@@ -175,14 +174,40 @@ export class StatusService implements IStatusService {
// Update running stats
this.updateRunningStats({ monitor, networkResponse: statusResponse });
// If the status window size has changed, empty
monitor.statusWindow = monitor.statusWindow || [];
monitor.statusWindow.push(status);
while (monitor.statusWindow.length > monitor.statusWindowSize) {
monitor.statusWindow.shift();
}
const checkSnapshot: CheckSnapshot = {
id: check.id,
status: check.status,
responseTime: check.responseTime,
timings: check.timings,
statusCode: check.statusCode,
message: check.message,
cpu: check.cpu,
memory: check.memory,
disk: check.disk,
host: check.host,
errors: check.errors,
capture: check.capture,
net: check.net,
accessibility: check.accessibility,
bestPractices: check.bestPractices,
seo: check.seo,
performance: check.performance,
audits: check.audits,
createdAt: check.createdAt,
};
monitor.recentChecks = monitor.recentChecks || [];
monitor.recentChecks.push(checkSnapshot);
const maxRecentChecks = 25;
while (monitor.recentChecks.length > maxRecentChecks) {
monitor.recentChecks.shift();
}
if (monitor.status === undefined || monitor.status === null) {
monitor.status = status;
}
+7 -2
View File
@@ -121,7 +121,6 @@ export interface PageSpeedChecksResult {
export interface HardwareChecksResult {
monitorType: "hardware";
aggregateData: {
latestCheck: Check | null;
totalChecks: number;
};
upChecks: {
@@ -178,10 +177,16 @@ export interface ChecksSummary {
cannotResolveChecks: number;
}
export type NormalizedCheck<T extends Check = Check> = T & {
export interface HasResponseTime {
responseTime: number;
}
export type NormalizedCheck<T extends HasResponseTime = Check> = T & {
originalResponseTime: number;
};
export type NormalizedUptimeCheck<T extends GroupedCheck = GroupedCheck> = T & {
originalAvgResponseTime: number;
};
export type CheckSnapshot = Omit<Check, "metadata" | "ack" | "ackAt" | "expiry" | "__v" | "updatedAt">;
+5 -6
View File
@@ -1,4 +1,7 @@
import type { Check } from "@/types/check.js";
import type { CheckSnapshot } from "@/types/check.js";
export type { CheckSnapshot } from "@/types/check.js";
export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "unknown"] as const;
export type MonitorType = (typeof MonitorTypes)[number];
@@ -42,6 +45,7 @@ export interface Monitor {
selectedDisks: string[];
gameId?: string;
group: string | null;
recentChecks: CheckSnapshot[];
createdAt: string;
updatedAt: string;
}
@@ -53,14 +57,10 @@ export interface MonitorsSummary {
pausedMonitors: number;
}
export interface MonitorWithChecks extends Monitor {
checks: Check[];
}
export interface MonitorsWithChecksByTeamIdResult {
summary: MonitorsSummary | null;
count: number;
monitors: MonitorWithChecks[];
monitors: Monitor[];
}
export interface UptimeDetailsResult {
@@ -78,7 +78,6 @@ export interface UptimeDetailsResult {
export interface HardwareDetailsResult extends Monitor {
stats: {
aggregateData: {
latestCheck: import("./check.js").Check | null;
totalChecks: number;
};
upChecks: {
+3 -3
View File
@@ -1,6 +1,6 @@
import type { Check, GroupedCheck, NormalizedCheck, NormalizedUptimeCheck } from "@/types/index.js";
import type { GroupedCheck, NormalizedCheck, NormalizedUptimeCheck, HasResponseTime } from "@/types/index.js";
const calculatePercentile = (arr: Check[], percentile: number): number => {
const calculatePercentile = <T extends HasResponseTime>(arr: T[], percentile: number): number => {
const sorted = arr.slice().sort((a, b) => a.responseTime - b.responseTime);
const index = (percentile / 100) * (sorted.length - 1);
const lower = Math.floor(index);
@@ -20,7 +20,7 @@ const calculatePercentileUptimeDetails = (arr: GroupedCheck[], percentile: numbe
return sorted[lower]!.avgResponseTime * (1 - weight) + sorted[upper]!.avgResponseTime * weight;
};
export const NormalizeData = <T extends Check>(checks: T[], rangeMin: number, rangeMax: number): NormalizedCheck<T>[] => {
export const NormalizeData = <T extends HasResponseTime>(checks: T[], rangeMin: number, rangeMax: number): NormalizedCheck<T>[] => {
if (checks.length > 1) {
const min = calculatePercentile(checks, 0);
const max = calculatePercentile(checks, 95);