feat: adding custom/default user agent support for http monitors

This commit is contained in:
mannilakash committed 2026-04-01 17:27:28 -04:00
1 parent 4222d39a8b
commit babe5ae177
19 files changed
+104 -4

No files matched your search

+1
View File
@@ -40,6 +40,7 @@ export const useMonitorForm = ({
matchMethod: data?.matchMethod || "",
expectedValue: data?.expectedValue || "",
jsonPath: data?.jsonPath || "",
customUserAgent: data?.customUserAgent || "",
};
break;
case "ping":
+1
View File
@@ -40,6 +40,7 @@ export const useSettingsForm = ({ data = null }: UseSettingsFormOptions = {}) =>
: 80,
},
checkTTL: data?.checkTTL ?? 30,
defaultUserAgent: data?.defaultUserAgent || "",
pagespeedApiKey: "",
systemEmailPassword: "",
};
+25
View File
@@ -795,6 +795,31 @@ const CreateMonitorPage = () => {
/>
)}
{watchedType === "http" && (
<ConfigBox
title={t("pages.createMonitor.form.userAgent.title")}
subtitle={t("pages.createMonitor.form.userAgent.description")}
rightContent={
<Controller
name="customUserAgent"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
value={field.value ?? ""}
type="text"
fieldLabel={t("pages.createMonitor.form.userAgent.option.label")}
placeholder={t("pages.createMonitor.form.userAgent.option.placeholder")}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
}
/>
)}
{watchedType === "http" && (
<ConfigBox
title={t("pages.createMonitor.form.advanced.title")}
+24
View File
@@ -425,6 +425,30 @@ export const SettingsPage = () => {
}
/>
{/* Default User-Agent - Admin Only */}
{isAdmin && (
<ConfigBox
title={t("pages.settings.form.userAgent.title")}
subtitle={t("pages.settings.form.userAgent.description")}
rightContent={
<Controller
name="defaultUserAgent"
control={form.control}
render={({ field, fieldState }) => (
<TextField
{...field}
value={field.value ?? ""}
fieldLabel={t("pages.settings.form.userAgent.option.label")}
placeholder={t("pages.settings.form.userAgent.option.placeholder")}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
}
/>
)}
{/* Clear All Stats */}
{isAdmin && (
<ConfigBox
+1
View File
@@ -73,6 +73,7 @@ export interface Monitor {
gameId?: string;
grpcServiceName?: string;
group: string | null;
customUserAgent?: string;
geoCheckEnabled?: boolean;
geoCheckLocations?: GeoContinent[];
geoCheckInterval?: number;
+1
View File
@@ -21,6 +21,7 @@ export interface Settings {
systemEmailRequireTLS: boolean;
systemEmailRejectUnauthorized: boolean;
showURL: boolean;
defaultUserAgent?: string;
singleton: boolean;
globalThresholds?: SettingsThresholds;
createdAt: string;
+1
View File
@@ -38,6 +38,7 @@ const httpSchema = baseSchema.extend({
matchMethod: z.enum(["equal", "include", "regex", ""]).optional(),
expectedValue: z.string().optional(),
jsonPath: z.string().optional(),
customUserAgent: z.string().max(500).optional(),
});
// Ping monitor schema
+5
View File
@@ -12,6 +12,11 @@ export const settingsSchema = z.object({
systemEmailSecure: z.boolean().optional(),
systemEmailPool: z.boolean().optional(),
showURL: z.boolean().optional(),
defaultUserAgent: z
.string()
.max(500)
.transform((val) => (val.trim() === "" ? null : val.trim()))
.optional(),
checkTTL: z
.number()
.int()
+16
View File
@@ -527,6 +527,14 @@
},
"title": "TLS/SSL settings"
},
"userAgent": {
"title": "Custom User-Agent",
"description": "Override the User-Agent header sent with HTTP requests for this monitor. Useful for identifying Checkmate in WAF logs. Leave blank to use the global default.",
"option": {
"label": "User-Agent",
"placeholder": "Checkmate/X.X (uptime monitor)"
}
},
"incidents": {
"description": "A sliding window is used to determine when a monitor goes down. The status of a monitor will only change when the percentage of checks in the sliding window meet the specified value.",
"option": {
@@ -1051,6 +1059,14 @@
}
}
},
"userAgent": {
"title": "Default User-Agent",
"description": "Set a default User-Agent header sent with all HTTP uptime monitor requests. Individual monitors can override this. Useful for identifying Checkmate in WAF logs and whitelists.",
"option": {
"label": "Default User-Agent",
"placeholder": "Checkmate/X.X (uptime monitor; your-instance)"
}
},
"stats": {
"title": "Monitor history",
"description": "Clear all monitoring history and statistics for your team. This action is irreversible.",
+1 -1
View File
@@ -248,7 +248,7 @@ export const initializeServices = async ({
// Network providers
const pingProvider = new PingProvider(ping);
const httpProvider = new HttpProvider(got, new AdvancedMatcher(jmespath));
const httpProvider = new HttpProvider(got, new AdvancedMatcher(jmespath), settingsService);
const pageSpeedProvider = new PageSpeedProvider(httpProvider, settingsService, logger);
const hardwareProvider = new HardwareProvider(httpProvider);
const dockerProvider = new DockerProvider(logger, Docker);
+1
View File
@@ -36,6 +36,7 @@ const AppSettingsSchema = new Schema<AppSettingsDocument>(
systemEmailRequireTLS: { type: Boolean, default: false },
systemEmailRejectUnauthorized: { type: Boolean, default: true },
showURL: { type: Boolean, default: false },
defaultUserAgent: { type: String },
singleton: { type: Boolean, required: true, unique: true, default: true },
version: { type: Number, default: 1 },
globalThresholds: { type: thresholdsSchema },
+3
View File
@@ -339,6 +339,9 @@ const MonitorSchema = new Schema<MonitorDocument>(
return value && value.trim() ? value.trim() : null;
},
},
customUserAgent: {
type: String,
},
geoCheckEnabled: {
type: Boolean,
default: false,
@@ -387,6 +387,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
gameId: doc.gameId ?? undefined,
grpcServiceName: doc.grpcServiceName ?? undefined,
group: doc.group ?? null,
customUserAgent: doc.customUserAgent ?? undefined,
recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)),
geoCheckEnabled: doc.geoCheckEnabled ?? false,
geoCheckLocations: doc.geoCheckLocations ?? [],
@@ -446,6 +447,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
gameId: doc.gameId ?? undefined,
grpcServiceName: doc.grpcServiceName ?? undefined,
group: doc.group ?? null,
customUserAgent: doc.customUserAgent ?? undefined,
recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)),
geoCheckEnabled: doc.geoCheckEnabled ?? false,
geoCheckLocations: doc.geoCheckLocations ?? [],
@@ -38,6 +38,7 @@ class MongoSettingsRepository implements ISettingsRepository {
systemEmailRequireTLS: doc.systemEmailRequireTLS ?? false,
systemEmailRejectUnauthorized: doc.systemEmailRejectUnauthorized ?? true,
showURL: doc.showURL ?? false,
defaultUserAgent: doc.defaultUserAgent ?? undefined,
singleton: doc.singleton,
version: doc.version ?? 1,
globalThresholds: doc.globalThresholds ?? undefined,
@@ -7,13 +7,15 @@ import { Agent as HttpsAgent } from "https";
import { Monitor, MonitorType } from "@/types/monitor.js";
import { NETWORK_ERROR } from "@/service/infrastructure/network/utils.js";
import CacheableLookup from "cacheable-lookup";
import { ISettingsService } from "@/service/system/settingsService.js";
export class HttpProvider implements IStatusProvider<HttpStatusPayload> {
readonly type = "http";
constructor(
private got: Got,
private advancedMatcher: IAdvancedMatcher
private advancedMatcher: IAdvancedMatcher,
private settingsService: ISettingsService
) {
const cacheable = new CacheableLookup({ maxTtl: 300, errorTtl: 30 });
this.got = got.extend({
@@ -57,14 +59,24 @@ export class HttpProvider implements IStatusProvider<HttpStatusPayload> {
}
async handle<T>(monitor: Monitor): Promise<MonitorStatusResponse<T>> {
const { url, secret, jsonPath, ignoreTlsErrors } = monitor;
const { url, secret, jsonPath, ignoreTlsErrors, customUserAgent } = monitor;
if (!url) {
throw new Error("URL is required for HTTP monitor");
}
let userAgent: string | undefined = customUserAgent;
if (!userAgent && monitor.type === "http") {
const dbSettings = await this.settingsService.getDBSettings();
userAgent = dbSettings?.defaultUserAgent ?? undefined;
}
const headers: Record<string, string> = {};
if (secret) headers["Authorization"] = `Bearer ${secret}`;
if (userAgent) headers["User-Agent"] = userAgent;
const options: Record<string, unknown> = {
headers: monitor.secret ? { Authorization: `Bearer ${secret}` } : undefined,
headers: Object.keys(headers).length > 0 ? headers : undefined,
};
options.agent = {
+1
View File
@@ -50,6 +50,7 @@ export interface Monitor {
gameId?: string;
grpcServiceName?: string;
group: string | null;
customUserAgent?: string;
geoCheckEnabled?: boolean;
geoCheckLocations?: GeoContinent[];
geoCheckInterval?: number;
+1
View File
@@ -31,6 +31,7 @@ export interface Settings {
systemEmailRequireTLS: boolean;
systemEmailRejectUnauthorized: boolean;
showURL: boolean;
defaultUserAgent?: string;
singleton: boolean;
version: number;
globalThresholds?: SettingsThresholds;
@@ -75,6 +75,7 @@ export const createMonitorBodyValidation = z.object({
grpcServiceName: z.union([z.string(), z.literal("")]).default(""),
selectedDisks: z.array(z.string()).optional(),
group: z.union([z.string().max(50).trim(), z.null(), z.literal("")]).optional(),
customUserAgent: z.string().max(500).optional(),
geoCheckEnabled: z.boolean().optional(),
geoCheckLocations: z.array(z.enum(GeoContinents)).optional(),
geoCheckInterval: z.number().min(300000).optional(),
@@ -104,6 +105,7 @@ export const editMonitorBodyValidation = z.object({
grpcServiceName: z.union([z.string(), z.literal("")]).optional(),
selectedDisks: z.array(z.string()).optional(),
group: z.union([z.string().max(50).trim(), z.null(), z.literal("")]).optional(),
customUserAgent: z.string().max(500).optional(),
geoCheckEnabled: z.boolean().optional(),
geoCheckLocations: z.array(z.enum(GeoContinents)).optional(),
geoCheckInterval: z.number().min(300000).optional(),
@@ -157,6 +159,7 @@ const importedMonitorSchema = z.object({
gameId: z.union([z.string(), z.literal("")]).optional(),
grpcServiceName: z.union([z.string(), z.literal("")]).default(""),
group: z.union([z.string().max(50).trim(), z.null()]).default(null),
customUserAgent: z.string().max(500).optional(),
geoCheckEnabled: z.boolean().default(false),
geoCheckLocations: z.array(z.enum(GeoContinents)).default([]),
geoCheckInterval: z.number().min(300000).default(300000),
@@ -20,6 +20,7 @@ export const updateAppSettingsBodyValidation = z
systemEmailTLSServername: z.string().nullable().optional(),
showURL: z.boolean().optional(),
defaultUserAgent: z.string().max(500).nullable().optional(),
systemEmailSecure: z.boolean().optional(),
systemEmailPool: z.boolean().optional(),
systemEmailIgnoreTLS: z.boolean().optional(),