Skip to content

Commit fbdf824

Browse files
committed
Stabilize diagnostics and receiver stats
1 parent 2e5723c commit fbdf824

35 files changed

Lines changed: 1414 additions & 592 deletions

docs/backend/diagnostics.md

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,13 @@ themselves reflect the different cadences at which conditions are produced.
7171

7272
## Delivery is a separate channel from status
7373

74-
Diagnostics travel to the browser on their own channel over the same websocket
75-
hub that carries the rest of the live state, distinct from the channel that
76-
carries producer status. The store publishes a full snapshot of the current set
77-
whenever its contents change, and coalesces bursts of changes into a single
74+
Backend diagnostics travel to the browser on their own channel over the same
75+
websocket hub that carries the rest of the live state, distinct from the channel
76+
that carries producer status. The store publishes a full snapshot of the current
77+
set whenever its contents change, and coalesces bursts of changes into a single
7878
publish so a flurry of re-emissions does not turn into a flurry of messages.
79+
The browser can add local diagnostics for conditions only it observes, such as a
80+
frontend decode failure or a metric that was present and then stops arriving.
7981

8082
An earlier option was to attach diagnostics directly to the status message, so
8183
that a status update and the conditions explaining it would always arrive
@@ -106,15 +108,19 @@ or an override that disagrees with the detected setup, are raised as soon as
106108
there is enough information to identify the problem. Producer identification can
107109
use receiver, aircraft, statistics, or outline files, so a setup with generic
108110
receiver metadata may stay unknown until another file provides enough evidence.
109-
An unknown or ambiguous producer is itself a diagnostic condition rather than a
110-
stream of per-file warnings.
111+
Ident gives startup a short observation window before warning that the producer
112+
is unknown, because the first file to arrive is often incomplete evidence. An
113+
unknown or ambiguous producer is a diagnostic condition rather than a stream of
114+
per-file warnings.
111115

112116
Because receiver and producer-identification conditions are event-driven and may
113-
not change for hours, a heartbeat re-raises active startup conditions every few
114-
minutes so a stable misconfiguration does not quietly expire between file
115-
changes. The heartbeat is deduplicated through the same condition identity rules
116-
as every other diagnostic, so refreshing a condition does not create a second
117-
notification.
117+
not change for hours, heartbeats re-raise active startup conditions so a stable
118+
misconfiguration does not quietly expire between file changes.
119+
Producer-identification conditions use a shorter heartbeat because startup
120+
classification can resolve quickly once more files arrive; receiver
121+
configuration conditions use a slower one. Both are deduplicated through the
122+
same condition identity rules as every other diagnostic, so refreshing a
123+
condition does not create a second notification.
118124

119125
## Expiry must not be mistaken for success
120126

docs/backend/producer-normalization.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,12 @@ flowchart TD
7373
R -->|several adapters tie| M[ambiguous]
7474
```
7575

76-
Unknown and ambiguous states are surfaced as diagnostics, including the
77-
strongest evidence `identd` has seen so far. That keeps an unsupported or
78-
unusual stack visible to the operator without guessing a decoder. Once a decoder
79-
has already been selected, later ambiguous evidence only blocks a switch away
80-
from that decoder unless the operator overrides it or stronger evidence appears.
76+
Unknown and ambiguous states are surfaced as diagnostics once startup has had
77+
time to observe more than the first arriving file, including the strongest
78+
evidence `identd` has seen so far. That keeps an unsupported or unusual stack
79+
visible to the operator without guessing a decoder. Once a decoder has already
80+
been selected, later ambiguous evidence only blocks a switch away from that
81+
decoder unless the operator overrides it or stronger evidence appears.
8182

8283
A decoder that no adapter recognizes stays unknown. Some decoders write
8384
aircraft JSON that Ident could in principle read but are simply not recognized
@@ -112,7 +113,8 @@ aircraft JSON that looks close to a supported shape, but publishing it before an
112113
adapter is selected would let raw decoder semantics leak into Ident's wire
113114
contract. The HTTP server still starts immediately, so the interface stays
114115
reachable while classification is waiting for enough evidence, and the
115-
diagnostic bell explains why live producer data is not flowing yet.
116+
diagnostic bell explains why live producer data is not flowing if
117+
classification remains unresolved.
116118

117119
## Where the decoders actually differ
118120

ident/src/data/frontendDiagnostics.test.ts

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { afterEach, describe, expect, it } from "vitest";
22
import {
33
__resetFrontendDiagnosticsForTest,
4-
clearFrontendDiagnostic,
54
DEFAULT_FRONTEND_DIAGNOSTIC_TTL_MS,
65
emitFrontendDiagnostic,
76
FRONTEND_DIAGNOSTIC_CAP,
@@ -130,52 +129,6 @@ describe("frontendDiagnostics", () => {
130129
).toHaveLength(0);
131130
});
132131

133-
it("clear removes an entry by identity", () => {
134-
emitFrontendDiagnostic({
135-
severity: "warning",
136-
channel: "frontend.x",
137-
code: "code.a",
138-
message: "m",
139-
});
140-
clearFrontendDiagnostic("frontend.x", "code.a");
141-
expect(snapshotFrontendDiagnostics()).toHaveLength(0);
142-
});
143-
144-
it("clear is a no-op when nothing matches the identity", () => {
145-
expect(() =>
146-
clearFrontendDiagnostic("frontend.nope", "code.absent"),
147-
).not.toThrow();
148-
emitFrontendDiagnostic({
149-
severity: "warning",
150-
channel: "frontend.x",
151-
code: "code.a",
152-
message: "m",
153-
});
154-
clearFrontendDiagnostic("frontend.x", "code.a", "wrong-scope");
155-
expect(snapshotFrontendDiagnostics()).toHaveLength(1);
156-
});
157-
158-
it("clear respects scope (matches only the identity with that scope)", () => {
159-
emitFrontendDiagnostic({
160-
severity: "warning",
161-
channel: "frontend.x",
162-
code: "code.a",
163-
scope: "s1",
164-
message: "m1",
165-
});
166-
emitFrontendDiagnostic({
167-
severity: "warning",
168-
channel: "frontend.x",
169-
code: "code.a",
170-
scope: "s2",
171-
message: "m2",
172-
});
173-
clearFrontendDiagnostic("frontend.x", "code.a", "s1");
174-
const snap = snapshotFrontendDiagnostics();
175-
expect(snap).toHaveLength(1);
176-
expect(snap[0].scope).toBe("s2");
177-
});
178-
179132
it("evicts the oldest entry when exceeding FRONTEND_DIAGNOSTIC_CAP", () => {
180133
// Emit one over the cap; the very first entry must be gone.
181134
for (let i = 0; i <= FRONTEND_DIAGNOSTIC_CAP; i++) {

ident/src/data/frontendDiagnostics.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -131,21 +131,6 @@ export function emitFrontendDiagnostic(input: FrontendDiagnosticInput): void {
131131
scheduleExpiry();
132132
}
133133

134-
export function clearFrontendDiagnostic(
135-
channel: string,
136-
code: string,
137-
scope?: string,
138-
): void {
139-
const key = identityKey(channel, code, scope ?? "");
140-
useFrontendDiagnosticsStore.setState((state) => {
141-
if (!state.entries.has(key)) return state;
142-
const next = new Map(state.entries);
143-
next.delete(key);
144-
return { entries: next };
145-
});
146-
scheduleExpiry();
147-
}
148-
149134
export function snapshotFrontendDiagnostics(
150135
nowMs: number = Date.now(),
151136
): IdentDiagnostic[] {

0 commit comments

Comments
 (0)