Functional Weave
Code in TypeScript

monitor.incidents

Turn a series of up, degraded and down checks into incidents, ignoring runs too short to confirm.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 19 tests, run in TypeScript, Python and Rust.

What it does

Finds the incidents in a stored series of checks (`monitor.check-status`'s `Check`): each run of consecutive bad checks is one incident.

## Rules

For example

  • findIncidents(checks ×5, down, 1, 300) → ×1 two down checks in a row are one incident ending at the first up check
  • findIncidents(checks ×5, down, 2, 300) → ×1 a run exactly confirmChecks long is an incident
  • findIncidents(checks ×5, down, 3, 300) → a run shorter than confirmChecks is ignored as a flap

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

export function findIncidents(checks: readonly Check[], incidentStatus: CheckStatus, confirmChecks: number, now: number): readonly Incident[]
checksCheck[]in strictly ascending time order
incidentStatusCheckStatusdown: only down checks are bad; degraded: degraded or down checks are bad
confirmChecksinthow many bad checks in a row make an incident, at least 1
nowintUnix seconds, not before the last check; ends the duration of an ongoing incident
returnsIncident[]

The type it declares, generated into your project

/** One run of bad checks, from the first bad check to the first good one after it. */
export interface Incident {
  /** at of the first bad check */
  readonly start: number;
  /** at of the first good check after the run; null while ongoing */
  readonly end: number | null;
  /** (end, or now while ongoing) - start */
  readonly durationSeconds: number;
  /** down if any check in the run was down, else degraded */
  readonly worst: CheckStatus;
  /** how many bad checks the run holds */
  readonly checks: number;
}

Your code names it in one line, in the file that uses it

import { findIncidents } from "#fune/monitor.incidents@^1";
impl/typescript.ts · 47 lines · open · raw
import { type Check, type CheckStatus } from "./monitor_check_status_types.ts";
import { type Incident } from "./monitor_incidents_types.ts";

/**
 * The incidents in a check series. A run of consecutive bad checks is one
 * incident, from its first bad check to the first good check after it, but
 * only when it holds at least confirmChecks checks: a single failed probe is
 * noise, not an outage, and would otherwise page and skew MTTR.
 */
export function findIncidents(checks: readonly Check[], incidentStatus: CheckStatus, confirmChecks: number, now: number): readonly Incident[] {
  if (!Number.isSafeInteger(now)) throw new RangeError("now must be a whole number of seconds");
  if (incidentStatus !== "down" && incidentStatus !== "degraded") {
    throw new RangeError(`incidentStatus must be down or degraded, received ${String(incidentStatus)}`);
  }
  if (!Number.isSafeInteger(confirmChecks) || confirmChecks < 1) {
    throw new RangeError(`confirmChecks must be at least 1, received ${confirmChecks}`);
  }
  for (let i = 0; i < checks.length; i++) {
    const s = checks[i].status;
    if (s !== "up" && s !== "degraded" && s !== "down") throw new RangeError(`unknown check status: ${String(s)}`);
    if (i > 0 && checks[i].at <= checks[i - 1].at) {
      throw new RangeError(`checks must be in strictly ascending time order: ${checks[i].at} follows ${checks[i - 1].at}`);
    }
  }
  if (checks.length > 0 && now < checks[checks.length - 1].at) {
    throw new RangeError(`now ${now} is before the last check at ${checks[checks.length - 1].at}`);
  }
  const bad = (s: CheckStatus): boolean => s === "down" || (incidentStatus === "degraded" && s === "degraded");
  const out: Incident[] = [];
  let i = 0;
  while (i < checks.length) {
    if (!bad(checks[i].status)) { i++; continue; }
    const start = checks[i].at;
    let count = 0;
    let worst: CheckStatus = "degraded";
    while (i < checks.length && bad(checks[i].status)) {
      if (checks[i].status === "down") worst = "down";
      count++;
      i++;
    }
    const end = i < checks.length ? checks[i].at : null;
    if (count >= confirmChecks) {
      out.push({ start, end, durationSeconds: (end ?? now) - start, worst, checks: count });
    }
  }
  return out;
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:

fune add monitor.incidents
Download for TypeScript monitor.incidents-1.0.0-typescript.fune · 12,977 bytes sha256 33c6fdc3c53956ae65a76e5bcd34d92e8b3ebd7087d50a833c3ae714266b89e5

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./monitor.incidents-1.0.0-typescript.fune, or fetch it from a terminal with fune pull monitor.incidents@1.0.0:typescript.

The whole function, every language, is one file too: monitor.incidents-1.0.0.fune, 19,417 bytes, sha256 4664c6be7bf3e0139623ab73f3d74becc8b401bdee0de17e21fa7824a05124a8. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

// fune: before monitor.incidents

after — your function gets the result and the arguments, and returns the final result.

// fune: after monitor.incidents

replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.

// fune: replace monitor.check-status in monitor.incidents

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show monitor.incidents --steps.

// fune: step monitor.incidents after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
two down checks in a row are one incident ending at the first up check checks ×5, down, 1, 300 → ×1
a run exactly confirmChecks long is an incident checks ×5, down, 2, 300 → ×1
a run shorter than confirmChecks is ignored as a flap checks ×5, down, 3, 300 →
a run still bad at the last check is ongoing, measured to now checks ×3, down, 1, 200 → ×1
degraded mode joins degraded and down checks into one incident, worst down checks ×5, degraded, 1, 240 → ×1
down mode: a degraded check is not bad, so it ends the down incident checks ×5, down, 1, 240 → ×1
a degraded-only incident has worst degraded checks ×3, degraded, 1, 120 → ×1
short blips around a real outage are dropped, and an unconfirmed ongoing run too checks ×8, down, 2, 480 → ×1
no checks, no incidents , down, 1, 1,000 →
a series that starts bad starts its incident at the first check checks ×2, down, 1, 60 → ×1
Show the other 9 tests
CaseArgumentsExpected
all up has no incidents checks ×3, down, 1, 180 →
an ongoing incident with now at its only check lasts 0 seconds checks ×1, down, 1, 100 → ×1
two separate incidents come out in time order checks ×4, down, 1, 45 → ×2
checks out of time order are an error checks ×2, down, 1, 100 → error: checks must be in strictly ascending time order
an unknown check status is an error checks ×1, down, 1, 100 → error: unknown check status: offline
up is not an incident status checks ×5, up, 1, 300 → error: incidentStatus must be down or degraded, received up
confirmChecks 0 is an error checks ×5, down, 0, 300 → error: confirmChecks must be at least 1
now before the last check is an error checks ×5, down, 1, 200 → error: now 200 is before the last check at 240
a fractional now is an error checks ×5, down, 1, 300.5 → error: now must be a whole number of seconds

More from the author

- **Bad** depends on `incidentStatus`: with `down`, only `down` checks are bad (a slow site is not an outage); with `degraded`, `degraded` and `down` both are, and one incident can mix them. `up` is refused: an "incident of being up" is a configuration mistake. - A run counts only when it has at least `confirmChecks` checks. This is flap suppression, the same idea as "alert after N consecutive failures" in most uptime monitors: one lost probe does not make an outage, and would drag MTTR down if it did. Shorter runs are dropped entirely, including an unconfirmed run still in progress at the last check. - `start` is the `at` of the first bad check; `end` is the `at` of the first check after the run that is not bad (with `incidentStatus: down`, a `degraded` check ends a down incident). An incident still bad at the last check is ongoing: `end` is null and `durationSeconds` runs to `now`. - The start is the first check that *saw* the failure, not a guess at when it really began between two checks; and the end is the first check that saw recovery. So durations are accurate to the check interval, and err long. - `worst` is `down` if any check in the run was down, else `degraded`. - `checks` counts the bad checks in the run.

Incidents come out in time order and never overlap, which is what `monitor.mttr` expects.

## Errors

- `checks must be in strictly ascending time order` - `unknown check status: X` - `incidentStatus must be down or degraded, received up` - `confirmChecks must be at least 1` - `now 200 is before the last check at 240`: now ends ongoing incidents, so it cannot be earlier than the data. - `now must be a whole number of seconds`

Files

PathBytes
README.md1,863
impl/python.py2,276
impl/rust.rs3,891
impl/typescript.ts2,253
vectors.json5,284