Diagnostics self-read invariants
Why this page exists
This page documents three durable contracts of the diagnostics self-read surface. They are properties of the platform, not behaviors of a particular release — each invariant has an explicit enforcement surface, and each is the kind of guarantee a privacy or security reviewer can cite without reading the backend source.
A reader auditing the diagnostics endpoints should be able to find every guarantee on this page in one place.
Surface
The self-read namespace gives an authenticated user read-only visibility into diagnostics data scoped to their own account. The three detail endpoints in scope are:
GET /api/auth/me/diagnostics/frontend-events/{client_event_id}— one product-event the client emitted on this user's behalf.GET /api/auth/me/diagnostics/backend-operations/{operation_id}— one backend operation traced for this user's account.GET /api/auth/me/diagnostics/frontend-incidents/{incident_id}— one client-side incident report scoped to this user.
These are paired with list endpoints (e.g.
GET /api/auth/me/diagnostics/frontend-events) documented in the
API reference. The list endpoints are not in scope for invariants 1
or 2 (uniform 404 is a property of the detail endpoints, where a single
identifier could leak existence; the no-admin-widening invariant is described
below at endpoint-family granularity).
Invariant 1 — Uniform 404 on detail endpoints
For every detail endpoint above, the backend returns 404 Not Found
indistinguishably for three different conditions:
- the identifier does not exist;
- the identifier exists but belongs to a different actor;
- the row exists but the actor has been anonymized (by account deletion or by the retention sweep — see PII policy).
A reviewer correlating responses across requests cannot infer which of the
three conditions caused the 404. There is no 403 Forbidden for cross-actor
identifiers — that would itself confirm the identifier exists.
Owner — backend: the contract is enforced at handler level in the
gnostikon backend. The handler
authorizes against the requesting user's identity and returns 404 (never 403)
when the row is not visible to that user, for any of the three reasons above.
See the handoff document for the self-read endpoints
for the full handler-level reasoning.
Owner — client: the in-app diagnostics screen in
gnostikon-client
collapses any 4xx response into a single "not available" enum value before
the render layer sees it. This prevents the client from accidentally
distinguishing the three conditions for the user via different error copy or
different UI affordances.
Invariant 2 — No admin widening for frontend-events
The diagnostics namespace exposes admin-scoped siblings for two of the three families:
GET /api/auth/diagnostics/backend-operations(and detail) — admin read of any user's backend-operations.GET /api/auth/diagnostics/frontend-incidents(and detail) — admin read of any user's frontend-incidents.
There is no GET /api/auth/diagnostics/frontend-events (or its detail
variant). The asymmetry is the contract: the channel that records what users
do — the product-events log — has no admin read path. The user's own actions
are visible only to that user via the /api/auth/me/... self-read endpoints.
Owner — backend: the contract is "the route does not exist". Adding a
route under /api/auth/diagnostics/frontend-events/* is a doctrine change,
not a route addition: it would require updating this page and a security
review, not just a router patch. Adding the route silently is the bug to
defend against; the absence is the affirmation.
The frontend-events admin namespace does contain operational siblings that
are not read-of-user-data — for example
/api/auth/diagnostics/frontend-events/allow-list (event-name allow-list
metadata). Those do not widen the read surface and are not in scope for this
invariant.
Invariant 3 — Properties payload verbatim on the client
When the client renders a diagnostics row, it shows the properties object
exactly as the backend returned it. The client does not re-hash, does not
mask, does not redact, and does not transform the payload in any
privacy-relevant way before display.
Hashing of PII fields is the backend's responsibility, applied at ingest to the canonical list of PII paths. See PII policy for the algorithm (SHA-256 with deployment-managed salt), the canonical paths list, and the deletion/retention semantics. By the time the read path returns a row, every PII-path value is already a hash; the client just renders what it received.
Owner — backend: ingest enforces the hash; see
pii_hashing.py.
Owner — client: the renderer treats properties as opaque JSON for
display. The
in-app diagnostics surface
describes the non-persistent render path that consumes these payloads. If
masking were ever needed at render time, it would be a backend-side change
(adding the path to the PII paths list); the client would not gain a masking
layer.
Related
- PII policy — ingest-side hashing, deletion, retention.
- In-app diagnostics surface — architecture of the screen that consumes these endpoints.
- API reference — request/response shapes for the diagnostics endpoints, generated from the canonical OpenAPI document.