refactor: switch Globalping to dynamic tier-based location lookup

## Changes
- Remove per-monitor location selection — locations are now determined
  globally by the tier setting in admin settings (3, 6, or 15 locations)
- GlobalpingService reads tier from settings on each check run, so
  changing the tier in settings immediately affects all HTTP/ping monitors
- Add resolveLocationId() to map Globalping probe results back to
  configured location IDs for proper grouping
- Export LOCATION_LABELS for human-readable location names on the client
- Remove unused locations field from Zod schemas and form defaults
- Remove per-monitor location ConfigBox from CreateMonitor page
- Fix settingsController.updateAppSettings missing try/catch
- Fix MongoSettingsRepository duplicate findOneAndUpdate call
- Use typed LocationCheckData instead of Record<string, any>
- Remove unused REVERSE_LOCATION_MAP from globalpingService

## Benefits
- Changing the location tier in settings takes effect immediately for
  all monitors without needing to edit each one individually
- Cleaner separation of concerns — location configuration lives in
  settings, not scattered across monitors
- Human-readable location labels shown in uptime details tabs
This commit is contained in:
gorkem-bwl committed 2026-02-19 10:58:18 -05:00
1 parent 7646d11ea0
commit 1966134647
10 files changed
+92 -113

No files matched your search

-2
View File
@@ -37,7 +37,6 @@ export const useMonitorForm = ({
matchMethod: data?.matchMethod || "",
expectedValue: data?.expectedValue || "",
jsonPath: data?.jsonPath || "",
locations: data?.locations || [],
};
break;
case "ping":
@@ -45,7 +44,6 @@ export const useMonitorForm = ({
...base,
type: "ping",
url: data?.url || "",
locations: data?.locations || [],
};
break;
case "port":
-60
View File
@@ -27,18 +27,12 @@ import {
SliderWithLabel,
Dialog,
} from "@/Components/v2/inputs";
import Chip from "@mui/material/Chip";
import { useGet, usePost, usePatch, useDelete } from "@/Hooks/UseApi";
import { useMonitorForm } from "@/Hooks/useMonitorForm";
import type { Monitor, MonitorType, GamesMap } from "@/Types/Monitor";
import type { Notification } from "@/Types/Notification";
import type { MonitorFormData } from "@/Validation/monitor";
interface GlobalpingLocation {
id: string;
label: string;
}
interface GeneralSettingsConfig {
urlLabel: string;
urlPlaceholder: string;
@@ -152,10 +146,6 @@ const CreateMonitorPage = () => {
const { data: notifications } = useGet<Notification[]>("/notifications/team");
const { data: games } = useGet<GamesMap>("/monitors/games");
const { data: globalpingLocations } = useGet<GlobalpingLocation[]>(
"/monitors/globalping/locations"
);
const { schema, defaults } = useMonitorForm({
data: existingMonitor ?? null,
defaultType,
@@ -498,56 +488,6 @@ const CreateMonitorPage = () => {
}
/>
{/* Global locations - only for http and ping types */}
{(watchedType === "http" || watchedType === "ping") && globalpingLocations && globalpingLocations.length > 0 && (
<ConfigBox
title={t("pages.createMonitor.form.locations.title")}
subtitle={t("pages.createMonitor.form.locations.description")}
rightContent={
<Controller
name="locations"
control={control}
render={({ field }) => {
const locationOptions = (globalpingLocations ?? []).map((loc) => ({
...loc,
name: loc.label,
}));
const selectedLocations = locationOptions.filter((loc) =>
(field.value ?? []).includes(loc.id)
);
return (
<Stack spacing={theme.spacing(4)}>
<Autocomplete
multiple
options={locationOptions}
value={selectedLocations}
getOptionLabel={(option) => option.name}
onChange={(_: unknown, newValue: typeof locationOptions) => {
const ids = newValue.map((loc) => loc.id);
if (ids.length <= 5) {
field.onChange(ids);
}
}}
isOptionEqualToValue={(option, value) => option.id === value.id}
renderTags={(value, getTagProps) =>
value.map((option, index) => (
<Chip
{...getTagProps({ index })}
key={option.id}
label={option.name}
size="small"
/>
))
}
/>
</Stack>
);
}}
/>
}
/>
)}
{/* Alert Thresholds - only for hardware type */}
{generalSettingsConfig.showSecret && (
<ConfigBox
+7 -1
View File
@@ -113,6 +113,12 @@ const UptimeDetailsPage = () => {
const checks = checksData?.checks ?? [];
const checksCount = checksData?.checksCount ?? 0;
const { data: globalpingData } = useGet<{
locations: { id: string; label: string }[];
labels: Record<string, string>;
}>("/monitors/globalping/locations");
const locationLabels = globalpingData?.labels ?? {};
const locationChecks = monitorData?.locationChecks;
const locationKeys = useMemo(
() => (locationChecks ? Object.keys(locationChecks).sort() : []),
@@ -182,7 +188,7 @@ const UptimeDetailsPage = () => {
{locationKeys.map((loc) => (
<Tab
key={loc}
label={loc}
label={locationLabels[loc] ?? loc}
/>
))}
</Tabs>
-2
View File
@@ -31,14 +31,12 @@ const httpSchema = baseSchema.extend({
matchMethod: z.enum(["equal", "include", "regex", ""]).optional(),
expectedValue: z.string().optional(),
jsonPath: z.string().optional(),
locations: z.array(z.string()).max(5).optional(),
});
// Ping monitor schema
const pingSchema = baseSchema.extend({
type: z.literal("ping"),
url: z.string().min(1, "Host is required"),
locations: z.array(z.string()).max(5).optional(),
});
// Port monitor schema
+2 -2
View File
@@ -26,7 +26,7 @@ import {
} from "./controllerUtils.js";
import { AppError } from "@/utils/AppError.js";
import { IMonitorService } from "@/service/index.js";
import { getGlobalpingLocationsByTier } from "@/service/infrastructure/globalpingService.js";
import { getGlobalpingLocationsByTier, LOCATION_LABELS } from "@/service/infrastructure/globalpingService.js";
import type { ISettingsService } from "@/service/system/settingsService.js";
const SERVICE_NAME = "monitorController";
@@ -399,7 +399,7 @@ class MonitorController {
return res.status(200).json({
success: true,
msg: "Globalping locations retrieved successfully",
data: locations,
data: { locations, labels: LOCATION_LABELS },
});
} catch (error) {
next(error);
+12 -8
View File
@@ -60,15 +60,19 @@ class SettingsController {
};
updateAppSettings = async (req: Request, res: Response, next: NextFunction) => {
await updateAppSettingsBodyValidation.validateAsync(req.body);
try {
await updateAppSettingsBodyValidation.validateAsync(req.body);
const updatedSettings = await this.settingsService.updateDbSettings(req.body);
const returnSettings = this.buildAppSettings(updatedSettings);
return res.status(200).json({
success: true,
msg: "App settings updated successfully",
data: returnSettings,
});
const updatedSettings = await this.settingsService.updateDbSettings(req.body);
const returnSettings = this.buildAppSettings(updatedSettings);
return res.status(200).json({
success: true,
msg: "App settings updated successfully",
data: returnSettings,
});
} catch (error) {
next(error);
}
};
sendTestEmail = async (req: Request, res: Response, next: NextFunction) => {
@@ -83,10 +83,6 @@ class MongoSettingsRepository implements ISettingsRepository {
delete update.$unset;
}
await AppSettingsModel.findOneAndUpdate({}, update, {
upsert: true,
});
const updatedSettings = await AppSettingsModel.findOneAndUpdate({}, update, {
upsert: true,
new: true,
@@ -6,6 +6,7 @@ import type {
UptimeDetailsResult,
HardwareDetailsResult,
PageSpeedDetailsResult,
LocationCheckData,
GamesMap,
} from "@/types/monitor.js";
import type {
@@ -222,9 +223,9 @@ export class MonitorService implements IMonitorService {
throw new AppError({ message: `${monitor.type} monitors are not supported for uptime details`, status: 400 });
}
// Fetch location checks if monitor has locations configured
let locationChecks: Record<string, any> | undefined;
if (monitor.locations && monitor.locations.length > 0) {
// Fetch location checks if this is an HTTP or ping monitor
let locationChecks: Record<string, LocationCheckData> | undefined;
if (monitor.type === "http" || monitor.type === "ping") {
const locData = await this.checksRepository.findLocationChecksByMonitorId(
monitor.id,
start,
@@ -128,7 +128,7 @@ class SuperSimpleQueueHelper {
}
// Step 2b. Run Globalping checks (fire-and-forget, doesn't affect local check pipeline)
if (monitor.locations && monitor.locations.length > 0 && (monitor.type === "http" || monitor.type === "ping")) {
if (monitor.type === "http" || monitor.type === "ping") {
this.globalpingService
.runChecks(monitor)
.then((locationStatuses) => {
@@ -5,20 +5,23 @@ import type { MonitorStatusResponse } from "@/types/network.js";
const SERVICE_NAME = "GlobalpingService";
interface GlobalpingLocationOption {
id: string;
label: string;
}
// Maps location IDs to Globalping API location objects
const LOCATION_MAP: Record<string, Record<string, string>> = {
// Continent-level (used for 3 and 6 tier)
"north-america": { continent: "NA" },
europe: { continent: "EU" },
asia: { continent: "AS" },
"south-america": { continent: "SA" },
oceania: { continent: "OC" },
africa: { continent: "AF" },
// City-level (used for 15 tier)
"us-east": { country: "US", state: "VA" },
"us-west": { country: "US", state: "CA" },
canada: { country: "CA", city: "Toronto" },
"uk": { country: "GB", city: "London" },
uk: { country: "GB", city: "London" },
germany: { country: "DE", city: "Frankfurt" },
netherlands: { country: "NL", city: "Amsterdam" },
poland: { country: "PL", city: "Warsaw" },
@@ -32,29 +35,19 @@ const LOCATION_MAP: Record<string, Record<string, string>> = {
uae: { country: "AE", city: "Dubai" },
};
interface GlobalpingLocationOption {
id: string;
label: string;
}
// 3 locations: core regions
const LOCATIONS_TIER_3: GlobalpingLocationOption[] = [
{ id: "north-america", label: "North America" },
{ id: "europe", label: "Europe" },
{ id: "asia", label: "Asia" },
];
// 6 locations: all continents
const LOCATIONS_TIER_6: GlobalpingLocationOption[] = [
{ id: "north-america", label: "North America" },
{ id: "europe", label: "Europe" },
{ id: "asia", label: "Asia" },
...LOCATIONS_TIER_3,
{ id: "south-america", label: "South America" },
{ id: "oceania", label: "Oceania" },
{ id: "africa", label: "Africa" },
];
// 15 locations: city/country level
const LOCATIONS_TIER_15: GlobalpingLocationOption[] = [
{ id: "us-east", label: "US East (Virginia)" },
{ id: "us-west", label: "US West (California)" },
@@ -79,6 +72,14 @@ const LOCATIONS_BY_TIER: Record<number, GlobalpingLocationOption[]> = {
15: LOCATIONS_TIER_15,
};
// Build a lookup from location ID to human-readable label
const LOCATION_LABELS: Record<string, string> = {};
for (const tier of Object.values(LOCATIONS_BY_TIER)) {
for (const loc of tier) {
LOCATION_LABELS[loc.id] = loc.label;
}
}
export const getGlobalpingLocationsByTier = (tier: number): GlobalpingLocationOption[] => {
return LOCATIONS_BY_TIER[tier] ?? LOCATIONS_TIER_6;
};
@@ -117,25 +118,34 @@ class GlobalpingService {
return this.client;
}
/**
* Gets the current tier's location IDs from settings.
* This is the source of truth for which locations to check.
*/
private async getTierLocationIds(): Promise<string[]> {
const dbSettings = await this.settingsService.getDBSettings();
const tier = dbSettings.globalpingLocationsTier ?? 6;
return getGlobalpingLocationsByTier(tier).map((loc) => loc.id);
}
async runChecks(monitor: Monitor): Promise<MonitorStatusResponse[]> {
if (!monitor.locations || monitor.locations.length === 0) {
if (monitor.type !== "http" && monitor.type !== "ping") {
return [];
}
const client = await this.getClient();
if (!client) {
this.logger.warn({
message: "Globalping API key not configured, skipping location checks",
service: SERVICE_NAME,
method: "runChecks",
});
return [];
}
const locationIds = await this.getTierLocationIds();
if (locationIds.length === 0) {
return [];
}
const measurementType = monitor.type === "ping" ? "ping" : "http";
const monitorUrl = monitor.url ?? "";
// Parse target from monitor URL
let target: string;
try {
if (measurementType === "http") {
@@ -148,7 +158,7 @@ class GlobalpingService {
target = monitorUrl;
}
const locations = monitor.locations
const locations = locationIds
.map((loc) => LOCATION_MAP[loc])
.filter(Boolean);
@@ -199,7 +209,7 @@ class GlobalpingService {
for (const item of measurement.results || []) {
const probe = item.probe;
const result = item.result;
const locationLabel = this.getLocationLabel(probe?.continent, probe?.country);
const locationId = this.resolveLocationId(probe?.continent, probe?.country, locationIds);
if (measurementType === "http") {
results.push({
@@ -210,10 +220,9 @@ class GlobalpingService {
code: result.statusCode || 0,
message: result.statusCodeName || result.rawOutput || "",
responseTime: result.timings?.total ?? 0,
location: locationLabel,
location: locationId,
});
} else {
// Ping
const stats = result.stats;
results.push({
monitorId: monitor.id,
@@ -223,7 +232,7 @@ class GlobalpingService {
code: stats?.loss === 0 ? 200 : 5000,
message: stats?.loss === 0 ? "Ping successful" : `Packet loss: ${stats?.loss}%`,
responseTime: stats?.avg ?? 0,
location: locationLabel,
location: locationId,
});
}
}
@@ -240,11 +249,37 @@ class GlobalpingService {
}
}
private getLocationLabel(continent?: string, country?: string): string {
if (continent && country) {
return `${continent}-${country}`;
/**
* Maps a probe's continent/country back to a configured location ID.
* For city-level tiers, matches by country code.
* For continent-level tiers, matches by continent code.
*/
private resolveLocationId(continent?: string, country?: string, configuredIds?: string[]): string {
if (!configuredIds || configuredIds.length === 0) {
return continent && country ? `${continent}-${country}` : continent || country || "unknown";
}
return continent || country || "unknown";
// Try country-level match first (for tier 15)
if (country) {
for (const id of configuredIds) {
const loc = LOCATION_MAP[id];
if (loc?.country === country) {
return id;
}
}
}
// Fall back to continent-level match (for tier 3/6)
if (continent) {
for (const id of configuredIds) {
const loc = LOCATION_MAP[id];
if (loc?.continent === continent) {
return id;
}
}
}
return continent && country ? `${continent}-${country}` : continent || country || "unknown";
}
async getLimits(): Promise<any | null> {
@@ -265,4 +300,5 @@ class GlobalpingService {
}
}
export { LOCATION_LABELS };
export default GlobalpingService;