Sextant
← Playground

Latency Percentiles

free

p50 / p95 / p99 request latency over time, auto-flagging the worst p99 deviation by z-score in a plain-language narrative.

Live preview

npx shadcn add @sextant/ops-latency-percentiles
State
Size
Mode

API Gateway · request latency

297 ms

p99 · latest

  • p50
  • p95
  • p99

p99 latency spiked to 536 on May 21, 2026, 4.1σ above a 285 baseline — driven by a deploy regression.

APM telemetry · updated every 60s

Source — this is the whole widget

"use client";

import * as React from "react";
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
import { format, parseISO } from "date-fns";

import { cn } from "@/lib/utils";
import { ChartCard } from "@/registry/default/widget-kit/chart-card";
import {
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
  type ChartConfig,
} from "@/components/ui/chart";
import {
  resolveNarrative,
  type NarrativeProp,
} from "@/registry/default/chart-narrative/chart-narrative";
import { detectAnomalies } from "@/registry/default/sextant-insights/stats";
import type { Insight } from "@/registry/default/sextant-insights/types";
import { StudioTarget, useStudio } from "@/registry/default/widget-kit/studio";
import type { SizeProp } from "@/registry/default/widget-kit/chart-size";
import type {
  ChartClassNames,
  SeriesConfig,
} from "@/registry/default/widget-kit/contract";
import { safeData } from "@/registry/default/sextant-contracts/contracts";
import { ChartStates } from "@/registry/default/chart-states/chart-states";
import { latencySeriesSchema, latencySeries, type LatencyPoint } from "./data";

// Re-export the data contract so consumers get the schema + types from a
// single import (no reaching into ./data).
export { latencySeriesSchema, type LatencyPoint } from "./data";

/** The three percentile lines, each independently recolorable / hideable. */
export type LatencySeries = {
  p50?: SeriesConfig;
  p95?: SeriesConfig;
  p99?: SeriesConfig;
};

type Props = {
  // ── Data ────────────────────────────────────────────────────────────────
  data?: LatencyPoint[];

  // ── Card chrome ───────────────────────────────────────────────────────────
  label?: string;
  attribution?: string;
  className?: string;
  classNames?: ChartClassNames;
  size?: SizeProp;

  // ── Customize ───────────────────────────────────────────────────────────
  series?: LatencySeries;
  /** Override latency formatting (axis, hero, tooltip). Default appends "ms". */
  valueFormatter?: (n: number) => string;
  /** Anomaly sensitivity on the p99 line, in σ. Default 2. */
  threshold?: number;
  /** Replace, hide (`false`), or fully render the one-line narrative. */
  narrative?: NarrativeProp;
};

const fmtMs = (n: number): string => `${Math.round(n)} ms`;

const PERCENTILES = [
  { key: "p50", label: "p50", color: "var(--chart-3)" },
  { key: "p95", label: "p95", color: "var(--chart-2)" },
  { key: "p99", label: "p99", color: "var(--chart-1)" },
] as const;

export function OpsLatencyPercentiles(props: Props) {
  // Validate consumer data at the boundary: a bad shape shows the error
  // state instead of crashing. The built-in demo data is trusted, so skip it.
  const parsed =
    props.data === undefined ? null : safeData(latencySeriesSchema, props.data);
  if (parsed && !parsed.ok) {
    return (
      <ChartStates status="error" shape="line" error={{ message: parsed.error }}>
        {null}
      </ChartStates>
    );
  }
  return <OpsLatencyPercentilesImpl {...props} />;
}

function OpsLatencyPercentilesImpl({
  data = latencySeries,
  label,
  attribution,
  className,
  classNames,
  size = "auto",
  series,
  valueFormatter,
  threshold = 2,
  narrative,
}: Props) {
  const studio = useStudio();
  const title = studio.text("title", label ?? "API Gateway · request latency", {
    label: "Chart title",
  });
  const fmt = valueFormatter ?? fmtMs;

  // Highest-|z| p99 reading drives the narrative.
  const insight = React.useMemo<Insight | undefined>(() => {
    const anomalies = detectAnomalies(data, (r) => r.p99, threshold);
    if (anomalies.length === 0) return undefined;
    const top = anomalies.reduce((b, c) => (Math.abs(c.z) > Math.abs(b.z) ? c : b));
    return {
      kind: "anomaly",
      metric: "p99 latency",
      sigma: top.z,
      date: format(parseISO(top.date), "MMM d, yyyy"),
      value: top.p99,
      baseline: data[0]?.p99 ?? 0,
      driver: "a deploy regression",
    };
  }, [data, threshold]);

  const latestP99 = data[data.length - 1]?.p99 ?? 0;

  const config = Object.fromEntries(
    PERCENTILES.map((p) => [
      p.key,
      {
        label: series?.[p.key]?.label ?? p.label,
        color: series?.[p.key]?.color ?? p.color,
      },
    ]),
  ) satisfies ChartConfig;

  return (
    <ChartCard.Root size={size} className={cn(className, classNames?.root)}>
      <ChartCard.Header className={classNames?.header}>
        <ChartCard.HeaderMain>
          <StudioTarget fieldKey="title" variant="inline">
            <ChartCard.Label>{title}</ChartCard.Label>
          </StudioTarget>
          <ChartCard.HeroNumber>{fmt(latestP99)}</ChartCard.HeroNumber>
          <p className="tabular text-sm text-muted-foreground">p99 · latest</p>
        </ChartCard.HeaderMain>
        <ChartCard.Legend
          items={PERCENTILES.filter((p) => !series?.[p.key]?.hidden).map((p) => ({
            label: config[p.key].label,
            color: config[p.key].color as string,
          }))}
        />
      </ChartCard.Header>

      {resolveNarrative(narrative, insight, { className: classNames?.narrative })}

      <ChartCard.Body className={classNames?.body}>
        <ChartContainer config={config} className="h-56 w-full">
          <LineChart data={data} margin={{ left: 0, right: 8, top: 8 }}>
            <CartesianGrid vertical={false} stroke="var(--chart-grid)" />
            <XAxis
              dataKey="date"
              tickFormatter={(v: string) => format(parseISO(v), "MMM d").toUpperCase()}
              tickLine={false}
              axisLine={false}
              tickMargin={10}
              fontSize={10}
              minTickGap={32}
              className="smallcaps"
            />
            <YAxis
              tickFormatter={(v: number) => `${v}`}
              tickLine={false}
              axisLine={false}
              width={40}
              fontSize={10}
              className="smallcaps tabular"
            />
            <ChartTooltip
              cursor={{
                stroke: "var(--foreground)",
                strokeOpacity: 0.25,
                strokeDasharray: "2 4",
              }}
              content={
                <ChartTooltipContent
                  labelFormatter={(v) => format(parseISO(String(v)), "MMM d, yyyy")}
                  formatter={(value, name, item) => (
                    <>
                      <span
                        aria-hidden
                        className="h-2.5 w-2.5 shrink-0 rounded-[2px]"
                        style={{ backgroundColor: (item as { color?: string }).color }}
                      />
                      <span className="flex flex-1 items-center justify-between gap-3 leading-none">
                        <span className="text-muted-foreground">
                          {config[name as string]?.label ?? String(name)}
                        </span>
                        <span className="font-mono font-medium text-foreground tabular-nums">
                          {fmt(Number(value))}
                        </span>
                      </span>
                    </>
                  )}
                />
              }
            />
            {PERCENTILES.filter((p) => !series?.[p.key]?.hidden).map((p) => (
              <Line
                key={p.key}
                dataKey={p.key}
                type="monotone"
                stroke={config[p.key].color as string}
                strokeWidth={p.key === "p99" ? 1.75 : 1.25}
                dot={false}
              />
            ))}
          </LineChart>
        </ChartContainer>
      </ChartCard.Body>

      <ChartCard.Footnote className={classNames?.footnote}>
        {attribution ?? "APM telemetry · updated every 60s"}
      </ChartCard.Footnote>
    </ChartCard.Root>
  );
}