denormalize resposne times
This commit is contained in:
1 parent
81aab35961
commit
e3075318b6
10 files changed
+31
-92
No files matched your search
@@ -0,0 +1,6 @@
|
||||
export const computeYAxisCap = (values: number[]): number | undefined => {
|
||||
const sorted = values.slice().sort((a, b) => a - b);
|
||||
if (sorted.length < 2) return undefined;
|
||||
const p95 = sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))];
|
||||
return p95 > 0 ? Math.ceil(p95 * 2) : undefined;
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
AreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
@@ -24,6 +25,8 @@ import { useTheme } from "@mui/material/styles";
|
||||
import type { Theme } from "@mui/material/styles";
|
||||
import type { GroupedCheck } from "@/Types/Check";
|
||||
import type { RootState } from "@/Types/state";
|
||||
import { useMemo } from "react";
|
||||
import { computeYAxisCap } from "@/Components/monitors/charts/ChartUtils";
|
||||
|
||||
type XTickProps = {
|
||||
x: number;
|
||||
@@ -69,7 +72,7 @@ const ResponseTimeToolTip = ({
|
||||
if (!active) return null;
|
||||
|
||||
const format = tooltipDateFormatLookup(range);
|
||||
const responseTime = Math.floor(payload[0]?.payload?.originalAvgResponseTime || 0);
|
||||
const responseTime = Math.floor(payload[0]?.payload?.avgResponseTime || 0);
|
||||
return (
|
||||
<BaseBox sx={{ py: theme.spacing(2), px: theme.spacing(4) }}>
|
||||
<Typography>{formatDateWithTz(String(label), format, uiTimezone)}</Typography>
|
||||
@@ -90,6 +93,10 @@ export const HistogramDetails = ({
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
|
||||
const yMax = useMemo(() => {
|
||||
const totals = checks.map((check) => check.avgResponseTime || 0);
|
||||
return computeYAxisCap(totals);
|
||||
}, [checks]);
|
||||
return (
|
||||
<BaseChart
|
||||
icon={
|
||||
@@ -132,6 +139,12 @@ export const HistogramDetails = ({
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<YAxis
|
||||
hide
|
||||
// scale="sqrt"
|
||||
domain={[0, yMax ?? "auto"]}
|
||||
allowDataOverflow
|
||||
/>
|
||||
<XAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { useSelector } from "react-redux";
|
||||
import { useMemo } from "react";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { SPACING } from "@/Utils/Theme/constants";
|
||||
import { computeYAxisCap } from "@/Components/monitors/charts/ChartUtils";
|
||||
|
||||
export const PHASE_KEYS = [
|
||||
"avgDns",
|
||||
@@ -140,12 +141,10 @@ export const HistogramDetailsStacked = ({
|
||||
// Cap the y domain at 2x the p95 of bucket totals, so typical stacks sit
|
||||
// around mid-height with headroom above
|
||||
const yMax = useMemo(() => {
|
||||
const totals = checks
|
||||
.map((check) => PHASE_KEYS.reduce((sum, key) => sum + check[key], 0))
|
||||
.sort((a, b) => a - b);
|
||||
if (totals.length < 2) return undefined;
|
||||
const p95 = totals[Math.min(totals.length - 1, Math.floor(totals.length * 0.95))];
|
||||
return p95 > 0 ? Math.ceil(p95 * 2) : undefined;
|
||||
const totals = checks.map((check) =>
|
||||
PHASE_KEYS.reduce((sum, key) => sum + check[key], 0)
|
||||
);
|
||||
return computeYAxisCap(totals);
|
||||
}, [checks]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -61,7 +61,6 @@ const UptimeDetailsPage = () => {
|
||||
}
|
||||
const params = new URLSearchParams();
|
||||
params.append("dateRange", dateRange);
|
||||
params.append("normalize", "true");
|
||||
return `/monitors/uptime/details/${monitorId}?${params.toString()}`;
|
||||
}, [monitorId, dateRange]);
|
||||
|
||||
|
||||
@@ -261,7 +261,3 @@ export interface HasResponseTime {
|
||||
export type NormalizedCheck<T extends HasResponseTime = Check> = T & {
|
||||
originalResponseTime: number;
|
||||
};
|
||||
|
||||
export type NormalizedUptimeCheck<T extends GroupedCheck = GroupedCheck> = T & {
|
||||
originalAvgResponseTime: number;
|
||||
};
|
||||
@@ -197,7 +197,6 @@ export const getUptimeDetailsByIdParamValidation = z.object({
|
||||
|
||||
export const getUptimeDetailsByIdQueryValidation = z.object({
|
||||
dateRange: z.enum(DateRanges),
|
||||
normalize: booleanCoercion.optional(),
|
||||
});
|
||||
|
||||
const importedMonitorSchema = z
|
||||
|
||||
@@ -172,10 +172,6 @@ export type NormalizedCheck<T extends HasResponseTime = Check> = T & {
|
||||
originalResponseTime: number;
|
||||
};
|
||||
|
||||
export type NormalizedUptimeCheck<T extends GroupedCheck = GroupedCheck> = T & {
|
||||
originalAvgResponseTime: number;
|
||||
};
|
||||
|
||||
export type CheckSnapshot = Omit<Check, "metadata" | "updatedAt">;
|
||||
|
||||
export interface HardwareDiskStats {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NormalizeData, NormalizeDataUptimeDetails } from "@/utils/dataUtils.js";
|
||||
import { NormalizeData } from "@/utils/dataUtils.js";
|
||||
import { type Monitor } from "@/domain/monitors/monitor.type.js";
|
||||
import type {
|
||||
MonitorType,
|
||||
@@ -193,9 +193,9 @@ export class MonitorService implements IMonitorService {
|
||||
return {
|
||||
monitorData: {
|
||||
monitor,
|
||||
groupedChecks: NormalizeDataUptimeDetails(checksData.groupedChecks, 10, 100),
|
||||
groupedUpChecks: NormalizeDataUptimeDetails(checksData.groupedUpChecks, 10, 100),
|
||||
groupedDownChecks: NormalizeDataUptimeDetails(checksData.groupedDownChecks, 10, 100),
|
||||
groupedChecks: checksData.groupedChecks,
|
||||
groupedUpChecks: checksData.groupedUpChecks,
|
||||
groupedDownChecks: checksData.groupedDownChecks,
|
||||
groupedAvgResponseTime: checksData.avgResponseTime,
|
||||
groupedUptimePercentage: checksData.uptimePercentage,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { GroupedCheck, HasResponseTime, NormalizedCheck, NormalizedUptimeCheck } from "@/domain/checks/check.type.js";
|
||||
import type { HasResponseTime, NormalizedCheck } from "@/domain/checks/check.type.js";
|
||||
import { type DateRange } from "@/types/query.js";
|
||||
|
||||
export const getDateForRange = (dateRange: DateRange): Date => {
|
||||
@@ -61,17 +61,3 @@ export const NormalizeData = <T extends HasResponseTime>(checks: T[], rangeMin:
|
||||
originalResponseTime: original(check),
|
||||
}));
|
||||
};
|
||||
|
||||
export const NormalizeDataUptimeDetails = <T extends GroupedCheck>(checks: T[], rangeMin: number, rangeMax: number): NormalizedUptimeCheck<T>[] => {
|
||||
const original = (check: T) => (typeof check.avgResponseTime === "number" ? check.avgResponseTime : 0);
|
||||
if (checks.length <= 1) {
|
||||
return checks.map((check) => ({ ...check, originalAvgResponseTime: original(check) }));
|
||||
}
|
||||
const min = percentileBy(checks, 0, (c) => c.avgResponseTime);
|
||||
const max = percentileBy(checks, 95, (c) => c.avgResponseTime);
|
||||
return checks.map((check) => ({
|
||||
...check,
|
||||
avgResponseTime: rescale(check.avgResponseTime, min, max, rangeMin, rangeMax),
|
||||
originalAvgResponseTime: original(check),
|
||||
}));
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, expect, it } from "@jest/globals";
|
||||
import { getDateForRange, getDateFormat, NormalizeData, NormalizeDataUptimeDetails } from "../../../src/utils/dataUtils.ts";
|
||||
import type { GroupedCheck } from "../../../src/domain/checks/check.type.ts";
|
||||
import { getDateForRange, getDateFormat, NormalizeData } from "../../../src/utils/dataUtils.ts";
|
||||
import { DateRanges, type DateRange } from "../../../src/types/query.ts";
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
@@ -12,13 +11,6 @@ const makeCheck = (responseTime: number, overrides?: Record<string, unknown>) =>
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const makeGrouped = (avgResponseTime: number, overrides?: Partial<GroupedCheck>): GroupedCheck => ({
|
||||
bucketDate: `bucket-${avgResponseTime}`,
|
||||
avgResponseTime,
|
||||
totalChecks: 5,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("getDateForRange", () => {
|
||||
@@ -126,50 +118,3 @@ describe("NormalizeData", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("NormalizeDataUptimeDetails", () => {
|
||||
it("rescales avgResponseTime into [rangeMin, rangeMax] anchored to the p0–p95 window", () => {
|
||||
const checks = [100, 200, 300, 400, 500].map((avg) => makeGrouped(avg));
|
||||
|
||||
const result = NormalizeDataUptimeDetails(checks, 10, 100);
|
||||
|
||||
expect(result[0]!.avgResponseTime).toBe(10);
|
||||
expect(result[1]!.avgResponseTime).toBeCloseTo(33.6842, 4);
|
||||
expect(result[4]!.avgResponseTime).toBe(100);
|
||||
});
|
||||
|
||||
it("preserves the raw value in originalAvgResponseTime and spreads other fields", () => {
|
||||
const checks = [makeGrouped(100), makeGrouped(500)];
|
||||
|
||||
const result = NormalizeDataUptimeDetails(checks, 10, 100);
|
||||
|
||||
expect(result[0]!.originalAvgResponseTime).toBe(100);
|
||||
expect(result[1]!.originalAvgResponseTime).toBe(500);
|
||||
expect(result[0]).toMatchObject({ bucketDate: "bucket-100", totalChecks: 5 });
|
||||
});
|
||||
|
||||
it("passes a single bucket through, only adding originalAvgResponseTime", () => {
|
||||
const result = NormalizeDataUptimeDetails([makeGrouped(250)], 10, 100);
|
||||
|
||||
expect(result).toEqual([{ bucketDate: "bucket-250", avgResponseTime: 250, totalChecks: 5, originalAvgResponseTime: 250 }]);
|
||||
});
|
||||
|
||||
it("returns an empty array for empty input", () => {
|
||||
expect(NormalizeDataUptimeDetails([], 10, 100)).toEqual([]);
|
||||
});
|
||||
|
||||
it("records originalAvgResponseTime as 0 for a null avgResponseTime ($avg over an empty bucket)", () => {
|
||||
const result = NormalizeDataUptimeDetails([makeGrouped(null as unknown as number)], 10, 100);
|
||||
|
||||
expect(result[0]!.originalAvgResponseTime).toBe(0);
|
||||
});
|
||||
|
||||
it("produces NaN when all values are equal (division by zero — known edge case)", () => {
|
||||
const result = NormalizeDataUptimeDetails([makeGrouped(50), makeGrouped(50)], 10, 100);
|
||||
|
||||
result.forEach((check) => {
|
||||
expect(check.avgResponseTime).toBeNaN();
|
||||
expect(check.originalAvgResponseTime).toBe(50);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user