Skip to content

fix(intelligence): score error-rate spikes against the rate's own variance - #265

Merged
vishr merged 1 commit into
mainfrom
fix/error-rate-zscore-denominator
Sep 21, 2026
Merged

vishr merged 1 commit into
mainfrom
fix/error-rate-zscore-denominator

Conversation

@vishr

@vishr vishr commented Sep 21, 2026

Copy link
Copy Markdown
Member

Follow-up to #264, which fixed the volume detector and named this as the
remaining reason error-rate detection was desensitised.

The z-score divides a difference of rates, so its denominator has to be
the scatter of the rate from bucket to bucket. It was STDDEV of a
per-span 0/1 indicator -- the scatter of individual span outcomes,
sqrt(p(1-p)), about 0.34 at a 14% error rate. Different quantities,
and the mismatch punished the services most worth watching: the noisier
the service, the larger the denominator. At the demo's 13.8% baseline,
clearing the 2.0 threshold needed a 69 percentage point jump.

It is also floored at one percentage point, because a healthy service's
error rate is flat at zero, giving it a bucket-to-bucket stddev of
exactly zero -- so the service that had just started failing was the one
that could not alert.

Measured on the fixtures, before -> after:

case before after
13.8% -> 41% (noisy baseline) 0.80 27.40 now fires
13.8% -> 13.8% (noisy baseline) 0.01 0.20 stays quiet
0% -> 42% (flat baseline) 0.00 42.33 now fires
0% -> 0.03% (one stray error) 0.00 0.03 stays quiet

Both directions are asserted, because a test that only checks the quiet
cases passes against a detector that never fires at all.

Verified on the demo across five detector cycles: 1-2 anomalies each,
health 85-95, no errors.

…iance

The z-score divides a difference of rates, so its denominator has to be
the scatter of the rate from bucket to bucket. It was

  STDDEV(CASE WHEN status IN ('STATUS_CODE_ERROR','ERROR') THEN 1.0 ELSE 0.0 END)

over raw spans, which is the scatter of individual span outcomes --
sqrt(p(1-p)), about 0.34 at a 14% error rate. Those are different
quantities, and the mismatch punished exactly the services worth
watching: the noisier the service, the larger the denominator. At the
13.8% baseline the live demo actually ran, clearing the 2.0 threshold
required the rate to jump 69 percentage points. A tripling from 13.8%
to 41% scored 0.80.

The denominator is now the standard deviation of the per-bucket error
rate across the baseline window, and both windows average over the same
5-minute buckets -- the same asymmetry the volume detector had.

It is also floored, because a healthy service's error rate is flat at
zero and its bucket-to-bucket stddev is therefore exactly zero. Dividing
by that hit the CASE's 0.0 fallback, so the service that had just
started failing was the one that could not alert: going from no errors
to 42% scored 0.00. One percentage point is the least noise worth
assuming. It keeps a single stray error in three thousand spans at
z=0.03 while a real break reaches z=42.

Measured on the fixtures in error_rate_sql_test.go, before -> after:

  13.8% -> 41%   (noisy baseline)     0.80 -> 27.40   now fires
  13.8% -> 13.8% (noisy baseline)     0.01 ->  0.20   stays quiet
  0%    -> 42%   (flat baseline)      0.00 -> 42.33   now fires
  0%    -> 0.03% (one stray error)    0.00 ->  0.03   stays quiet

Verified on the demo across five detector cycles: 1-2 anomalies each,
health 85-95, no errors. The concern that a more sensitive denominator
would produce an alert flood does not survive contact with the data.
@vishr
vishr merged commit f2e37f4 into main Sep 21, 2026
8 checks passed
@vishr
vishr deleted the fix/error-rate-zscore-denominator branch September 21, 2026 19:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant