pagespeed details

This commit is contained in:
Alex Holliday
2026-01-14 22:44:23 +00:00
parent 940247b7bf
commit d4af86f26c
9 changed files with 354 additions and 186 deletions
+46 -46
View File
@@ -6,7 +6,7 @@ import { useMonitorUtils } from "./useMonitorUtils.js";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
const useFetchMonitorsWithSummary = ({ types, monitorUpdateTrigger }) => {
export const useFetchMonitorsWithSummary = ({ types, monitorUpdateTrigger }) => {
const [isLoading, setIsLoading] = useState(false);
const [monitors, setMonitors] = useState(undefined);
const [monitorsSummary, setMonitorsSummary] = useState(undefined);
@@ -37,7 +37,7 @@ const useFetchMonitorsWithSummary = ({ types, monitorUpdateTrigger }) => {
return [monitors, monitorsSummary, isLoading, networkError];
};
const useFetchMonitorsWithChecks = ({
export const useFetchMonitorsWithChecks = ({
types,
limit,
page,
@@ -100,7 +100,7 @@ const useFetchMonitorsWithChecks = ({
return [monitors, count, isLoading, networkError];
};
const useFetchMonitorsByTeamId = ({ types, filter, updateTrigger }) => {
export const useFetchMonitorsByTeamId = ({ types, filter, updateTrigger }) => {
const [isLoading, setIsLoading] = useState(false);
const [monitors, setMonitors] = useState(undefined);
const [networkError, setNetworkError] = useState(false);
@@ -126,15 +126,11 @@ const useFetchMonitorsByTeamId = ({ types, filter, updateTrigger }) => {
}
};
fetchMonitors();
}, [
types,
filter,
updateTrigger,
]);
}, [types, filter, updateTrigger]);
return [monitors, isLoading, networkError];
};
const useFetchStatsByMonitorId = ({
export const useFetchStatsByMonitorId = ({
monitorId,
sortOrder,
limit,
@@ -173,7 +169,7 @@ const useFetchStatsByMonitorId = ({
return [monitor, audits, isLoading, networkError];
};
const useFetchMonitorGames = ({ setGames, updateTrigger }) => {
export const useFetchMonitorGames = ({ setGames, updateTrigger }) => {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchGames = async () => {
@@ -192,7 +188,7 @@ const useFetchMonitorGames = ({ setGames, updateTrigger }) => {
return [isLoading];
};
const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => {
export const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
if (typeof monitorId === "undefined") {
@@ -215,7 +211,7 @@ const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => {
return [isLoading];
};
const useFetchHardwareMonitorById = ({ monitorId, dateRange, updateTrigger }) => {
export const useFetchHardwareMonitorById = ({ monitorId, dateRange, updateTrigger }) => {
const [isLoading, setIsLoading] = useState(true);
const [networkError, setNetworkError] = useState(false);
const [monitor, setMonitor] = useState(undefined);
@@ -241,8 +237,34 @@ const useFetchHardwareMonitorById = ({ monitorId, dateRange, updateTrigger }) =>
}, [monitorId, dateRange, updateTrigger]);
return [monitor, isLoading, networkError];
};
export const useFetchPageSpeedMonitorById = ({ monitorId, dateRange, updateTrigger }) => {
const [isLoading, setIsLoading] = useState(true);
const [networkError, setNetworkError] = useState(false);
const [monitor, setMonitor] = useState(undefined);
const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) => {
useEffect(() => {
const fetchMonitor = async () => {
try {
if (!monitorId) {
return { monitor: undefined, isLoading: false, networkError: undefined };
}
const response = await networkService.getPageSpeedDetailsByMonitorId({
monitorId: monitorId,
dateRange: dateRange,
});
setMonitor(response.data.data);
} catch (error) {
setNetworkError(true);
} finally {
setIsLoading(false);
}
};
fetchMonitor();
}, [monitorId, dateRange, updateTrigger]);
return [monitor, isLoading, networkError];
};
export const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) => {
const [networkError, setNetworkError] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [monitor, setMonitor] = useState(undefined);
@@ -270,7 +292,7 @@ const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) => {
return [monitor, monitorStats, isLoading, networkError];
};
const useCreateMonitor = () => {
export const useCreateMonitor = () => {
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
const createMonitor = async ({ monitor, redirect }) => {
@@ -290,7 +312,7 @@ const useCreateMonitor = () => {
return [createMonitor, isLoading];
};
const useFetchGlobalSettings = () => {
export const useFetchGlobalSettings = () => {
const [isLoading, setIsLoading] = useState(true);
const [globalSettings, setGlobalSettings] = useState(undefined);
useEffect(() => {
@@ -312,7 +334,7 @@ const useFetchGlobalSettings = () => {
return [globalSettings, isLoading];
};
const useDeleteMonitor = () => {
export const useDeleteMonitor = () => {
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
const deleteMonitor = async ({ monitor, redirect }) => {
@@ -333,7 +355,7 @@ const useDeleteMonitor = () => {
return [deleteMonitor, isLoading];
};
const useUpdateMonitor = () => {
export const useUpdateMonitor = () => {
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
const updateMonitor = async ({ monitor, redirect }) => {
@@ -380,7 +402,7 @@ const useUpdateMonitor = () => {
return [updateMonitor, isLoading];
};
const usePauseMonitor = () => {
export const usePauseMonitor = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
const pauseMonitor = async ({ monitorId, triggerUpdate }) => {
@@ -403,7 +425,7 @@ const usePauseMonitor = () => {
return [pauseMonitor, isLoading, error];
};
const useAddDemoMonitors = () => {
export const useAddDemoMonitors = () => {
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const addDemoMonitors = async () => {
@@ -420,7 +442,7 @@ const useAddDemoMonitors = () => {
return [addDemoMonitors, isLoading];
};
const useDeleteAllMonitors = () => {
export const useDeleteAllMonitors = () => {
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const deleteAllMonitors = async () => {
@@ -437,7 +459,7 @@ const useDeleteAllMonitors = () => {
return [deleteAllMonitors, isLoading];
};
const useDeleteMonitorStats = () => {
export const useDeleteMonitorStats = () => {
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const deleteMonitorStats = async () => {
@@ -455,7 +477,7 @@ const useDeleteMonitorStats = () => {
return [deleteMonitorStats, isLoading];
};
const useCreateBulkMonitors = () => {
export const useCreateBulkMonitors = () => {
const [isLoading, setIsLoading] = useState(false);
const createBulkMonitors = async (file, user) => {
@@ -478,7 +500,7 @@ const useCreateBulkMonitors = () => {
return [createBulkMonitors, isLoading];
};
const useExportMonitors = () => {
export const useExportMonitors = () => {
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
@@ -511,7 +533,7 @@ const useExportMonitors = () => {
return [exportMonitors, isLoading];
};
const useFetchJson = () => {
export const useFetchJson = () => {
const [isLoading, setIsLoading] = useState(false);
const fetchJson = async () => {
try {
@@ -527,25 +549,3 @@ const useFetchJson = () => {
};
return [fetchJson, isLoading];
};
export {
useFetchMonitorsWithSummary,
useFetchMonitorsWithChecks,
useFetchMonitorsByTeamId,
useFetchStatsByMonitorId,
useFetchMonitorById,
useFetchUptimeMonitorById,
useFetchHardwareMonitorById,
useCreateMonitor,
useFetchGlobalSettings,
useDeleteMonitor,
useUpdateMonitor,
usePauseMonitor,
useAddDemoMonitors,
useDeleteAllMonitors,
useDeleteMonitorStats,
useCreateBulkMonitors,
useExportMonitors,
useFetchMonitorGames,
useFetchJson,
};
+3 -8
View File
@@ -11,14 +11,13 @@ import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
import { useTheme } from "@emotion/react";
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
import { useParams } from "react-router-dom";
import { useFetchStatsByMonitorId } from "../../../Hooks/monitorHooks.js";
import { useFetchPageSpeedMonitorById } from "../../../Hooks/monitorHooks.js";
import { useState } from "react";
import { useTranslation } from "react-i18next";
// Constants
const BREADCRUMBS = [
{ name: "pagespeed", path: "/pagespeed" },
{ name: "details", path: `` },
// { name: "details", path: `/pagespeed/${monitorId}` }, // Not needed?
];
const PageSpeedDetails = () => {
@@ -36,13 +35,9 @@ const PageSpeedDetails = () => {
});
const [trigger, setTrigger] = useState(false);
// Network
const [monitor, audits, isLoading, networkError] = useFetchStatsByMonitorId({
const [monitor, isLoading, networkError] = useFetchPageSpeedMonitorById({
monitorId,
sortOrder: "desc",
limit: 50,
dateRange: "day",
numToDisplay: null,
normalize: null,
updateTrigger: trigger,
});
@@ -116,7 +111,7 @@ const PageSpeedDetails = () => {
/>
<PerformanceReport
shouldRender={!isLoading}
audits={audits}
audits={monitor?.checks[0]?.audits}
/>
</Stack>
);
+10 -1
View File
@@ -152,7 +152,8 @@ class NetworkService {
params.append("type", type);
});
}
if (filter !== undefined && filter !== null && filter !== "") params.append("filter", filter);
if (filter !== undefined && filter !== null && filter !== "")
params.append("filter", filter);
return this.axiosInstance.get(`/monitors/team?${params.toString()}`, {
headers: {
@@ -197,6 +198,14 @@ class NetworkService {
`/monitors/hardware/details/${config.monitorId}?${params.toString()}`
);
}
async getPageSpeedDetailsByMonitorId(config) {
const params = new URLSearchParams();
if (config.dateRange) params.append("dateRange", config.dateRange);
return this.axiosInstance.get(
`/monitors/pagespeed/details/${config.monitorId}?${params.toString()}`
);
}
async getUptimeDetailsById(config) {
const params = new URLSearchParams();
if (config.dateRange) params.append("dateRange", config.dateRange);
+100 -1
View File
@@ -1,3 +1,6 @@
import { AppError } from "@/utils/AppError.js";
import { type MonitorType, MonitorTypes } from "@/types/index.js";
const fetchMonitorCertificate = async (sslChecker: any, monitor: any): Promise<any> => {
const monitorUrl = new URL(monitor.url);
const hostname = monitorUrl.hostname;
@@ -8,5 +11,101 @@ const fetchMonitorCertificate = async (sslChecker: any, monitor: any): Promise<a
}
return cert;
};
const requireString = (value: unknown, fieldName: string): string => {
if (typeof value === "string" && value.trim().length > 0) {
return value;
}
throw new AppError({ message: `${fieldName} is required`, status: 400 });
};
export { fetchMonitorCertificate };
const optionalString = (value: unknown, fieldName: string): string | undefined => {
if (value === undefined) {
return undefined;
}
if (typeof value === "string") {
return value;
}
throw new AppError({ message: `${fieldName} must be a string`, status: 400 });
};
const optionalNumber = (value: unknown, fieldName: string): number | undefined => {
if (value === undefined) {
return undefined;
}
if (typeof value === "number" && Number.isFinite(value)) {
return value;
}
if (typeof value === "string" && value.trim() !== "") {
const parsed = Number(value);
if (!Number.isNaN(parsed)) {
return parsed;
}
}
throw new AppError({ message: `${fieldName} must be a number`, status: 400 });
};
const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined => {
if (value === undefined) {
return undefined;
}
if (typeof value === "boolean") {
return value;
}
if (typeof value === "string") {
if (value === "true") {
return true;
}
if (value === "false") {
return false;
}
}
throw new AppError({ message: `${fieldName} must be a boolean`, status: 400 });
};
const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | undefined => {
const parseSingle = (input: unknown): MonitorType => {
if (typeof input !== "string") {
throw new AppError({ message: "Monitor type must be a string", status: 400 });
}
if (!MonitorTypes.includes(input as MonitorType)) {
throw new AppError({ message: `Invalid monitor type: ${input}`, status: 400 });
}
return input as MonitorType;
};
if (value === undefined) {
return undefined;
}
if (Array.isArray(value)) {
return value.map((entry) => parseSingle(entry));
}
return parseSingle(value);
};
const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => {
if (value === undefined) {
return undefined;
}
if (value === "asc" || value === "desc") {
return value;
}
throw new AppError({ message: "order must be either 'asc' or 'desc'", status: 400 });
};
const requireTeamId = (teamId?: string): string => {
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
return teamId;
};
export {
fetchMonitorCertificate,
requireString,
optionalString,
optionalNumber,
optionalBoolean,
parseMonitorTypeFilter,
parseSortOrder,
requireTeamId,
};
+79 -73
View File
@@ -14,16 +14,26 @@ import {
getHardwareDetailsByIdQueryValidation,
} from "@/validation/joi.js";
import sslChecker from "ssl-checker";
import { fetchMonitorCertificate } from "./controllerUtils.js";
import {
fetchMonitorCertificate,
requireString,
optionalString,
optionalNumber,
optionalBoolean,
parseMonitorTypeFilter,
parseSortOrder,
requireTeamId,
} from "./controllerUtils.js";
import { AppError } from "@/utils/AppError.js";
import { IMonitorService } from "@/service/index.js";
const SERVICE_NAME = "monitorController";
class MonitorController {
static SERVICE_NAME = SERVICE_NAME;
private monitorService: any;
private monitorService: IMonitorService;
constructor(monitorService: any) {
constructor(monitorService: IMonitorService) {
this.monitorService = monitorService;
}
@@ -32,8 +42,8 @@ class MonitorController {
}
async verifyTeamAccess(teamId: string, monitorId: string) {
const monitor = await this.monitorService.getMonitorById(monitorId);
if (!monitor.teamId.equals(teamId)) {
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
if (monitor.teamId !== teamId) {
throw new AppError({ message: "Access denied", status: 403 });
}
}
@@ -41,11 +51,8 @@ class MonitorController {
getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => {
try {
await getCertificateParamValidation.validateAsync(req.params);
const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const { monitorId } = req.params;
const teamId = requireTeamId(req?.user?.teamId);
const monitorId = requireString(req.params?.monitorId, "Monitor ID");
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
const certificate = await fetchMonitorCertificate(sslChecker, monitor);
@@ -63,15 +70,10 @@ class MonitorController {
getUptimeDetailsById = async (req: Request, res: Response, next: NextFunction) => {
try {
const monitorId = req?.params?.monitorId;
const dateRange = req?.query?.dateRange;
const normalize = req?.query?.normalize;
const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const dateRange = requireString(req?.query?.dateRange, "dateRange");
const normalize = optionalBoolean(req?.query?.normalize, "normalize");
const teamId = requireTeamId(req?.user?.teamId);
const data = await this.monitorService.getUptimeDetailsById({
teamId,
@@ -94,12 +96,9 @@ class MonitorController {
await getHardwareDetailsByIdParamValidation.validateAsync(req.params);
await getHardwareDetailsByIdQueryValidation.validateAsync(req.query);
const monitorId = req?.params?.monitorId;
const dateRange = req?.query?.dateRange;
const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const dateRange = requireString(req?.query?.dateRange, "dateRange");
const teamId = requireTeamId(req?.user?.teamId);
const monitor = await this.monitorService.getHardwareDetailsById({
teamId,
@@ -116,18 +115,40 @@ class MonitorController {
next(error);
}
};
getPageSpeedDetailsById = async (req: Request, res: Response, next: NextFunction) => {
try {
await getHardwareDetailsByIdParamValidation.validateAsync(req.params);
await getHardwareDetailsByIdQueryValidation.validateAsync(req.query);
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const dateRange = requireString(req?.query?.dateRange, "dateRange");
const teamId = requireTeamId(req?.user?.teamId);
const monitor = await this.monitorService.getPageSpeedDetailsById({
teamId,
monitorId,
dateRange,
});
return res.status(200).json({
success: true,
msg: "Page speed details retrieved successfully",
data: monitor,
});
} catch (error) {
next(error);
}
};
getMonitorById = async (req: Request, res: Response, next: NextFunction) => {
try {
await getMonitorByIdParamValidation.validateAsync(req.params);
await getMonitorByIdQueryValidation.validateAsync(req.query);
const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const teamId = requireTeamId(req?.user?.teamId);
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId: req?.params?.monitorId });
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
return res.status(200).json({
success: true,
@@ -143,8 +164,8 @@ class MonitorController {
try {
await createMonitorBodyValidation.validateAsync(req.body);
const userId = req?.user?._id;
const teamId = req?.user?.teamId;
const userId = requireString(req?.user?._id, "User ID");
const teamId = requireTeamId(req?.user?.teamId);
const monitor = await this.monitorService.createMonitor(teamId, userId, req.body);
@@ -172,12 +193,8 @@ class MonitorController {
throw new AppError({ message: "File is empty", status: 400 });
}
const userId = req?.user?._id;
const teamId = req?.user?.teamId;
if (!userId || !teamId) {
throw new AppError({ message: "Missing userId or teamId", status: 400 });
}
const userId = requireString(req?.user?._id, "User ID");
const teamId = requireTeamId(req?.user?.teamId);
const fileData = req?.file?.buffer?.toString("utf-8");
if (!fileData) {
@@ -199,11 +216,8 @@ class MonitorController {
deleteMonitor = async (req: Request, res: Response, next: NextFunction) => {
try {
await getMonitorByIdParamValidation.validateAsync(req.params);
const monitorId = req.params.monitorId;
const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const teamId = requireTeamId(req?.user?.teamId);
const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId });
@@ -219,10 +233,7 @@ class MonitorController {
deleteAllMonitors = async (req: Request, res: Response, next: NextFunction) => {
try {
const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const teamId = requireTeamId(req?.user?.teamId);
const deletedCount = await this.monitorService.deleteAllMonitors({ teamId });
@@ -239,12 +250,8 @@ class MonitorController {
try {
await getMonitorByIdParamValidation.validateAsync(req.params);
await editMonitorBodyValidation.validateAsync(req.body);
const monitorId = req?.params?.monitorId;
const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const teamId = requireTeamId(req?.user?.teamId);
const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: req.body });
@@ -262,11 +269,8 @@ class MonitorController {
try {
await pauseMonitorParamValidation.validateAsync(req.params);
const monitorId = req.params.monitorId;
const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const teamId = requireTeamId(req?.user?.teamId);
const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId });
@@ -282,7 +286,8 @@ class MonitorController {
addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => {
try {
const { _id, teamId } = req.user;
const _id = requireString(req?.user?._id, "User ID");
const teamId = requireTeamId(req?.user?.teamId);
const demoMonitors = await this.monitorService.addDemoMonitors({ userId: _id, teamId });
return res.status(200).json({
@@ -318,8 +323,9 @@ class MonitorController {
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
const { type, filter } = req.query;
const teamId = req?.user?.teamId;
const teamId = requireTeamId(req?.user?.teamId);
const type = parseMonitorTypeFilter(req.query?.type);
const filter = optionalString(req.query?.filter, "filter");
const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, type, filter });
@@ -338,12 +344,9 @@ class MonitorController {
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
const explain = req?.query?.explain;
const type = req?.query?.type;
const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const explain = optionalBoolean(req?.query?.explain, "explain");
const type = parseMonitorTypeFilter(req?.query?.type);
const teamId = requireTeamId(req?.user?.teamId);
const result = await this.monitorService.getMonitorsAndSummaryByTeamId({ teamId, type, explain });
@@ -361,12 +364,15 @@ class MonitorController {
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
await getMonitorsWithChecksQueryValidation.validateAsync(req.query);
const explain = req?.query?.explain;
let { limit, type, page, rowsPerPage, filter, field, order } = req.query;
const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const explain = optionalBoolean(req?.query?.explain, "explain");
const limit = optionalNumber(req?.query?.limit, "limit");
const page = optionalNumber(req?.query?.page, "page");
const rowsPerPage = optionalNumber(req?.query?.rowsPerPage, "rowsPerPage");
const filter = optionalString(req?.query?.filter, "filter");
const field = optionalString(req?.query?.field, "field");
const order = parseSortOrder(req?.query?.order);
const type = parseMonitorTypeFilter(req?.query?.type);
const teamId = requireTeamId(req?.user?.teamId);
const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({
teamId,
@@ -1,6 +1,58 @@
import type { Check, MonitorType } from "@/types/index.js";
import type { Check, CheckAudits, MonitorType } from "@/types/index.js";
import type { LatestChecksMap } from "@/repositories/checks/MongoChecksRepistory.js";
export interface PageSpeedChecksResult {
monitorType: "pagespeed";
checks: Check[];
}
export interface HardwareChecksResult {
monitorType: "hardware";
aggregateData: {
latestCheck: Check | null;
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: Array<{ _id: string; avgResponseTime: number; totalChecks: number }>;
groupedUpChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>;
groupedDownChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>;
uptimePercentage: number;
avgResponseTime: number;
}
export interface IChecksRepository {
findLatestChecksByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise<LatestChecksMap>;
findDateRangeChecksByMonitor(
@@ -9,51 +61,5 @@ export interface IChecksRepository {
endDate: Date,
dateString: string,
options?: { type?: MonitorType }
): Promise<
| {
monitorType: "uptime";
groupedChecks: Array<{ _id: string; avgResponseTime: number; totalChecks: number }>;
groupedUpChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>;
groupedDownChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>;
uptimePercentage: number;
avgResponseTime: number;
}
| {
monitorType: "hardware";
aggregateData: {
latestCheck: Check | null;
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;
}>;
}>;
}
>;
): Promise<UptimeChecksResult | HardwareChecksResult | PageSpeedChecksResult>;
}
@@ -11,6 +11,7 @@ import type {
CheckMetadata,
CheckNetworkInterfaceInfo,
CheckTimings,
MonitorType,
} from "@/types/index.js";
import { CheckModel, type CheckDocument } from "@/db/models/index.js";
import mongoose from "mongoose";
@@ -177,10 +178,7 @@ class MongoChecksRepistory implements IChecksRepository {
};
};
findLatestChecksByMonitorIds = async (
monitorIds: string[],
options?: { limitPerMonitor?: number }
): Promise<LatestChecksMap> => {
findLatestChecksByMonitorIds = async (monitorIds: string[], options?: { limitPerMonitor?: number }): Promise<LatestChecksMap> => {
if (monitorIds.length === 0) {
return {};
}
@@ -218,15 +216,24 @@ class MongoChecksRepistory implements IChecksRepository {
}, {});
};
findDateRangeChecksByMonitor = async (monitorId: string, startDate: Date, endDate: Date, dateString: string, options?: { type?: string }) => {
findDateRangeChecksByMonitor = async (monitorId: string, startDate: Date, endDate: Date, dateString: string, options?: { type?: MonitorType }) => {
const monitorObjectId = new mongoose.Types.ObjectId(monitorId);
if (options?.type === "hardware") {
return this.findHardwareDateRangeChecks(monitorObjectId, startDate, endDate, dateString);
}
return this.findUptimeDateRangeChecks(monitorObjectId, startDate, endDate, dateString);
if (options?.type === "pagespeed") {
return this.findPageSpeedDateRangeChecks(monitorObjectId, startDate, endDate);
}
return this.findUptimeDateRangeChecks(options?.type ?? "http", monitorObjectId, startDate, endDate, dateString);
};
private findUptimeDateRangeChecks = async (monitorObjectId: mongoose.Types.ObjectId, startDate: Date, endDate: Date, dateString: string) => {
private findUptimeDateRangeChecks = async (
monitorType: Exclude<MonitorType, "hardware" | "pagespeed">,
monitorObjectId: mongoose.Types.ObjectId,
startDate: Date,
endDate: Date,
dateString: string
) => {
const matchStage = {
"metadata.monitorId": monitorObjectId,
updatedAt: { $gte: startDate, $lte: endDate },
@@ -307,7 +314,7 @@ class MongoChecksRepistory implements IChecksRepository {
const avgResponseTime = result?.groupedAvgResponseTime?.[0]?.avgResponseTime ?? 0;
return {
monitorType: "uptime" as const,
monitorType,
groupedChecks: result?.groupedChecks ?? [],
groupedUpChecks: result?.groupedUpChecks ?? [],
groupedDownChecks: result?.groupedDownChecks ?? [],
@@ -369,6 +376,19 @@ class MongoChecksRepistory implements IChecksRepository {
checks,
};
};
private findPageSpeedDateRangeChecks = async (monitorObjectId: mongoose.Types.ObjectId, startDate: Date, endDate: Date) => {
const matchStage = {
"metadata.monitorId": monitorObjectId,
createdAt: { $gte: startDate, $lte: endDate },
};
const checks = await CheckModel.find(matchStage).sort({ createdAt: -1 }).limit(25).lean();
return {
monitorType: "pagespeed" as const,
checks: checks.map((doc) => this.toEntity(doc)),
};
};
}
export default MongoChecksRepistory;
+2
View File
@@ -27,6 +27,8 @@ class MonitorRoutes {
// Hardware routes
this.router.get("/hardware/details/:monitorId", this.monitorController.getHardwareDetailsById);
// PageSpeed routes
this.router.get("/pagespeed/details/:monitorId", this.monitorController.getPageSpeedDetailsById);
// General monitor routes
this.router.post("/pause/:monitorId", isAllowed(["admin", "superadmin"]), this.monitorController.pauseMonitor);
+32 -1
View File
@@ -24,6 +24,7 @@ export interface IMonitorService {
// read
getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise<any>;
getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<any>;
getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<any>;
getMonitorById(args: { teamId: string; monitorId: string }): Promise<Monitor>;
getMonitorsByTeamId(args: {
teamId: string;
@@ -268,7 +269,13 @@ export class MonitorService implements IMonitorService {
});
const monitorStats = await this.monitorStatsRepository.findByMonitorId(monitor.id);
if (checksData.monitorType !== "uptime") {
if (
checksData.monitorType !== "http" &&
checksData.monitorType !== "ping" &&
checksData.monitorType !== "docker" &&
checksData.monitorType !== "port" &&
checksData.monitorType !== "game"
) {
throw new AppError({ message: `${monitor.type} monitors are not supported for uptime details`, status: 400 });
}
@@ -317,6 +324,30 @@ export class MonitorService implements IMonitorService {
};
};
getPageSpeedDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise<any> => {
await this.verifyTeamAccess({ teamId, monitorId });
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
if (!monitor) {
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
}
if (monitor.type !== "pagespeed") {
throw new AppError({ message: `${monitor.type} monitors are not supported for pagespeed details`, status: 400 });
}
const rangeKey = (dateRange as DateRangeKey) ?? "recent";
const { start, end } = this.getDateRange(rangeKey);
const checksData = await this.checksRepository.findDateRangeChecksByMonitor(monitor.id, start, end, this.getDateFormat(rangeKey), {
type: monitor.type,
});
if (checksData.monitorType !== "pagespeed") {
throw new AppError({ message: "Unable to load pagespeed stats for this monitor", status: 500 });
}
return {
...monitor,
checks: checksData.checks,
};
};
getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<any> => {
await this.verifyTeamAccess({ teamId, monitorId });
const monitor = await this.monitorsRepository.findById(monitorId, teamId);