From c8dc5b93cb52451de19edfd32369aa493f1266f3 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 17 Dec 2025 21:11:51 +0000 Subject: [PATCH] aggregates --- client/public/env.js | 4 +- .../components/monitors/ChartResponseTime.tsx | 7 +- .../components/monitors/HistogramStatus.tsx | 58 ++++---- client/src/locales/en.json | 3 +- client/src/pages/uptime/UptimeDetails.tsx | 59 ++++---- client/src/types/check.ts | 4 + server/src/config/index.ts | 2 + server/src/controllers/MonitorController.ts | 8 +- server/src/db/models/monitors/StatsDaily.ts | 5 +- server/src/db/models/monitors/StatsHourly.ts | 9 +- .../src/services/business/MonitorService.ts | 56 ++++++-- .../business/StatsAggregationService.ts | 129 ++++++++++++++++++ .../services/infrastructure/JobGenerator.ts | 46 +++++-- .../src/services/infrastructure/JobQueue.ts | 11 +- 14 files changed, 302 insertions(+), 99 deletions(-) diff --git a/client/public/env.js b/client/public/env.js index 92cd34980..9c6c7c9a2 100644 --- a/client/public/env.js +++ b/client/public/env.js @@ -2,6 +2,6 @@ window.__ENV = { GLOBAL_REFRESH: "60000", APP_HOST: "http://localhost:5173", APP_API_BASE_URL: "http://localhost:52345/api/v1", - // APP_DEPLOYMENT_MODE: "self_hosted", - APP_DEPLOYMENT_MODE: "saas", + APP_DEPLOYMENT_MODE: "self_hosted", + // APP_DEPLOYMENT_MODE: "saas", }; diff --git a/client/src/components/monitors/ChartResponseTime.tsx b/client/src/components/monitors/ChartResponseTime.tsx index fda790cf9..12ee5252f 100644 --- a/client/src/components/monitors/ChartResponseTime.tsx +++ b/client/src/components/monitors/ChartResponseTime.tsx @@ -94,9 +94,12 @@ export const ChartResponseTime = ({ "avgResponseTime" ); return ( - } title="Response times"> + } + title="Response times" + > - + { const { t } = useTranslation(); - const uiTimezone = useAppSelector((state: any) => state.ui.timezone); const icon = status === "up" ? ( @@ -74,10 +80,31 @@ export const HistogramStatus = ({ ); const theme = useTheme(); - const [idx, setIdx] = useState(null); - const dateFormat = range === "1d" || range === "2h" ? "MMM D, h A" : "MMM D"; const normalChecks = normalizeResponseTimes(checks, "avgResponseTime"); + const CustomTooltip = ({ active, payload }: any) => { + if (!active || !payload?.length) return null; + const d = payload[0]?.payload as GroupedCheck & { + avgResponseTime?: number; + }; + const avg = d?.avgResponseTime ?? 0; + const titleText = t("common.charts.uptime.avgResponseTime"); + return ( + + {titleText} + {Math.floor(avg)} ms + + ); + }; + if (normalChecks.length === 0) { return ( @@ -105,23 +132,7 @@ export const HistogramStatus = ({ justifyContent="space-between" > - Total checks - {idx ? ( - - - {normalChecks?.[idx]?.count} - - - {formatDateWithTz( - normalChecks?.[idx]?._id, - dateFormat, - uiTimezone - )} - - - ) : ( - {totalChecks} - )} + Checks: {totalChecks} @@ -138,19 +149,18 @@ export const HistogramStatus = ({ /> } /> + } /> - {normalChecks?.map((groupedCheck, idx) => { + {normalChecks?.map((groupedCheck) => { const fillColor = getResponseTimeColor( groupedCheck.normalResponseTime ); return ( setIdx(idx)} - onMouseLeave={() => setIdx(null)} key={groupedCheck._id} fill={theme.palette[fillColor].main} /> diff --git a/client/src/locales/en.json b/client/src/locales/en.json index e7d087bea..a86245605 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -211,7 +211,8 @@ "downTitle": "Downtime", "emptyDown": "Great, no downtime!", "emptyUp": "No checks yet", - "upTitle": "Uptime" + "upTitle": "Uptime", + "avgResponseTime": "Avg response time" } }, "dialog": { diff --git a/client/src/pages/uptime/UptimeDetails.tsx b/client/src/pages/uptime/UptimeDetails.tsx index 193aa301b..1e74c4835 100644 --- a/client/src/pages/uptime/UptimeDetails.tsx +++ b/client/src/pages/uptime/UptimeDetails.tsx @@ -40,25 +40,7 @@ const UptimeDetailsPage = () => { { refreshInterval: GLOBAL_REFRESH, keepPreviousData: true } ); - const { - response: upResponse, - loading: upIsLoading, - error: upError, - } = useGet>( - `/monitors/${id}?embedChecks=true&range=${range}&status=up`, - {}, - { keepPreviousData: true } - ); - - const { - response: downResponse, - error: downError, - loading: downIsLoading, - } = useGet>( - `/monitors/${id}?embedChecks=true&range=${range}&status=down`, - {}, - { keepPreviousData: true } - ); + // Up/Down derived from single response (server returns up/down/all in one payload) const { patch, @@ -85,20 +67,33 @@ const UptimeDetailsPage = () => { : -1; const checks = response?.data?.checks || []; - const upChecks = upResponse?.data?.checks - ? [...upResponse.data.checks].reverse() - : []; - const downChecks = downResponse?.data?.checks - ? [...downResponse.data.checks].reverse() - : []; + const checksAll = checks.length ? [...checks].reverse() : []; + const upChecksMapped = checksAll.map((c: any) => ({ + ...c, + count: c.upChecks ?? 0, + avgResponseTime: c.avgResponseTimeUp ?? 0, + })); + const totalUp = upChecksMapped.reduce( + (sum: number, c: any) => sum + (c.count ?? 0), + 0 + ); + const upChecks = totalUp === 0 ? [] : upChecksMapped; + const downChecksMapped = checksAll.map((c: any) => ({ + ...c, + count: c.downChecks ?? 0, + avgResponseTime: c.avgResponseTimeDown ?? 0, + })); + const totalDown = downChecksMapped.reduce( + (sum: number, c: any) => sum + (c.count ?? 0), + 0 + ); + const downChecks = totalDown === 0 ? [] : downChecksMapped; const palette = getStatusPalette(monitor?.status); - if (error || upError || downError || postError) { + if (error || postError) { console.error("Error fetching monitor data:", { error, - upError, - downError, postError, }); } @@ -145,11 +140,7 @@ const UptimeDetailsPage = () => { } /> - + { /> - + ); diff --git a/client/src/types/check.ts b/client/src/types/check.ts index 60effdbda..ecfffea07 100644 --- a/client/src/types/check.ts +++ b/client/src/types/check.ts @@ -147,6 +147,10 @@ export interface GroupedCheck { _id: string; avgResponseTime: number; count: number; + upChecks?: number; + downChecks?: number; + avgResponseTimeUp?: number | null; + avgResponseTimeDown?: number | null; } export interface LatestCheck { diff --git a/server/src/config/index.ts b/server/src/config/index.ts index f7c9ab52a..29fd67a91 100644 --- a/server/src/config/index.ts +++ b/server/src/config/index.ts @@ -15,6 +15,7 @@ export interface IEnvConfig { SMTP_PASS: string; STRIPE_SECRET: string; STRIPE_WEBHOOK_SECRET: string; + STATS_USE_CURRENT_HOUR?: boolean; } dotenv.config(); @@ -34,4 +35,5 @@ export const config: IEnvConfig = { SMTP_PASS: process.env.SMTP_PASS || "not_set", STRIPE_SECRET: process.env.STRIPE_SECRET || "not_set", STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET || "not_set", + STATS_USE_CURRENT_HOUR: process.env.STATS_USE_CURRENT_HOUR === "true", }; diff --git a/server/src/controllers/MonitorController.ts b/server/src/controllers/MonitorController.ts index a2ec60bff..d98956cce 100644 --- a/server/src/controllers/MonitorController.ts +++ b/server/src/controllers/MonitorController.ts @@ -205,11 +205,6 @@ class MonitorController { let monitor; - const status = req.query.status; - if (status && typeof status !== "string") { - throw new ApiError("Status query parameter must be a string", 400); - } - if (req.query.embedChecks === "true") { const range = req.query.range; if (!range || typeof range !== "string") @@ -218,8 +213,7 @@ class MonitorController { monitor = await this.monitorService.getEmbedChecks( teamId, monitorId, - range, - status + range ); } else { monitor = await this.monitorService.get(teamId, monitorId); diff --git a/server/src/db/models/monitors/StatsDaily.ts b/server/src/db/models/monitors/StatsDaily.ts index 6a382f076..1d41a5604 100644 --- a/server/src/db/models/monitors/StatsDaily.ts +++ b/server/src/db/models/monitors/StatsDaily.ts @@ -100,6 +100,10 @@ const StatsDailySchema = new Schema( finalized: { type: Boolean, default: false }, count: { type: Number, required: true, default: 0 }, avgResponseTime: { type: Number, required: true, default: 0 }, + upChecks: { type: Number, required: false }, + downChecks: { type: Number, required: false }, + avgResponseTimeUp: { type: Number, required: false }, + avgResponseTimeDown: { type: Number, required: false }, // Pagespeed accessibility: { type: Number }, @@ -129,4 +133,3 @@ export const StatsDaily = mongoose.model( "StatsDaily", StatsDailySchema ); - diff --git a/server/src/db/models/monitors/StatsHourly.ts b/server/src/db/models/monitors/StatsHourly.ts index 3ee6ca685..34a9a7ab1 100644 --- a/server/src/db/models/monitors/StatsHourly.ts +++ b/server/src/db/models/monitors/StatsHourly.ts @@ -11,6 +11,10 @@ export interface IStatsBase { finalized: boolean; count: number; // number of checks in window avgResponseTime: number; // ms + upChecks?: number; + downChecks?: number; + avgResponseTimeUp?: number; + avgResponseTimeDown?: number; createdAt: Date; updatedAt: Date; } @@ -170,6 +174,10 @@ const StatsHourlySchema = new Schema( finalized: { type: Boolean, default: false }, count: { type: Number, required: true, default: 0 }, avgResponseTime: { type: Number, required: true, default: 0 }, + upChecks: { type: Number, required: false }, + downChecks: { type: Number, required: false }, + avgResponseTimeUp: { type: Number, required: false }, + avgResponseTimeDown: { type: Number, required: false }, // Pagespeed accessibility: { type: Number }, @@ -199,4 +207,3 @@ export const StatsHourly = mongoose.model( "StatsHourly", StatsHourlySchema ); - diff --git a/server/src/services/business/MonitorService.ts b/server/src/services/business/MonitorService.ts index 0ed063826..774950560 100644 --- a/server/src/services/business/MonitorService.ts +++ b/server/src/services/business/MonitorService.ts @@ -64,8 +64,7 @@ export interface IMonitorService { getEmbedChecks: ( teamId: string, monitorId: string, - range: string, - status?: string + range: string ) => Promise; togglePause: ( userId: string, @@ -504,11 +503,11 @@ class MonitorService implements IMonitorService { return {}; }; + // status parameter deprecated and ignored; retained for backward compatibility getEmbedChecks = async ( teamId: string, monitorId: string, - range: string, - status: string | undefined + range: string ): Promise => { const monitor = await Monitor.findOne({ _id: monitorId, teamId }); if (!monitor) { @@ -519,7 +518,9 @@ class MonitorService implements IMonitorService { // Route for aggregated stats (24h, 7d, 30d) if (range === "24h" || range === "7d" || range === "30d") { - const monitorStats = await MonitorStats.findOne({ monitorId: monitor._id }); + const monitorStats = await MonitorStats.findOne({ + monitorId: monitor._id, + }); if (!monitorStats) { throw new ApiError("Monitor stats not found", 404); } @@ -540,6 +541,10 @@ class MonitorService implements IMonitorService { _id: new Date(d.windowStart).toISOString(), count: d.count ?? 0, avgResponseTime: d.avgResponseTime ?? 0, + upChecks: d.upChecks ?? 0, + downChecks: d.downChecks ?? 0, + avgResponseTimeUp: d.avgResponseTimeUp ?? null, + avgResponseTimeDown: d.avgResponseTimeDown ?? null, }; if (monitor.type === "pagespeed") { return { @@ -579,16 +584,11 @@ class MonitorService implements IMonitorService { const matchStage: { "metadata.monitorId": mongoose.Types.ObjectId; createdAt: { $gte: Date }; - status?: string; } = { "metadata.monitorId": monitor._id, createdAt: { $gte: startDate }, }; - if (status) { - matchStage.status = status; - } - const dateFormat = "%Y-%m-%dT%H:%M:00Z"; let groupClause; if (monitor.type === "pagespeed") { @@ -620,11 +620,41 @@ class MonitorService implements IMonitorService { $match: matchStage, }, { $sort: { createdAt: 1 } }, - { $project: projectStage }, - { $group: groupClause }, + { $project: { ...projectStage, status: 1 } }, + { + $group: { + ...groupClause, + upChecks: { $sum: { $cond: [{ $eq: ["$status", "up"] }, 1, 0] } }, + downChecks: { $sum: { $cond: [{ $eq: ["$status", "down"] }, 1, 0] } }, + sumResponseUp: { + $sum: { $cond: [{ $eq: ["$status", "up"] }, "$responseTime", 0] }, + }, + sumResponseDown: { + $sum: { $cond: [{ $eq: ["$status", "down"] }, "$responseTime", 0] }, + }, + }, + }, { $sort: { _id: -1 } }, { - $project: finalProjection, + $project: { + ...finalProjection, + upChecks: 1, + downChecks: 1, + avgResponseTimeUp: { + $cond: [ + { $gt: ["$upChecks", 0] }, + { $divide: ["$sumResponseUp", "$upChecks"] }, + null, + ], + }, + avgResponseTimeDown: { + $cond: [ + { $gt: ["$downChecks", 0] }, + { $divide: ["$sumResponseDown", "$downChecks"] }, + null, + ], + }, + }, }, ]); diff --git a/server/src/services/business/StatsAggregationService.ts b/server/src/services/business/StatsAggregationService.ts index cfa3a9e32..d6e8c81e0 100644 --- a/server/src/services/business/StatsAggregationService.ts +++ b/server/src/services/business/StatsAggregationService.ts @@ -35,6 +35,46 @@ class StatsAggregationService implements IStatsAggregationService { type: { $first: "$metadata.type" }, count: { $sum: 1 }, avgResponseTime: { $avg: "$responseTime" }, + upChecks: { + $sum: { $cond: [{ $eq: ["$status", "up"] }, 1, 0] }, + }, + downChecks: { + $sum: { $cond: [{ $eq: ["$status", "down"] }, 1, 0] }, + }, + sumResponseUp: { + $sum: { + $cond: [{ $eq: ["$status", "up"] }, "$responseTime", 0], + }, + }, + sumResponseDown: { + $sum: { + $cond: [{ $eq: ["$status", "down"] }, "$responseTime", 0], + }, + }, + }, + }, + { + $project: { + teamId: 1, + type: 1, + count: 1, + avgResponseTime: 1, + upChecks: 1, + downChecks: 1, + avgResponseTimeUp: { + $cond: [ + { $gt: ["$upChecks", 0] }, + { $divide: ["$sumResponseUp", "$upChecks"] }, + null, + ], + }, + avgResponseTimeDown: { + $cond: [ + { $gt: ["$downChecks", 0] }, + { $divide: ["$sumResponseDown", "$downChecks"] }, + null, + ], + }, }, }, ]); @@ -44,6 +84,7 @@ class StatsAggregationService implements IStatsAggregationService { { $match: { ...matchWindow, "metadata.type": "pagespeed" } }, { $project: { + status: 1, responseTime: 1, createdAt: 1, "lighthouse.accessibility": 1, @@ -67,6 +108,14 @@ class StatsAggregationService implements IStatsAggregationService { type: { $first: "$metadata.type" }, count: { $sum: 1 }, avgResponseTime: { $avg: "$responseTime" }, + upChecks: { $sum: { $cond: [{ $eq: ["$status", "up"] }, 1, 0] } }, + downChecks: { $sum: { $cond: [{ $eq: ["$status", "down"] }, 1, 0] } }, + sumResponseUp: { + $sum: { $cond: [{ $eq: ["$status", "up"] }, "$responseTime", 0] }, + }, + sumResponseDown: { + $sum: { $cond: [{ $eq: ["$status", "down"] }, "$responseTime", 0] }, + }, accessibility: { $avg: "$lighthouse.accessibility" }, bestPractices: { $avg: "$lighthouse.bestPractices" }, seo: { $avg: "$lighthouse.seo" }, @@ -78,6 +127,39 @@ class StatsAggregationService implements IStatsAggregationService { tbt: { $avg: "$lighthouse.audits.tbt.score" }, }, }, + { + $project: { + teamId: 1, + type: 1, + count: 1, + avgResponseTime: 1, + upChecks: 1, + downChecks: 1, + avgResponseTimeUp: { + $cond: [ + { $gt: ["$upChecks", 0] }, + { $divide: ["$sumResponseUp", "$upChecks"] }, + null, + ], + }, + avgResponseTimeDown: { + $cond: [ + { $gt: ["$downChecks", 0] }, + { $divide: ["$sumResponseDown", "$downChecks"] }, + null, + ], + }, + accessibility: 1, + bestPractices: 1, + seo: 1, + performance: 1, + cls: 1, + si: 1, + fcp: 1, + lcp: 1, + tbt: 1, + }, + }, ]); // 3) Infrastructure @@ -85,6 +167,7 @@ class StatsAggregationService implements IStatsAggregationService { { $match: { ...matchWindow, "metadata.type": "infrastructure" } }, { $project: { + status: 1, responseTime: 1, createdAt: 1, "system.cpu.physical_core": 1, @@ -116,6 +199,14 @@ class StatsAggregationService implements IStatsAggregationService { type: { $first: "$metadata.type" }, count: { $sum: 1 }, avgResponseTime: { $avg: "$responseTime" }, + upChecks: { $sum: { $cond: [{ $eq: ["$status", "up"] }, 1, 0] } }, + downChecks: { $sum: { $cond: [{ $eq: ["$status", "down"] }, 1, 0] } }, + sumResponseUp: { + $sum: { $cond: [{ $eq: ["$status", "up"] }, "$responseTime", 0] }, + }, + sumResponseDown: { + $sum: { $cond: [{ $eq: ["$status", "down"] }, "$responseTime", 0] }, + }, physicalCores: { $last: "$system.cpu.physical_core" }, logicalCores: { $last: "$system.cpu.logical_core" }, frequency: { $avg: "$system.cpu.frequency" }, @@ -142,6 +233,22 @@ class StatsAggregationService implements IStatsAggregationService { type: 1, count: 1, avgResponseTime: 1, + upChecks: 1, + downChecks: 1, + avgResponseTimeUp: { + $cond: [ + { $gt: ["$upChecks", 0] }, + { $divide: ["$sumResponseUp", "$upChecks"] }, + null, + ], + }, + avgResponseTimeDown: { + $cond: [ + { $gt: ["$downChecks", 0] }, + { $divide: ["$sumResponseDown", "$downChecks"] }, + null, + ], + }, cpu: { physical_core: "$physicalCores", logical_core: "$logicalCores", @@ -291,6 +398,10 @@ class StatsAggregationService implements IStatsAggregationService { finalized: false, count: d.count ?? 0, avgResponseTime: d.avgResponseTime ?? 0, + upChecks: d.upChecks ?? 0, + downChecks: d.downChecks ?? 0, + avgResponseTimeUp: (d.avgResponseTimeUp ?? null) as any, + avgResponseTimeDown: (d.avgResponseTimeDown ?? null) as any, accessibility: d.accessibility, bestPractices: d.bestPractices, seo: d.seo, @@ -354,8 +465,12 @@ class StatsAggregationService implements IStatsAggregationService { type: string; // totals count: number; + upChecks: number; + downChecks: number; // base rt: Weighted; + rtUp: Weighted; + rtDown: Weighted; // pagespeed accessibility: Weighted; bestPractices: Weighted; @@ -400,7 +515,11 @@ class StatsAggregationService implements IStatsAggregationService { teamId: h.teamId as any, type: h.type, count: 0, + upChecks: 0, + downChecks: 0, rt: { sum: 0, w: 0 }, + rtUp: { sum: 0, w: 0 }, + rtDown: { sum: 0, w: 0 }, accessibility: { sum: 0, w: 0 }, bestPractices: { sum: 0, w: 0 }, seo: { sum: 0, w: 0 }, @@ -428,7 +547,13 @@ class StatsAggregationService implements IStatsAggregationService { const w = h.count ?? 0; g.count += w; + const up = (h as any).upChecks ?? 0; + const down = (h as any).downChecks ?? 0; + g.upChecks += up; + g.downChecks += down; addW(g.rt, h.avgResponseTime, w); + addW(g.rtUp, (h as any).avgResponseTimeUp, up); + addW(g.rtDown, (h as any).avgResponseTimeDown, down); // pagespeed addW(g.accessibility, (h as any).accessibility, w); @@ -601,7 +726,11 @@ class StatsAggregationService implements IStatsAggregationService { $set: { finalized: finalize, count: g.count, + upChecks: g.upChecks, + downChecks: g.downChecks, avgResponseTime: avg(g.rt) ?? 0, + avgResponseTimeUp: (avg(g.rtUp) ?? null) as any, + avgResponseTimeDown: (avg(g.rtDown) ?? null) as any, accessibility: avg(g.accessibility), bestPractices: avg(g.bestPractices), seo: avg(g.seo), diff --git a/server/src/services/infrastructure/JobGenerator.ts b/server/src/services/infrastructure/JobGenerator.ts index 6f366bcd6..11ebb32e6 100644 --- a/server/src/services/infrastructure/JobGenerator.ts +++ b/server/src/services/infrastructure/JobGenerator.ts @@ -144,7 +144,7 @@ class JobGenerator implements IJobGenerator { generateStatsAggregationJob = () => { return async () => { - // Get previous hour window in UTC + // Compute hour window in UTC based on flag const now = new Date(); const utcNow = new Date( Date.UTC( @@ -157,18 +157,37 @@ class JobGenerator implements IJobGenerator { 0 ) ); - const hourEnd = new Date( - Date.UTC( - now.getUTCFullYear(), - now.getUTCMonth(), - now.getUTCDate(), - now.getUTCHours(), - 0, - 0, - 0 - ) - ); - const hourStart = new Date(hourEnd.getTime() - 60 * 60 * 1000); + let hourStart: Date; + let hourEnd: Date; + if (config.STATS_USE_CURRENT_HOUR) { + // Current hour [hh:00, hh+1:00) + hourStart = new Date( + Date.UTC( + now.getUTCFullYear(), + now.getUTCMonth(), + now.getUTCDate(), + now.getUTCHours(), + 0, + 0, + 0 + ) + ); + hourEnd = new Date(hourStart.getTime() + 60 * 60 * 1000); + } else { + // Previous hour [hh-1:00, hh:00) + hourEnd = new Date( + Date.UTC( + now.getUTCFullYear(), + now.getUTCMonth(), + now.getUTCDate(), + now.getUTCHours(), + 0, + 0, + 0 + ) + ); + hourStart = new Date(hourEnd.getTime() - 60 * 60 * 1000); + } // Upsert hourly stats for the last full hour await this.statsAggregationService.upsertHourly(hourStart, hourEnd); @@ -199,3 +218,4 @@ class JobGenerator implements IJobGenerator { } export default JobGenerator; +import { config } from "@/config/index.js"; diff --git a/server/src/services/infrastructure/JobQueue.ts b/server/src/services/infrastructure/JobQueue.ts index c411accab..57dd19738 100644 --- a/server/src/services/infrastructure/JobQueue.ts +++ b/server/src/services/infrastructure/JobQueue.ts @@ -3,6 +3,7 @@ import { Monitor, IMonitor } from "@/db/models/index.js"; import Scheduler from "super-simple-scheduler"; import { IJobGenerator } from "./JobGenerator.js"; import { getChildLogger } from "@/logger/Logger.js"; +import { config } from "@/config/index.js"; const SERVICE_NAME = "JobQueue"; const logger = getChildLogger(SERVICE_NAME); @@ -86,7 +87,7 @@ export default class JobQueue implements IJobQueue { active: true, }); - // Stats aggregation job (hourly) + // Stats aggregation job (frequency depends on config) this.scheduler.addTemplate( "stats-aggregation-job", this.jobGenerator.generateStatsAggregationJob() @@ -98,6 +99,14 @@ export default class JobQueue implements IJobQueue { active: true, }); + // Trigger an immediate run to populate stats on startup + try { + const runNow = this.jobGenerator.generateStatsAggregationJob(); + await runNow(); + } catch (e) { + logger.warn("Initial stats aggregation run failed", e as any); + } + const monitors = await Monitor.find(); for (const monitor of monitors) { const randomOffset = 1000 + Math.random() * 1000;