Skip to content

Commit ca9a74e

Browse files
authored
feat(observability-map): static observability scorer for webapp route entry points (#4455)
A static observability scorer for the webapp's route entry points, Lighthouse-style. The idea comes from evlog's `map` command, but that tool has no Remix adapter and checks for its own logging API, so the idea is ported rather than the tool. It scans all 427 loader/action entry points in `apps/webapp/app/routes` with the TypeScript compiler API and scores each against five checks: error-classification, auth-boundary, auth-scope, request-context and audit-trail. Current output on the real tree is **19/100** over 412 measured entry points. ``` cd internal-packages/observability-map pnpm exec tsx src/cli.ts # terminal report pnpm exec tsx src/cli.ts --json # machine output pnpm exec tsx src/cli.ts api/v1/token # one entry, per-check detail ``` The two findings at the top of the fix list are real: `/auth/sso` and `/api/v1/authorization-code` mint or exchange credentials unauthenticated, and `/_app/orgs/:organizationSlug/settings/team` resolves its org from a URL slug and gates each mutating branch on an RBAC check alone, which per `apps/webapp/CLAUDE.md` is not the tenant floor on self-hosted. Decisions worth knowing, all with the reasoning in the README: - The score started at 83 during development and fell to 19. Every drop was a perverse incentive being removed, not a regression: routes were being paid for having no error handling, two checks were reading the same fact, suppressing a failure raised the score, and a no-op `catch (e) { throw e }` was worth 50 points a route. - **A mutation corpus is the tool's main defence.** 44 entries apply semantics-preserving edits to a copy of the real route tree and assert the score cannot rise, per route as well as globally, because a mean can hide one route going up by taking another down. One entry runs as a live expected failure: `try { String(0); }` with a deciding catch is a known open hole worth 19 to 44, and it is disclosed rather than quietly excluded. - `audit-trail` and `request-context` are reported as headline figures rather than one finding repeated hundreds of times. Both still count in full where they should. - A cohort change moves the number without anything in the codebase getting better. Widening the sensitive cohort from 26 to 67 took the global from 15 to 19 with no webapp change at all, so the report prints per-check applicability and what the global would be without each one. CI: a report-only job posts a sticky comment when a PR moves the report, and says nothing when it does not. The package's own tests gate through `pr_checks.yml`. The diff-scoped merge gate is still deferred until the report has been used in anger. 524 tests plus the corpus. No runtime or dependency changes to anything that ships. <!-- GitButler Footer Boundary Top --> --- This is **part 1 of 4 in a stack** made with GitButler: - <kbd>&nbsp;4&nbsp;</kbd> #4485 - <kbd>&nbsp;3&nbsp;</kbd> #4484 - <kbd>&nbsp;2&nbsp;</kbd> #4483 - <kbd>&nbsp;1&nbsp;</kbd> #4455 👈 <!-- GitButler Footer Boundary Bottom -->
1 parent 4f69c43 commit ca9a74e

50 files changed

Lines changed: 15710 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/observability-map.yml

Lines changed: 375 additions & 0 deletions
Large diffs are not rendered by default.

.github/workflows/pr_checks.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ jobs:
2222
webapp: ${{ steps.filter.outputs.webapp }}
2323
packages: ${{ steps.filter.outputs.packages }}
2424
internal: ${{ steps.filter.outputs.internal }}
25+
obsmap: ${{ steps.filter.outputs.obsmap }}
2526
cli: ${{ steps.filter.outputs.cli }}
2627
sdk: ${{ steps.filter.outputs.sdk }}
2728
steps:
@@ -81,6 +82,36 @@ jobs:
8182
- 'pnpm-lock.yaml'
8283
- 'pnpm-workspace.yaml'
8384
- 'turbo.json'
85+
# The whole webapp app tree, not just its routes, and that is the whole reason this
86+
# filter exists. Two tests in @internal/observability-map read it: integration.test.ts
87+
# scans the live route tree, and webappSymbols.test.ts walks all of apps/webapp/app and
88+
# fails when a guard, sensitive or audit symbol stops resolving. Routes-only was this
89+
# filter's own bug: renaming e.g. requireUserId in app/services/session.server.ts
90+
# matched `webapp` and nothing else, so no job ran the suite and the break landed on
91+
# main, or on the next unrelated internal-packages PR.
92+
#
93+
# The cost of the wider set, measured over the last 400 commits on main: 31% touch
94+
# routes, 52% touch apps/webapp/app, so the job goes from firing on roughly a third of
95+
# PRs to roughly a half. It is the cheap one -- a single 4x runner, no containers, no
96+
# database, no prisma generate -- which is what makes that affordable.
97+
#
98+
# observability-map.yml is here because integration.test.ts asserts on its text and no
99+
# other filter watches it, so editing the report workflow alone ran nothing at all.
100+
#
101+
# Deliberately NOT here: this package's own paths, and packages/plugins/src and
102+
# internal-packages/rbac/src, the other two trees webappSymbols.test.ts reads.
103+
# `internal` above already matches `internal-packages/**` and `packages/**`, and
104+
# `unit-tests-internal.yml` runs `turbo run test --filter "@internal/*"`, which picks up
105+
# @internal/observability-map and runs the same vitest suite. Listing them here as well
106+
# ran the suite twice on every PR touching them, which was this filter's own doing.
107+
obsmap:
108+
- 'apps/webapp/app/**'
109+
- '.github/workflows/pr_checks.yml'
110+
- '.github/workflows/unit-tests-observability-map.yml'
111+
- '.github/workflows/observability-map.yml'
112+
- 'package.json'
113+
- 'pnpm-lock.yaml'
114+
- 'pnpm-workspace.yaml'
84115
cli:
85116
- 'packages/cli-v3/**'
86117
- 'packages/build/**'
@@ -149,6 +180,11 @@ jobs:
149180
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
150181
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
151182

183+
obsmap:
184+
needs: changes
185+
if: needs.changes.outputs.obsmap == 'true'
186+
uses: ./.github/workflows/unit-tests-observability-map.yml
187+
152188
e2e:
153189
needs: changes
154190
if: needs.changes.outputs.cli == 'true'
@@ -172,6 +208,7 @@ jobs:
172208
- e2e-webapp
173209
- packages
174210
- internal
211+
- obsmap
175212
- e2e
176213
- sdk-compat
177214
if: always()
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: "🧪 Unit Tests: Observability Map"
2+
3+
permissions:
4+
contents: read
5+
6+
# Its own workflow rather than a job inside observability-map.yml, because that workflow is not
7+
# reachable from pr_checks.yml's all-checks aggregate and so gates nothing. Called from there
8+
# instead, behind a paths filter, which is how every other test suite in this repo is gated.
9+
on:
10+
workflow_call:
11+
12+
jobs:
13+
unitTests:
14+
name: "🧪 Unit Tests: Observability Map"
15+
# No containers and no database: the package is a static analyser over source text, so the
16+
# suite is CPU bound on parsing the route tree and needs nothing the runner does not have.
17+
runs-on: warp-ubuntu-latest-x64-4x
18+
steps:
19+
- name: ⬇️ Checkout repo
20+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
21+
with:
22+
fetch-depth: 1
23+
persist-credentials: false
24+
25+
- name: ⎔ Setup pnpm
26+
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
27+
with:
28+
version: 10.33.2
29+
30+
- name: ⎔ Setup node
31+
uses: WarpBuilds/setup-node@bc639b444d583175926b588962199c247d23e8d3 # v6
32+
with:
33+
node-version: 24.18.0
34+
cache: "pnpm"
35+
36+
- name: 📥 Download deps
37+
run: pnpm install --frozen-lockfile
38+
39+
# This suite reads apps/webapp/app (the route tree for the scan, the whole app tree for the
40+
# symbol check) and the report workflow's text, which is why the filter that gates this
41+
# workflow watches all of those and not only the routes folder.
42+
- name: 🧪 Run tests
43+
run: pnpm --filter @internal/observability-map run test

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,6 @@ ailogger-output.log
8282

8383
# local planning/design docs, not committed
8484
**/docs/superpowers/
85+
86+
# observability-map CLI output artifact, not committed
87+
observability-map.json

0 commit comments

Comments
 (0)