Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions server/src/domain/checks/check.repository.interface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {
Check,
CheckSnapshot,
ChecksQueryResult,
ChecksSummary,
HardwareChecksResult,
Expand Down Expand Up @@ -38,6 +39,7 @@ export interface IChecksRepository {
dateRange: DateRange,
options?: { type?: MonitorType }
): Promise<UptimeChecksResult | HardwareChecksResult | PageSpeedChecksResult>;
findSnapshotsByMonitorIdsAndDateRange(monitorIds: string[], dateRange: DateRange): Promise<Record<string, CheckSnapshot[]>>;
findSummaryByTeamId(teamId: string, dateRange: DateRange): Promise<ChecksSummary>;
findUnevaluatedByMonitorId(monitorId: string, since: number): Promise<Check[]>;
// update
Expand Down
67 changes: 67 additions & 0 deletions server/src/domain/checks/check.repository.mongo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
CheckMemoryInfo,
CheckMetadata,
CheckNetworkInterfaceInfo,
CheckSnapshot,
GotTimings,
HardwareCheckStats,
} from "@/domain/checks/check.type.js";
Expand Down Expand Up @@ -302,6 +303,72 @@ class MongoChecksRepository implements IChecksRepository {
return this.findUptimeDateRangeChecks(options?.type ?? "http", monitorObjectId, start, end, dateString);
};

findSnapshotsByMonitorIdsAndDateRange = async (monitorIds: string[], dateRange: DateRange): Promise<Record<string, CheckSnapshot[]>> => {
if (monitorIds.length === 0) {
return {};
}

const dateString = getDateFormat(dateRange);
const rows = await CheckModel.aggregate<{
monitorId: mongoose.Types.ObjectId;
bucketDate: string;
performance: number;
accessibility: number;
bestPractices: number;
seo: number;
}>([
{
$match: {
"metadata.monitorId": { $in: monitorIds.map((monitorId) => new mongoose.Types.ObjectId(monitorId)) },
"metadata.type": "pagespeed",
createdAt: { $gte: getDateForRange(dateRange) },
},
},
{
$group: {
_id: {
monitorId: "$metadata.monitorId",
bucketDate: { $dateToString: { format: dateString, date: "$createdAt" } },
},
performance: { $avg: { $ifNull: ["$performance", 0] } },
accessibility: { $avg: { $ifNull: ["$accessibility", 0] } },
bestPractices: { $avg: { $ifNull: ["$bestPractices", 0] } },
seo: { $avg: { $ifNull: ["$seo", 0] } },
},
},
{ $sort: { "_id.monitorId": 1, "_id.bucketDate": 1 } },
{
$project: {
_id: 0,
monitorId: "$_id.monitorId",
bucketDate: "$_id.bucketDate",
performance: 1,
accessibility: 1,
bestPractices: 1,
seo: 1,
},
},
]);

return rows.reduce<Record<string, CheckSnapshot[]>>((snapshotsByMonitorId, row) => {
const monitorId = toStringId(row.monitorId);
const snapshot: CheckSnapshot = {
id: row.bucketDate,
status: true,
responseTime: 0,
statusCode: 200,
message: "",
createdAt: row.bucketDate,
performance: row.performance,
accessibility: row.accessibility,
bestPractices: row.bestPractices,
seo: row.seo,
};
(snapshotsByMonitorId[monitorId] ??= []).push(snapshot);
return snapshotsByMonitorId;
}, {});
};

findSummaryByTeamId = async (teamId: string, dateRange: DateRange) => {
const baseMatch = {
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
Expand Down
14 changes: 13 additions & 1 deletion server/src/domain/monitors/monitor.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,11 +359,23 @@ export class MonitorService implements IMonitorService {
const requestedTypes = Array.isArray(type) ? type : type ? [type] : [];
const snapshotOnlyRequest =
requestedTypes.length > 0 && requestedTypes.every((requestedType) => snapshotTypes.includes(requestedType as MonitorType));
const isPageSpeedOverview = requestedTypes.length > 0 && requestedTypes.every((requestedType) => requestedType === "pagespeed");
const pageSpeedChecksByMonitorId = isPageSpeedOverview
? await this.checksRepository.findSnapshotsByMonitorIdsAndDateRange(
monitors.map((monitor) => monitor.id),
"month"
)
: {};

const monitorsWithChecks = monitors.map((monitor: Monitor) => {
const rawChecks = monitor.recentChecks ?? [];
const isSnapshotType = snapshotOnlyRequest || snapshotTypes.includes(monitor.type);
const checks = isSnapshotType ? rawChecks.slice(-1) : rawChecks;
const checks =
isPageSpeedOverview && monitor.type === "pagespeed"
? (pageSpeedChecksByMonitorId[monitor.id] ?? [])
: isSnapshotType
? rawChecks.slice(-1)
: rawChecks;
monitor.recentChecks = checks;
return monitor;
});
Expand Down
71 changes: 71 additions & 0 deletions server/test/integration/checksRepositoryGroupedChecks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,75 @@ describe("MongoChecksRepository groupedChecks facet", () => {
avgDownload: 0,
});
});

it("returns ordered daily Pagespeed snapshots for the requested monitors", async () => {
const pageSpeedMonitorId = new mongoose.Types.ObjectId();
const otherMonitorId = new mongoose.Types.ObjectId();
const earlyDay = new Date(Date.now() - 10 * 24 * 60 * 60 * 1000);
earlyDay.setUTCHours(10, 0, 0, 0);
const laterDay = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000);
laterDay.setUTCHours(10, 0, 0, 0);

await CheckModel.create([
{
metadata: { monitorId: pageSpeedMonitorId, teamId: TEAM_ID, type: "pagespeed" },
status: true,
responseTime: 100,
createdAt: earlyDay,
performance: 40,
accessibility: 60,
bestPractices: 80,
seo: 100,
},
{
metadata: { monitorId: pageSpeedMonitorId, teamId: TEAM_ID, type: "pagespeed" },
status: true,
responseTime: 100,
createdAt: new Date(earlyDay.getTime() + 60 * 60 * 1000),
performance: 60,
accessibility: 80,
bestPractices: 100,
seo: 80,
},
{
metadata: { monitorId: pageSpeedMonitorId, teamId: TEAM_ID, type: "pagespeed" },
status: true,
responseTime: 100,
createdAt: laterDay,
performance: 90,
accessibility: 90,
bestPractices: 90,
seo: 90,
},
{
metadata: { monitorId: otherMonitorId, teamId: TEAM_ID, type: "pagespeed" },
status: true,
responseTime: 100,
createdAt: laterDay,
performance: 10,
accessibility: 10,
bestPractices: 10,
seo: 10,
},
]);

const snapshots = await repo.findSnapshotsByMonitorIdsAndDateRange([pageSpeedMonitorId.toString()], "month");
const toBucketDate = (date: Date) => `${date.toISOString().slice(0, 10)}T00:00:00Z`;

expect(snapshots).toEqual({
[pageSpeedMonitorId.toString()]: [
expect.objectContaining({
createdAt: toBucketDate(earlyDay),
performance: 50,
accessibility: 70,
bestPractices: 90,
seo: 90,
}),
expect.objectContaining({
createdAt: toBucketDate(laterDay),
performance: 90,
}),
],
});
});
});
38 changes: 38 additions & 0 deletions server/test/unit/services/monitorService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const createMonitorsRepositoryMock = () =>
const createChecksRepositoryMock = () =>
({
findByDateRangeAndMonitorId: jest.fn(),
findSnapshotsByMonitorIdsAndDateRange: jest.fn(),
deleteByMonitorId: jest.fn(),
}) as unknown as IChecksRepository;

Expand Down Expand Up @@ -570,6 +571,43 @@ describe("MonitorService", () => {
expect(result.monitors).toHaveLength(1);
});

it("loads a month of history for pagespeed overview monitors", async () => {
const monitorsRepository = createMonitorsRepositoryMock();
const checksRepository = createChecksRepositoryMock();
const monthHistory = [
{
id: "check-from-last-month",
status: true,
responseTime: 100,
statusCode: 200,
message: "OK",
createdAt: "2026-07-10T12:00:00.000Z",
accessibility: 90,
bestPractices: 80,
performance: 70,
seo: 95,
},
];

(monitorsRepository.findMonitorsSummaryByTeamId as jest.Mock).mockResolvedValue({ totalMonitors: 1 });
(monitorsRepository.findMonitorCountByTeamIdAndType as jest.Mock).mockResolvedValue(1);
(monitorsRepository.findByTeamIdWithStats as jest.Mock).mockResolvedValue([
makeMonitor({
type: "pagespeed",
recentChecks: [],
}),
]);
(checksRepository.findSnapshotsByMonitorIdsAndDateRange as jest.Mock).mockResolvedValue({
[MONITOR_ID]: monthHistory,
});

const { service } = createService({ monitorsRepository, checksRepository });
const result = await service.getMonitorsWithChecksByTeamId({ teamId: TEAM_ID, type: "pagespeed" });

expect(checksRepository.findSnapshotsByMonitorIdsAndDateRange).toHaveBeenCalledWith([MONITOR_ID], "month");
expect(result.monitors[0].recentChecks).toEqual(monthHistory);
});

it("returns null summary when repository returns null", async () => {
const monitorsRepository = createMonitorsRepositoryMock();
(monitorsRepository.findMonitorsSummaryByTeamId as jest.Mock).mockResolvedValue(null);
Expand Down
Loading