Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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>;
findDailyPageSpeedSnapshotsByMonitorIdsAndDateRange(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
70 changes: 70 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,75 @@ class MongoChecksRepository implements IChecksRepository {
return this.findUptimeDateRangeChecks(options?.type ?? "http", monitorObjectId, start, end, dateString);
};

findDailyPageSpeedSnapshotsByMonitorIdsAndDateRange = 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: "$performance" },
accessibility: { $avg: "$accessibility" },
bestPractices: { $avg: "$bestPractices" },
seo: { $avg: "$seo" },
},
},
{ $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.findDailyPageSpeedSnapshotsByMonitorIdsAndDateRange(
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
77 changes: 77 additions & 0 deletions server/test/integration/checksRepositoryGroupedChecks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,81 @@ 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: new Date(earlyDay.getTime() + 2 * 60 * 60 * 1000),
},
{
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.findDailyPageSpeedSnapshotsByMonitorIdsAndDateRange([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(),
findDailyPageSpeedSnapshotsByMonitorIdsAndDateRange: 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.findDailyPageSpeedSnapshotsByMonitorIdsAndDateRange as jest.Mock).mockResolvedValue({
[MONITOR_ID]: monthHistory,
});

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

expect(checksRepository.findDailyPageSpeedSnapshotsByMonitorIdsAndDateRange).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