From 8a608961e6b2c521bf1c8dedc6f1a98cc85f3a9f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 31 Jul 2026 10:31:39 -0700 Subject: [PATCH] remove server side normalization --- .../src/api/validation/monitorValidation.ts | 1 - server/src/domain/checks/check.type.ts | 4 - server/src/domain/monitors/monitor.service.ts | 3 +- .../status-pages/status-page.service.ts | 2 - .../domain/status-pages/status-page.type.ts | 3 +- server/src/utils/dataUtils.ts | 30 ------- server/test/unit/utils/dataUtils.test.ts | 78 +------------------ 7 files changed, 3 insertions(+), 118 deletions(-) diff --git a/server/src/api/validation/monitorValidation.ts b/server/src/api/validation/monitorValidation.ts index 86db942a5..441809c42 100644 --- a/server/src/api/validation/monitorValidation.ts +++ b/server/src/api/validation/monitorValidation.ts @@ -25,7 +25,6 @@ export const getMonitorByIdQueryValidation = z.object({ limit: z.coerce.number().optional(), dateRange: z.enum(DateRanges).optional(), numToDisplay: z.coerce.number().optional(), - normalize: booleanCoercion.optional(), continent: z.union([z.enum(GeoContinents), z.array(z.enum(GeoContinents))]).optional(), }); diff --git a/server/src/domain/checks/check.type.ts b/server/src/domain/checks/check.type.ts index d4c47cfb9..d6e60c3e3 100644 --- a/server/src/domain/checks/check.type.ts +++ b/server/src/domain/checks/check.type.ts @@ -168,10 +168,6 @@ export interface HasResponseTime { responseTime: number; } -export type NormalizedCheck = T & { - originalResponseTime: number; -}; - export type CheckSnapshot = Omit; export interface HardwareDiskStats { diff --git a/server/src/domain/monitors/monitor.service.ts b/server/src/domain/monitors/monitor.service.ts index 29046f83d..7a8478c5a 100644 --- a/server/src/domain/monitors/monitor.service.ts +++ b/server/src/domain/monitors/monitor.service.ts @@ -1,4 +1,3 @@ -import { NormalizeData } from "@/utils/dataUtils.js"; import { type Monitor } from "@/domain/monitors/monitor.type.js"; import type { MonitorType, @@ -364,7 +363,7 @@ export class MonitorService implements IMonitorService { const monitorsWithChecks = monitors.map((monitor: Monitor) => { const rawChecks = monitor.recentChecks ?? []; const isSnapshotType = snapshotOnlyRequest || snapshotTypes.includes(monitor.type); - const checks = isSnapshotType ? rawChecks.slice(0, 1) : NormalizeData(rawChecks, 10, 100); + const checks = isSnapshotType ? rawChecks.slice(0, 1) : rawChecks; monitor.recentChecks = checks; return monitor; }); diff --git a/server/src/domain/status-pages/status-page.service.ts b/server/src/domain/status-pages/status-page.service.ts index 834f87384..7a2a0724b 100644 --- a/server/src/domain/status-pages/status-page.service.ts +++ b/server/src/domain/status-pages/status-page.service.ts @@ -10,7 +10,6 @@ import { import { AppError } from "@/utils/AppError.js"; import { normalizeStatusPageDomain } from "@/utils/statusPageDomain.js"; import { Monitor } from "@/domain/monitors/monitor.type.js"; -import { NormalizeData } from "@/utils/dataUtils.js"; export interface IStatusPageService { createStatusPage(userId: string, teamId: string, image: Express.Multer.File | undefined, data: Partial): Promise; @@ -79,7 +78,6 @@ export class StatusPageService implements IStatusPageService { status: monitor.status, uptimePercentage: monitor.uptimePercentage, recentChecks: monitor.recentChecks, - checks: NormalizeData(monitor.recentChecks, 10, 100), }; if (showURL) { diff --git a/server/src/domain/status-pages/status-page.type.ts b/server/src/domain/status-pages/status-page.type.ts index 65f331ad9..e80087b39 100644 --- a/server/src/domain/status-pages/status-page.type.ts +++ b/server/src/domain/status-pages/status-page.type.ts @@ -1,5 +1,4 @@ import type { Monitor } from "@/domain/monitors/monitor.type.js"; -import type { NormalizedCheck, CheckSnapshot } from "@/domain/checks/check.type.js"; export const StatusPageTypes = ["uptime", "infrastructure"] as const; export type StatusPageType = (typeof StatusPageTypes)[number]; @@ -48,7 +47,7 @@ export interface StatusPage { } export type PublicStatusPageMonitor = Pick & - Partial> & { checks: NormalizedCheck[] }; + Partial>; export interface PublicStatusPagePayload { statusPage: StatusPage; diff --git a/server/src/utils/dataUtils.ts b/server/src/utils/dataUtils.ts index 2c15b943e..08e8de740 100755 --- a/server/src/utils/dataUtils.ts +++ b/server/src/utils/dataUtils.ts @@ -1,4 +1,3 @@ -import type { HasResponseTime, NormalizedCheck } from "@/domain/checks/check.type.js"; import { type DateRange } from "@/types/query.js"; export const getDateForRange = (dateRange: DateRange): Date => { @@ -32,32 +31,3 @@ export const getDateFormat = (dateRange: DateRange): string => { }; return formatLookup[dateRange]; }; - -const percentileBy = (arr: T[], percentile: number, value: (item: T) => number): number => { - const sorted = arr.slice().sort((a, b) => value(a) - value(b)); - const index = (percentile / 100) * (sorted.length - 1); - const lower = Math.floor(index); - const upper = lower + 1; - const weight = index % 1; - if (upper >= sorted.length) return value(sorted[lower]!); - return value(sorted[lower]!) * (1 - weight) + value(sorted[upper]!) * weight; -}; - -const rescale = (raw: number, min: number, max: number, rangeMin: number, rangeMax: number): number => { - const scaled = rangeMin + ((raw - min) * (rangeMax - rangeMin)) / (max - min); - return Math.max(rangeMin, Math.min(rangeMax, scaled)); -}; - -export const NormalizeData = (checks: T[], rangeMin: number, rangeMax: number): NormalizedCheck[] => { - const original = (check: T) => (typeof check.responseTime === "number" ? check.responseTime : 0); - if (checks.length <= 1) { - return checks.map((check) => ({ ...check, originalResponseTime: original(check) })); - } - const min = percentileBy(checks, 0, (c) => c.responseTime); - const max = percentileBy(checks, 95, (c) => c.responseTime); - return checks.map((check) => ({ - ...check, - responseTime: rescale(check.responseTime, min, max, rangeMin, rangeMax), - originalResponseTime: original(check), - })); -}; diff --git a/server/test/unit/utils/dataUtils.test.ts b/server/test/unit/utils/dataUtils.test.ts index af9434bab..d89cc0762 100644 --- a/server/test/unit/utils/dataUtils.test.ts +++ b/server/test/unit/utils/dataUtils.test.ts @@ -1,18 +1,7 @@ import { describe, expect, it } from "@jest/globals"; -import { getDateForRange, getDateFormat, NormalizeData } from "../../../src/utils/dataUtils.ts"; +import { getDateForRange, getDateFormat } from "../../../src/utils/dataUtils.ts"; import { DateRanges, type DateRange } from "../../../src/types/query.ts"; -// ── Helpers ────────────────────────────────────────────────────────────────── - -const makeCheck = (responseTime: number, overrides?: Record) => ({ - id: `check-${responseTime}`, - status: true, - responseTime, - ...overrides, -}); - -// ── Tests ──────────────────────────────────────────────────────────────────── - describe("getDateForRange", () => { it.each([ ["recent", 2 * 60 * 60 * 1000], @@ -53,68 +42,3 @@ describe("getDateFormat", () => { } }); }); - -describe("NormalizeData", () => { - // Fixture math: p0 of [100..500] is 100; p95 interpolates between the 4th and 5th - // sorted values at weight 0.8 → 400*0.2 + 500*0.8 = 480. Values rescale into - // [10, 100] via 10 + (rt - 100) * 90 / 380, clamped. - it("rescales responseTime into [rangeMin, rangeMax] anchored to the p0–p95 window", () => { - const checks = [100, 200, 300, 400, 500].map((rt) => makeCheck(rt)); - - const result = NormalizeData(checks, 10, 100); - - expect(result[0]!.responseTime).toBe(10); - expect(result[1]!.responseTime).toBeCloseTo(33.6842, 4); - expect(result[2]!.responseTime).toBeCloseTo(57.3684, 4); - expect(result[3]!.responseTime).toBeCloseTo(81.0526, 4); - }); - - it("clamps values above the p95 anchor to rangeMax", () => { - const checks = [100, 200, 300, 400, 500].map((rt) => makeCheck(rt)); - - const result = NormalizeData(checks, 10, 100); - - expect(result[4]!.responseTime).toBe(100); - }); - - it("preserves the raw value in originalResponseTime and spreads other fields", () => { - const checks = [makeCheck(100, { message: "OK" }), makeCheck(500)]; - - const result = NormalizeData(checks, 10, 100); - - expect(result[0]!.originalResponseTime).toBe(100); - expect(result[1]!.originalResponseTime).toBe(500); - expect(result[0]).toMatchObject({ id: "check-100", status: true, message: "OK" }); - }); - - it("passes a single check through, only adding originalResponseTime", () => { - const result = NormalizeData([makeCheck(250)], 10, 100); - - expect(result).toEqual([{ id: "check-250", status: true, responseTime: 250, originalResponseTime: 250 }]); - }); - - it("returns an empty array for empty input", () => { - expect(NormalizeData([], 10, 100)).toEqual([]); - }); - - it("records originalResponseTime as 0 for a non-numeric responseTime (failed checks)", () => { - const checks = [makeCheck(undefined as unknown as number), makeCheck(100), makeCheck(200)]; - - const result = NormalizeData(checks, 10, 100); - - expect(result[0]!.originalResponseTime).toBe(0); - }); - - // Characterization, not endorsement: when every value is equal the p0–p95 window - // has zero width and the rescale divides by zero. Fixing this is a planned - // follow-up (docs/planning/dataUtils-generic-normalization-refactor.md); this - // test pins the current behavior so the fix is a deliberate, visible change. - it("produces NaN when all values are equal (division by zero — known edge case)", () => { - const result = NormalizeData([makeCheck(50), makeCheck(50), makeCheck(50)], 10, 100); - - result.forEach((check) => { - expect(check.responseTime).toBeNaN(); - expect(check.originalResponseTime).toBe(50); - }); - }); -});