Merge pull request #3385 from bluewave-labs/feat/globalping-ping-type

feat: globalping ping type
This commit is contained in:
Alexander Holliday authored and GitHub committed 2026-03-06 11:10:45 -08:00
commit ad427dc765
10 files changed
+97 -58

No files matched your search

+7 -2
View File
@@ -32,7 +32,12 @@ import {
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
import { useGet, usePost, usePatch, useDelete } from "@/Hooks/UseApi";
import { useMonitorForm } from "@/Hooks/useMonitorForm";
import type { Monitor, MonitorType, GamesMap } from "@/Types/Monitor";
import {
type Monitor,
type MonitorType,
type GamesMap,
supportsGeoCheck,
} from "@/Types/Monitor";
import type { Notification } from "@/Types/Notification";
import type { MonitorFormData } from "@/Validation/monitor";
@@ -885,7 +890,7 @@ const CreateMonitorPage = () => {
/>
)}
{watchedType === "http" && (
{supportsGeoCheck(watchedType) && (
<ConfigBox
title={t("pages.createMonitor.form.geoChecks.title")}
subtitle={t("pages.createMonitor.form.geoChecks.description")}
+3 -3
View File
@@ -20,7 +20,7 @@ import { useState, useMemo } from "react";
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import { useGet } from "@/Hooks/UseApi";
import type { MonitorDetailsResponse } from "@/Types/Monitor";
import { type MonitorDetailsResponse, supportsGeoCheck } from "@/Types/Monitor";
import type { ChecksResponse } from "@/Types/Check";
import type {
GeoChecksResult,
@@ -122,7 +122,7 @@ const UptimeDetailsPage = () => {
);
const geoChecksUrl = useMemo(() => {
if (!monitorId || monitor?.type !== "http" || !monitor?.geoCheckEnabled) {
if (!monitorId || !supportsGeoCheck(monitor?.type) || !monitor?.geoCheckEnabled) {
return null;
}
const params = new URLSearchParams();
@@ -141,7 +141,7 @@ const UptimeDetailsPage = () => {
// Fetch paginated geo checks for the table
const geoChecksTableUrl = useMemo(() => {
if (!monitorId || monitor?.type !== "http" || !monitor?.geoCheckEnabled) {
if (!monitorId || !supportsGeoCheck(monitor?.type) || !monitor?.geoCheckEnabled) {
return null;
}
const params = new URLSearchParams();
+9
View File
@@ -16,6 +16,15 @@ export const MonitorTypes = [
] as const;
export type MonitorType = (typeof MonitorTypes)[number];
export const GeoCheckSupportedTypes: readonly MonitorType[] = ["http", "ping"] as const;
export const supportsGeoCheck = (type: MonitorType | undefined): boolean => {
if (!type) {
return false;
}
return GeoCheckSupportedTypes.includes(type);
};
export const MonitorStatuses = [
"up",
"down",
@@ -1,17 +1,18 @@
import { IGeoChecksRepository } from "./IGeoChecksRepository.js";
import type { GeoCheck, GeoCheckMetadata, GeoCheckResult, GroupedGeoCheck, GeoContinent, FlatGeoCheck } from "@/types/geoCheck.js";
import type { GeoChecksQueryResult, FlatGeoChecksQueryResult } from "./IGeoChecksRepository.js";
import type { FlatGeoChecksQueryResult } from "./IGeoChecksRepository.js";
import { GeoCheckModel, type GeoCheckDocument } from "@/db/models/index.js";
import mongoose from "mongoose";
import { getDateForRange } from "@/utils/dataUtils.js";
import { ILogger } from "@/utils/logger.js";
const SERVICE_NAME = "GeoChecksRepository";
class MongoGeoChecksRepository implements IGeoChecksRepository {
static SERVICE_NAME = SERVICE_NAME;
private logger: any;
constructor(logger: any) {
private logger: ILogger;
constructor(logger: ILogger) {
this.logger = logger;
}
@@ -68,7 +68,7 @@ export class GeoChecksService implements IGeoChecksService {
}
// Step 1: Create measurement request
const measurementId = await this.globalPingService.createMeasurement(monitor.url, monitor.geoCheckLocations);
const measurementId = await this.globalPingService.createMeasurement(monitor.type, monitor.url, monitor.geoCheckLocations);
if (!measurementId) {
// GlobalPing API is down, skip this check
@@ -76,7 +76,6 @@ export class GeoChecksService implements IGeoChecksService {
message: "Skipping geo check due to API unavailability",
service: SERVICE_NAME,
method: "buildGeoCheck",
details: { monitorId: monitor.id },
});
return null;
}
@@ -90,7 +89,6 @@ export class GeoChecksService implements IGeoChecksService {
message: "No successful geo check results",
service: SERVICE_NAME,
method: "buildGeoCheck",
details: { monitorId: monitor.id, measurementId },
});
return null;
}
@@ -102,17 +100,16 @@ export class GeoChecksService implements IGeoChecksService {
message: `Geo check completed for monitor ${monitor.id}`,
service: SERVICE_NAME,
method: "buildGeoCheck",
details: { monitorId: monitor.id, resultsCount: results.length },
});
return geoCheck;
} catch (error: any) {
} catch (error: unknown) {
this.logger.error({
message: "Error executing geo check",
service: SERVICE_NAME,
method: "buildGeoCheck",
details: { monitorId: monitor.id, error: error.message },
stack: error.stack,
details: { monitorId: monitor.id, error: error instanceof Error ? error.message : "Unknown error" },
stack: error instanceof Error ? error.stack : undefined,
});
return null;
}
@@ -172,19 +169,9 @@ export class GeoChecksService implements IGeoChecksService {
const { sortOrder, dateRange, page, rowsPerPage, continent } = query;
const continents = continent ? (Array.isArray(continent) ? continent : [continent]) : undefined;
this.logger.debug({
message: "getGeoChecksByMonitor query params",
service: SERVICE_NAME,
method: "getGeoChecksByMonitor",
details: { continent, continents, query },
});
const parsedPage = page ? parseInt(page) : page;
const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage;
const result = await this.geoChecksRepository.findByMonitorId(monitorId, sortOrder, dateRange, parsedPage, parsedRowsPerPage, continents);
return result;
};
}
@@ -8,6 +8,7 @@ import type {
PageSpeedDetailsResult,
GamesMap,
} from "@/types/monitor.js";
import { supportsGeoCheck } from "@/types/monitor.js";
import type { GeoContinent } from "@/types/geoCheck.js";
import type {
IChecksRepository,
@@ -340,7 +341,7 @@ export class MonitorService implements IMonitorService {
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
}
if (monitor.type !== "http" || !monitor.geoCheckEnabled) {
if (!supportsGeoCheck(monitor.type) || !monitor.geoCheckEnabled) {
return { groupedGeoChecks: [] };
}
@@ -2,6 +2,7 @@ import { IMonitorsRepository } from "@/repositories/index.js";
import { ILogger } from "@/utils/logger.js";
import Scheduler from "super-simple-scheduler";
import { ISuperSimpleQueueHelper } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import { supportsGeoCheck } from "@/types/monitor.js";
const SERVICE_NAME = "JobQueue";
type QueueJobFailure = {
@@ -125,8 +126,13 @@ export class SuperSimpleQueue implements ISuperSimpleQueue {
data: monitor,
});
// Return early if we don't need geo checks
if (!supportsGeoCheck(monitor.type)) {
return;
}
// Add geo check job if enabled for HTTP monitors
if (monitor.geoCheckEnabled && monitor.type === "http") {
if (monitor.geoCheckEnabled) {
this.scheduler.addJob({
id: `${monitorId}-geo`,
template: "geo-check-job",
@@ -175,7 +181,7 @@ export class SuperSimpleQueue implements ISuperSimpleQueue {
// Handle geo check job lifecycle
const geoJobId = `${monitor.id}-geo`;
if (monitor.geoCheckEnabled && monitor.type === "http") {
if (monitor.geoCheckEnabled && supportsGeoCheck(monitor.type)) {
// Check if geo job exists
const existingGeoJob = await this.scheduler.getJob(geoJobId);
if (existingGeoJob) {
@@ -1,5 +1,6 @@
const SERVICE_NAME = "JobQueueHelper";
import type { Monitor } from "@/types/monitor.js";
import { supportsGeoCheck } from "@/types/monitor.js";
import { AppError } from "@/utils/AppError.js";
import { INetworkService, INotificationsService, IStatusService, IncidentService, type IGeoChecksService } from "@/service/index.js";
import type { StatusChangeResult } from "@/types/index.js";
@@ -280,19 +281,18 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getGeoCheckJob" });
}
if (monitor.type !== "http") {
if (!monitor.geoCheckEnabled) {
return;
}
if (!supportsGeoCheck(monitor.type)) {
this.logger.debug({
message: `Monitor ${monitorId} is not HTTP type, skipping geo check`,
message: `Monitor ${monitorId} type does not support geo checks, skipping`,
service: SERVICE_NAME,
method: "getGeoCheckJob",
});
return;
}
if (!monitor.geoCheckEnabled) {
return;
}
if (!monitor.geoCheckLocations || monitor.geoCheckLocations.length === 0) {
this.logger.warn({
message: `No geo check locations configured for monitor ${monitorId}`,
@@ -1,4 +1,6 @@
import type { GeoContinent, GeoCheckResult, GeoCheckTimings, GeoCheckLocation } from "@/types/geoCheck.js";
import { supportsGeoCheck } from "@/types/monitor.js";
import { MonitorType } from "@/types/index.js";
import type { ILogger } from "@/utils/logger.js";
import got from "got";
@@ -8,7 +10,7 @@ const POLL_INTERVAL_MS = 2000;
const MAX_POLL_TIMEOUT_MS = 30000;
interface GlobalPingMeasurementRequest {
type: "http";
type: MonitorType;
target: string;
locations: Array<{ continent: GeoContinent }>;
limit: number;
@@ -44,13 +46,22 @@ interface GlobalPingProbeResult {
firstByte: number;
download: number;
};
stats?: {
min: number;
max: number;
avg: number;
total: number;
loss: number;
rcv: number;
drop: number;
};
rawOutput?: string;
};
}
export interface IGlobalPingService {
readonly serviceName: string;
createMeasurement(url: string, locations: GeoContinent[]): Promise<string | null>;
createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise<string | null>;
pollForResults(measurementId: string, timeoutMs?: number): Promise<GeoCheckResult[]>;
}
@@ -67,13 +78,16 @@ export class GlobalPingService implements IGlobalPingService {
return GlobalPingService.SERVICE_NAME;
}
async createMeasurement(url: string, locations: GeoContinent[]): Promise<string | null> {
async createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise<string | null> {
try {
if (!supportsGeoCheck(monitorType)) {
throw new Error(`Unsupported monitor type for GlobalPing: ${monitorType}`);
}
// GlobalPing API expects target without protocol (http:// or https://)
const cleanTarget = url.replace(/^https?:\/\//, "");
const requestBody: GlobalPingMeasurementRequest = {
type: "http",
type: monitorType,
target: cleanTarget,
locations: locations.map((continent) => ({ continent })),
limit: locations.length,
@@ -88,10 +102,9 @@ export class GlobalPingService implements IGlobalPingService {
const measurementId = response.body.id;
this.logger.debug({
message: `Created GlobalPing measurement: ${measurementId}`,
message: `Created GlobalPing measurement: ${measurementId} for target: ${cleanTarget}`,
service: SERVICE_NAME,
method: "createMeasurement",
details: { measurementId, url, locations },
});
return measurementId;
@@ -165,8 +178,7 @@ export class GlobalPingService implements IGlobalPingService {
const successfulResults: GeoCheckResult[] = [];
for (const probeResult of probeResults) {
// Skip failed or timeout results
if (probeResult.result.status !== "finished" || !probeResult.result.statusCode || !probeResult.result.timings) {
if (probeResult.result.status !== "finished") {
continue;
}
@@ -180,23 +192,38 @@ export class GlobalPingService implements IGlobalPingService {
latitude: probeResult.probe.latitude,
};
const timings: GeoCheckTimings = {
total: probeResult.result.timings.total,
dns: probeResult.result.timings.dns,
tcp: probeResult.result.timings.tcp,
tls: probeResult.result.timings.tls,
firstByte: probeResult.result.timings.firstByte,
download: probeResult.result.timings.download,
};
// HTTP results have statusCode and timings, ping results have stats
if (probeResult.result.statusCode && probeResult.result.timings) {
const timings: GeoCheckTimings = {
total: probeResult.result.timings.total,
dns: probeResult.result.timings.dns,
tcp: probeResult.result.timings.tcp,
tls: probeResult.result.timings.tls,
firstByte: probeResult.result.timings.firstByte,
download: probeResult.result.timings.download,
};
const result: GeoCheckResult = {
location,
status: probeResult.result.statusCode >= 200 && probeResult.result.statusCode < 300,
statusCode: probeResult.result.statusCode,
timings,
};
successfulResults.push(result);
successfulResults.push({
location,
status: probeResult.result.statusCode >= 200 && probeResult.result.statusCode < 300,
statusCode: probeResult.result.statusCode,
timings,
});
} else if (probeResult.result.stats) {
successfulResults.push({
location,
status: probeResult.result.stats.loss === 0,
statusCode: probeResult.result.stats.loss === 0 ? 200 : 5000,
timings: {
total: probeResult.result.stats.avg,
dns: 0,
tcp: 0,
tls: 0,
firstByte: 0,
download: 0,
},
});
}
}
return successfulResults;
+3
View File
@@ -6,6 +6,9 @@ export type { GeoContinent } from "@/types/geoCheck.js";
export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "unknown"] as const;
export type MonitorType = (typeof MonitorTypes)[number];
export const GeoCheckSupportedTypes: readonly MonitorType[] = ["http", "ping"] as const;
export const supportsGeoCheck = (type: MonitorType): boolean => GeoCheckSupportedTypes.includes(type);
export const MonitorStatuses = ["up", "down", "paused", "initializing", "maintenance", "breached"] as const;
export type MonitorStatus = (typeof MonitorStatuses)[number];