diff --git a/client/src/Components/monitors/charts/HistogramDetails.tsx b/client/src/Components/monitors/charts/HistogramDetails.tsx
index bd78f3289..17849fadb 100644
--- a/client/src/Components/monitors/charts/HistogramDetails.tsx
+++ b/client/src/Components/monitors/charts/HistogramDetails.tsx
@@ -12,6 +12,7 @@ import {
import type { TooltipProps } from "recharts";
import type { NameType, ValueType } from "recharts/types/component/DefaultTooltipContent";
import Typography from "@mui/material/Typography";
+import prettyMilliseconds from "pretty-ms";
import { useSelector } from "react-redux";
import {
@@ -68,12 +69,14 @@ const ResponseTimeToolTip = ({
if (!active) return null;
const format = tooltipDateFormatLookup(range);
- console.log(payload);
const responseTime = Math.floor(payload[0]?.payload?.originalAvgResponseTime || 0);
return (
{formatDateWithTz(String(label), format, uiTimezone)}
- Response time: {responseTime} ms
+
+ Response time:{" "}
+ {prettyMilliseconds(responseTime, { formatSubMilliseconds: true, compact: true })}
+
);
};
@@ -95,7 +98,7 @@ export const HistogramDetails = ({
strokeWidth={1.5}
/>
}
- title="Response times"
+ title="Average response time (First byte)"
>
= {
+ avgDns: "DNS",
+ avgTcp: "TCP",
+ avgTls: "TLS",
+ avgRequest: "Request",
+ avgFirstByte: "First byte",
+ avgDownload: "Download",
+};
+
+type TimingPhaseToolTipProps = TooltipProps & {
+ range: string;
+ theme: Theme;
+ uiTimezone: string;
+};
+
+type XTickProps = {
+ x: number;
+ y: number;
+ payload: { value: string };
+ range: string;
+};
+
+const TimingPhasesToolTip = ({
+ active,
+ payload,
+ range,
+ theme,
+ uiTimezone,
+}: TimingPhaseToolTipProps) => {
+ if (!active || !payload?.length) return null;
+ const bucket = payload[0].payload as GroupedUptimeCheck;
+ const total = PHASE_KEYS.reduce((sum, key) => sum + bucket[key], 0);
+ const format = tooltipDateFormatLookup(range);
+ return (
+
+ {formatDateWithTz(bucket.bucketDate, format, uiTimezone)}
+ Total: {prettyMilliseconds(total)}
+ {PHASE_KEYS.map((key) => (
+
+
+
+ {PHASE_LABELS[key]}:{" "}
+ {prettyMilliseconds(bucket[key], {
+ formatSubMilliseconds: true,
+ compact: true,
+ })}
+
+
+ ))}
+
+ );
+};
+
+export const XTick = ({ x, y, payload, range }: XTickProps) => {
+ const format = tickDateFormatLookup(range);
+ const theme = useTheme();
+ const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
+ return (
+
+ {formatDateWithTz(payload?.value, format, uiTimezone)}
+
+ );
+};
+
export const HistogramDetailsStacked = ({
checks = [],
range,
@@ -21,83 +135,103 @@ export const HistogramDetailsStacked = ({
range: string;
}) => {
const theme = useTheme();
+ const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
- // Cap the y-domain at the p95 of bucket totals so a single timeout bucket
- // doesn't flatten every other bucket; spikes clip off the top instead.
+ // 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) : undefined;
+ return p95 > 0 ? Math.ceil(p95 * 2) : undefined;
}, [checks]);
- console.log(JSON.stringify(checks, null, 2));
-
return (
-
+ }
+ title="Timing phases"
>
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+ {PHASE_KEYS.map((key) => (
+
+
+
+
+ ))}
+
+
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+ {PHASE_KEYS.map((key) => (
+
+ ))}
+
+
+
);
};
diff --git a/client/src/Pages/Uptime/Details/index.tsx b/client/src/Pages/Uptime/Details/index.tsx
index 7aa0b1046..04d207d06 100644
--- a/client/src/Pages/Uptime/Details/index.tsx
+++ b/client/src/Pages/Uptime/Details/index.tsx
@@ -31,7 +31,10 @@ import type { RootState } from "@/Types/state";
import { formatDateWithTz } from "@/Utils/TimeUtils";
import { t } from "i18next";
import { Typography } from "@mui/material";
-import { HistogramDetailsStacked } from "@/Components/monitors/charts/HistogramDetailsStacked";
+import {
+ HistogramDetailsStacked,
+ PHASE_KEYS,
+} from "@/Components/monitors/charts/HistogramDetailsStacked";
const certificateDateFormat = "MMM D, YYYY h A";
@@ -77,6 +80,14 @@ const UptimeDetailsPage = () => {
const monitor = monitorData?.monitor;
const monitorStats = monitorDetailsData?.monitorStats ?? null;
+ const hasTimingPhases = useMemo(
+ () =>
+ (monitorData?.groupedChecks ?? []).some((check) =>
+ PHASE_KEYS.some((key) => check[key] > 0)
+ ),
+ [monitorData]
+ );
+
// Certificate fetch - only for HTTP monitors
const certificateUrl = useMemo(() => {
if (!monitorId || monitor?.type !== "http") {
@@ -225,13 +236,15 @@ const UptimeDetailsPage = () => {
/>
-
+ {hasTimingPhases && (
+
+ )}