feat(backend): add customUpCodes support for HTTP monitors
Adds a customUpCodes field to the monitor schema to allow users to specify which HTTP status codes should be treated as UP instead of DOWN. Includes core logic in HttpProvider and unit tests. Refs #3657
This commit is contained in:
1 parent
b9e8a6dbc8
commit
4e31499f78
8 files changed
+86
-8
No files matched your search
@@ -291,6 +291,10 @@ const MonitorSchema = new Schema<MonitorDocument>(
|
||||
ref: "Tag",
|
||||
},
|
||||
],
|
||||
customUpCodes: {
|
||||
type: [Number],
|
||||
default: [],
|
||||
},
|
||||
secret: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
@@ -456,6 +456,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
uptimePercentage: doc.uptimePercentage ?? undefined,
|
||||
notifications: notificationIds,
|
||||
tags: tagIds,
|
||||
customUpCodes: doc.customUpCodes ?? [],
|
||||
secret: doc.secret ?? undefined,
|
||||
cpuAlertThreshold: doc.cpuAlertThreshold,
|
||||
cpuAlertCounter: doc.cpuAlertCounter,
|
||||
@@ -520,6 +521,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
uptimePercentage: doc.uptimePercentage ?? undefined,
|
||||
notifications: notificationIds,
|
||||
tags: tagIds,
|
||||
customUpCodes: doc.customUpCodes ?? [],
|
||||
secret: doc.secret ?? undefined,
|
||||
cpuAlertThreshold: doc.cpuAlertThreshold,
|
||||
cpuAlertCounter: doc.cpuAlertCounter,
|
||||
|
||||
@@ -89,7 +89,7 @@ export class GeoChecksService implements IGeoChecksService {
|
||||
}
|
||||
|
||||
// Step 2: Poll for results
|
||||
const results = await this.globalPingService.pollForResults(measurementId);
|
||||
const results = await this.globalPingService.pollForResults(measurementId, undefined, monitor.customUpCodes ?? []);
|
||||
|
||||
if (results.length === 0) {
|
||||
// No successful results (all locations timed out or failed)
|
||||
|
||||
@@ -62,7 +62,7 @@ interface GlobalPingProbeResult {
|
||||
export interface IGlobalPingService {
|
||||
readonly serviceName: string;
|
||||
createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise<string | null>;
|
||||
pollForResults(measurementId: string, timeoutMs?: number): Promise<GeoCheckResult[]>;
|
||||
pollForResults(measurementId: string, timeoutMs?: number, customUpCodes?: number[]): Promise<GeoCheckResult[]>;
|
||||
}
|
||||
|
||||
export class GlobalPingService implements IGlobalPingService {
|
||||
@@ -119,7 +119,7 @@ export class GlobalPingService implements IGlobalPingService {
|
||||
}
|
||||
}
|
||||
|
||||
async pollForResults(measurementId: string, timeoutMs: number = MAX_POLL_TIMEOUT_MS): Promise<GeoCheckResult[]> {
|
||||
async pollForResults(measurementId: string, timeoutMs: number = MAX_POLL_TIMEOUT_MS, customUpCodes: number[] = []): Promise<GeoCheckResult[]> {
|
||||
const startTime = Date.now();
|
||||
|
||||
while (Date.now() - startTime < timeoutMs) {
|
||||
@@ -132,7 +132,7 @@ export class GlobalPingService implements IGlobalPingService {
|
||||
const measurement = response.body;
|
||||
|
||||
if (measurement.status === "finished") {
|
||||
const results = this.transformResults(measurement.results || []);
|
||||
const results = this.transformResults(measurement.results || [], customUpCodes);
|
||||
this.logger.debug({
|
||||
message: `GlobalPing measurement completed: ${measurementId}`,
|
||||
service: SERVICE_NAME,
|
||||
@@ -174,7 +174,7 @@ export class GlobalPingService implements IGlobalPingService {
|
||||
return [];
|
||||
}
|
||||
|
||||
private transformResults(probeResults: GlobalPingProbeResult[]): GeoCheckResult[] {
|
||||
private transformResults(probeResults: GlobalPingProbeResult[], customUpCodes: number[] = []): GeoCheckResult[] {
|
||||
const successfulResults: GeoCheckResult[] = [];
|
||||
|
||||
for (const probeResult of probeResults) {
|
||||
@@ -205,7 +205,8 @@ export class GlobalPingService implements IGlobalPingService {
|
||||
|
||||
successfulResults.push({
|
||||
location,
|
||||
status: probeResult.result.statusCode >= 200 && probeResult.result.statusCode < 300,
|
||||
status:
|
||||
customUpCodes.includes(probeResult.result.statusCode) || (probeResult.result.statusCode >= 200 && probeResult.result.statusCode < 300),
|
||||
statusCode: probeResult.result.statusCode,
|
||||
timings,
|
||||
});
|
||||
|
||||
@@ -31,11 +31,13 @@ export class HttpProvider implements IStatusProvider<HttpStatusPayload> {
|
||||
|
||||
private handleHttpError<T>(error: unknown, monitor: Monitor): MonitorStatusResponse<T> {
|
||||
if (error instanceof HTTPError || error instanceof RequestError) {
|
||||
const isCustomUp = error instanceof HTTPError && (monitor.customUpCodes ?? []).includes(error.response?.statusCode);
|
||||
|
||||
return {
|
||||
monitorId: monitor.id,
|
||||
teamId: monitor.teamId,
|
||||
type: monitor.type,
|
||||
status: false,
|
||||
status: isCustomUp,
|
||||
code: error.response?.statusCode ?? NETWORK_ERROR,
|
||||
message: error.message,
|
||||
responseTime: error.timings?.phases?.total ?? 0,
|
||||
@@ -106,7 +108,7 @@ export class HttpProvider implements IStatusProvider<HttpStatusPayload> {
|
||||
monitorId: monitor.id,
|
||||
teamId: monitor.teamId,
|
||||
type: monitor.type,
|
||||
status: response.ok && matchResult.ok,
|
||||
status: ((monitor.customUpCodes ?? []).includes(response.statusCode) || response.ok) && matchResult.ok,
|
||||
code: response.statusCode,
|
||||
message: matchResult.ok ? (response.statusMessage ?? "OK") : matchResult.message,
|
||||
responseTime: response.timings.phases.total ?? 0,
|
||||
|
||||
@@ -60,6 +60,7 @@ export interface Monitor {
|
||||
uptimePercentage?: number;
|
||||
notifications: string[];
|
||||
tags: string[];
|
||||
customUpCodes: number[];
|
||||
secret?: string;
|
||||
cpuAlertThreshold: number;
|
||||
cpuAlertCounter: number;
|
||||
|
||||
@@ -81,6 +81,7 @@ export const createMonitorBodyValidation = z
|
||||
tempAlertThreshold: z.number().optional(),
|
||||
notifications: z.array(z.string()).optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
customUpCodes: z.array(z.number()).default([]),
|
||||
secret: z.string().optional(),
|
||||
jsonPath: z.union([z.string(), z.literal("")]).optional(),
|
||||
expectedValue: z.union([z.string(), z.literal("")]).optional(),
|
||||
@@ -110,6 +111,7 @@ export const editMonitorBodyValidation = z
|
||||
interval: z.number().optional(),
|
||||
notifications: z.array(z.string()).optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
customUpCodes: z.array(z.number()).optional(),
|
||||
secret: z.string().optional(),
|
||||
ignoreTlsErrors: z.boolean().optional(),
|
||||
useAdvancedMatching: z.boolean().optional(),
|
||||
@@ -180,6 +182,7 @@ const importedMonitorSchema = z
|
||||
uptimePercentage: z.number().optional(),
|
||||
notifications: z.array(z.string()).default([]),
|
||||
tags: z.array(z.string()).default([]),
|
||||
customUpCodes: z.array(z.number()).default([]),
|
||||
secret: z.string().optional(),
|
||||
cpuAlertThreshold: z.number().default(100),
|
||||
cpuAlertCounter: z.number().default(5),
|
||||
@@ -241,6 +244,7 @@ export const monitorResponseSchema = z
|
||||
matchMethod: z.enum(MonitorMatchMethods).optional(),
|
||||
notifications: z.array(z.string()),
|
||||
tags: z.array(z.string()),
|
||||
customUpCodes: z.array(z.number()).optional(),
|
||||
secret: z.string().optional(),
|
||||
cpuAlertThreshold: z.number(),
|
||||
memoryAlertThreshold: z.number(),
|
||||
|
||||
@@ -345,4 +345,68 @@ describe("HttpProvider", () => {
|
||||
await expect(provider.handle(makeMonitor({ url: "" }))).rejects.toThrow("URL is required for HTTP monitor");
|
||||
});
|
||||
});
|
||||
|
||||
// ── customUpCodes ────────────────────────────────────────────────────
|
||||
|
||||
describe("customUpCodes", () => {
|
||||
it("returns status true when HTTPError status code is in customUpCodes", async () => {
|
||||
const err = new HTTPError("Unauthorized");
|
||||
(err as any).response = { statusCode: 401 };
|
||||
(err as any).timings = { phases: { total: 30 } };
|
||||
mockGot.mockRejectedValue(err);
|
||||
const { provider } = createProvider();
|
||||
|
||||
const result = await provider.handle(makeMonitor({ customUpCodes: [401] }));
|
||||
|
||||
expect(result.status).toBe(true);
|
||||
expect(result.code).toBe(401);
|
||||
});
|
||||
|
||||
it("returns status false when HTTPError status code is not in customUpCodes", async () => {
|
||||
const err = new HTTPError("Internal Server Error");
|
||||
(err as any).response = { statusCode: 500 };
|
||||
(err as any).timings = { phases: { total: 40 } };
|
||||
mockGot.mockRejectedValue(err);
|
||||
const { provider } = createProvider();
|
||||
|
||||
const result = await provider.handle(makeMonitor({ customUpCodes: [401, 403] }));
|
||||
|
||||
expect(result.status).toBe(false);
|
||||
expect(result.code).toBe(500);
|
||||
});
|
||||
|
||||
it("returns status false when customUpCodes is empty", async () => {
|
||||
const err = new HTTPError("Unauthorized");
|
||||
(err as any).response = { statusCode: 401 };
|
||||
(err as any).timings = { phases: { total: 25 } };
|
||||
mockGot.mockRejectedValue(err);
|
||||
const { provider } = createProvider();
|
||||
|
||||
const result = await provider.handle(makeMonitor({ customUpCodes: [] }));
|
||||
|
||||
expect(result.status).toBe(false);
|
||||
expect(result.code).toBe(401);
|
||||
});
|
||||
|
||||
it("treats non-2xx success response as up when status code is in customUpCodes", async () => {
|
||||
mockGot.mockResolvedValue(makeGotResponse({ ok: false, statusCode: 301, statusMessage: "Moved Permanently" }));
|
||||
const { provider } = createProvider();
|
||||
|
||||
const result = await provider.handle(makeMonitor({ customUpCodes: [301] }));
|
||||
|
||||
expect(result.status).toBe(true);
|
||||
expect(result.code).toBe(301);
|
||||
});
|
||||
|
||||
it("does not override matcher failure even when status code is in customUpCodes", async () => {
|
||||
mockGot.mockResolvedValue(makeGotResponse({ ok: false, statusCode: 301 }));
|
||||
const matcher = createMockMatcher({ ok: false, message: "Body mismatch" });
|
||||
const { provider } = createProvider(matcher);
|
||||
|
||||
const result = await provider.handle(makeMonitor({ customUpCodes: [301] }));
|
||||
|
||||
expect(result.status).toBe(false);
|
||||
expect(result.message).toBe("Body mismatch");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user