Skip to content

feat(admin): real user monitoring (v1.19.0) - #104

Merged
krazymir merged 2 commits into
mainfrom
claude/admin-user-management-2909ad
Oct 21, 2026
Merged

krazymir merged 2 commits into
mainfrom
claude/admin-user-management-2909ad

Conversation

@krazymir

@krazymir krazymir commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

What changed

A new platform-admin page, Real user monitoring (/admin/monitoring, INNOBOX_SPEC.md §14.12). It shows how InnoBox performs in people's browsers, page by page: views, sessions, p75 LCP, INP, CLS, TTFB, navigation time, the app's own API latency and failure rate, and script errors. The spec section was approved before implementation.

1. Collection (off by default)

  • A collector mounted by the authenticated app shell only. The signed-out Home, the public form and the agent routes never load it.
  • One sampling draw per browser tab, kept in sessionStorage. The sample rate is 100 / 50 / 25 / 10 / 1 %.
  • Views are named by route template only, resolved in the browser through a registry, so no number, title or id leaves it. A unit test walks app/**/page.tsx and fails when a page is missing from the registry.
  • Hard-load LCP, CLS and TTFB come from web-vitals 5.3.0 (new dependency, Apache-2.0, no NOTICE file). INP is per view, from event timing entries.
  • API timings come from a window.fetch wrapper around same-origin /api calls. The shell's 30-second pulse, the beacon and /api/auth/* are excluded. Only 5xx, network failures and timeouts count as errors; a cancelled request is not counted.
  • Script errors are deduplicated server-side by a fingerprint. Extension and cross-origin errors are dropped in the browser.
  • Batched beacons go out every 17 s while active, backing off to 157 s, and immediately when the page is hidden.

2. Beacon endpoint

  • POST /api/rum takes a session, then the new rum rate-limit bucket (20 a minute), then the switch. While off it answers 204 and discards the body unread.
  • The user id never reaches a row: no table has a user column, and the system-log wrapper records this route with no actor.
  • A new unrecordedStatuses option on the system-log wrapper keeps the beacon's 429s out of the system log.

3. Storage and the worker (migration 0052)

  • rum_views holds raw views for 7 days. rum_daily holds a per-route rollup plus an all-routes row, kept forever. rum_client_errors keeps error rows 90 days past last seen.
  • The worker's hourly housekeeping sweep recomputes the open 7-day window from raw views, then trims. Web and worker share one aggregate SQL module.
  • The switch and the sample rate are two platform_settings keys. A change writes one settings.telemetry_changed audit row; reads are not audited.

4. The page

  • Control row, a page-views-plus-LCP/INP chart on the shared ECharts wrapper, a sortable routes table with good / needs improvement / poor pills, and the error list.
  • Linked from the Administration console, with a pointer card on Platform settings.

For reviewers

  • Spec corrections made during implementation. The approved text kept raw views 7 days but computed 30d from them, which cannot work. Only 7d now gives true percentiles; 30d, 90d and All blend daily p75 by views, and the page caption says so. The spec also now says API calls are observed by wrapping window.fetch, and how a non-Error promise rejection is recorded.
  • Rebased onto v1.18.0. Main shipped its own migration 0051 and version 1.18.0 meanwhile, so this is migration 0052 and v1.19.0. The collector's poller exclusion now names the new pulse endpoint, and opening the bell's inbox list counts as page work.
  • The hard-load view waits for the page hide. Its CLS and a late LCP are only final then, so it is sent on pagehide / hidden, not on the regular tick.
  • Stack frames are parsed in linear time. CodeQL flagged a polynomial pattern in the first push. The second commit replaces it with string checks, reads only the first 20 stack lines cut to 1000 characters, and adds a hostile-input unit test.
  • rum_daily has no DELETE grant. Days outside the 7-day window are never recomputed, so there is nothing to delete.

Testing

  • pnpm typecheck passes.
  • Unit tests pass: shared 526, web 607, worker 180 (on the rebased branch). New suites cover the registry walk, beacon validation and coercion, fingerprints, the INP rule, thresholds, the rollup blend, the collector engine, the chart builder and the route contracts.
  • DB suites pass on a throwaway database with all migrations, on the rebased branch: web 132, worker 19. New suites cover the beacon write path, the switch and its audit row, the 7d raw percentile against the 30d blend, the series and error list, and the worker rollup (idempotent, late-beacon repair, trims).
  • Checked in a browser against a throwaway database: real browsing produced beacons under the right page names, a simulated page hide delivered the held hard-load view, error HTML renders as text, and the page works at phone width and in both themes.
  • Not run locally: Playwright e2e, which runs in CI. e2e/admin-monitoring.spec.ts turns collection on, posts a beacon, and switches it back off at the end.

Adds the platform-admin Real user monitoring page (/admin/monitoring,
INNOBOX_SPEC.md §14.12): page views and sessions per page, p75 LCP, INP,
CLS, TTFB, navigation and API latency, API error rate and client errors,
measured in people's browsers.

- Collector in the authenticated app shell only; off by default, sampled
  once per tab, anonymous, route templates only. Hard-load vitals through
  web-vitals 5.3.0; per-view INP; fetch wrapper for the page's own /api
  calls (pollers excluded); deduplicated script errors.
- POST /api/rum beacon: session-gated, own rate-limit bucket whose 429s
  stay out of the system log, discards everything while off, never stores
  a user id.
- Migration 0052: rum_views (7-day raw), rum_daily (per-route rollup plus
  an all-routes row, kept forever), rum_client_errors (90 days), and the
  rum_enabled / rum_sample_rate settings.
- Worker hourly sweep recomputes the open 7-day window, then trims.
- 7d reads raw views (true p75); 30d, 90d and All blend daily p75 by
  views, as the page caption states.
- Unit, live-DB and e2e tests.
Comment thread packages/shared/src/rum.ts Fixed
CodeQL flagged the Firefox-frame check (/@\S+:\d+/) and the line:col
split as polynomial on browser-supplied stacks such as a long run of
'@'. Both are now string operations plus a single unambiguous trailing
pattern, and only the first 20 stack lines, each cut to 1000 characters,
are read at all. A unit test feeds 50 000-character hostile stacks.
@krazymir
krazymir merged commit 98d833b into main Oct 21, 2026
13 checks passed
@krazymir
krazymir deleted the claude/admin-user-management-2909ad branch October 21, 2026 11:23
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.

2 participants