feat(portal-admin): jaeger deep link on trace_id + actor-pivot on actor_id_hash #166
Reference in New Issue
Block a user
Delete Branch "feat/portal-admin-audit-trace-link-and-actor-pivot"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Turns the audit-log table's
trace_idandactor_id_hashcolumns from inert text into the two pivots an investigator actually needs:What lands
Trace-id deep link to Jaeger
audit.html— the cell becomes an<a target="_blank" rel="noopener noreferrer">pointing at${environment.jaegerBaseUrl}/trace/<traceId>. Anonymous events (traceId === null) keep the dash placeholder.environment.tsgainsjaegerBaseUrl. Dev defaults tohttp://localhost:16686(matches the composeobservabilityprofile frominfra/local/dev.compose.yml). Per-env replacement picks up whatever trace backend the future infrastructure ADR settles on — Tempo, Grafana Cloud, on-prem Jaeger; the SPA-side wiring doesn't care.Actor-pivot click
audit.html— non-nullactorIdHashbecomes a<button>styled to read inline like the hash text (.actor-hash--clickable: button reset + dotted-underline hover + brand-colored focus ring). Click →filterByActor(hash)sets the existingactorIdHashfilter signal, resets offset to 0, and re-runs the query. Each pivot still emits its ownadmin.audit.queryaudit row server-side (per ADR-0020) so the drill is itself auditable.Anonymous rows keep the
(anonymous)plain-text rendering — there's no useful filter value to pivot on.Why not inline-expand Pino log lines under the row
Considered, deferred. The BFF's Pino output goes to stdout only today; standing up a queryable log aggregator (Loki, OpenSearch, …) is a separate infrastructure chantier with its own ADR. The Jaeger jump-off carries ~99 % of the investigator's needs anyway — the trace already contains span attributes (
db.statement,http.status_code, exception events) for the same request scope; Pino lines on top of that would be redundant for most investigations.When the log aggregator does land, the inline-expand model can come back as a follow-up:
GET /api/admin/logs?traceId=<id>+ an expand affordance on the same row. The current Jaeger anchor and the future inline-logs would naturally coexist (different drills, both surfaced on the sametrace_id).Notes for the reviewer
<button>for the actor cell rather than an<a>? The action is an in-page filter change, not a navigation. Buttons keep keyboard activation (Enter / Space), don't pollute browser history, and screen readers announce "Filter the table on hash(jane), button" rather than a misleading link role.cell-actorpadding ≥ 12 px on each side).titleattributes are hover hints, not screen-reader-essential — the underlying hash + traceId are the actual semantic content. The "(anonymous)" string and the dash placeholder were already in the template.traceIdandactorIdHashin every row.Test plan
pnpm nx run-many -t lint test build --projects=portal-admin— green.auditchunk: 18.26 → ~18.5 KB raw / 4.44 KB gzip — comfortably under the per-chunk budget.Portal.Admin→ open/audit.http://localhost:16686/trace/<id>(assuming./infra/local/dev.sh up observabilityis running so Jaeger is up).—for trace,(anonymous)(plain text, not clickable) for actor.<details>summary gets focus ring.Follow-ups (optional)
/userspage (clicking a row'soidto "show me this user's audit trail") is the natural sibling. Defer until there's an investigator workflow that asks for it — premature otherwise.The audit-log table carries `trace_id` + `actor_id_hash` columns already populated by the BFF (ADR-0013) but they were plain text — an investigator could see the values but had to copy them out of the table to do anything with them. This PR turns both into interactive affordances: * **`trace_id` becomes a Jaeger deep link.** Each non-null `traceId` renders as `<a target="_blank" rel="noopener noreferrer">` pointing at `${jaegerBaseUrl}/trace/<id>`. The base URL is per-env in `apps/portal-admin/src/environments/ environment.ts` (dev: `localhost:16686` matching the compose observability profile; prod: the future infra ADR will swap to whatever trace backend is settled on — Tempo, Grafana Cloud, on-prem Jaeger…). Anonymous events (no traceId) keep the dash placeholder. Closes the "join audit + traces by trace_id" promise of ADR-0012 / ADR-0013 with the lowest-coupling option available today (no new BFF endpoint, no log aggregator required). * **`actor_id_hash` becomes a click-to-filter button.** Each non-null hash renders as a `<button>` styled to read inline like the hash text but with a hover affordance + focus ring + underline. Clicking it sets `actorIdHash.set(hash)`, resets the offset to 0, and re-runs the query — pivoting the table on that single actor's events. Anonymous rows keep the `(anonymous)` plain-text rendering since there's no filter value to apply. Each pivot still emits its own `admin.audit.query` audit row server-side (ADR-0020) so the drill is auditable. Why not inline-expand Pino logs under the row? Considered, deferred to a future chantier — the BFF's Pino output goes to stdout today with no queryable backend; standing up a log aggregator (Loki / OpenSearch / …) is a separate infra ADR. Jaeger jump-off carries ~99 % of the investigator's needs because the trace already contains span attributes (db.statement, http.status_code, exception events) for the same scope. Implementation: * `audit.ts` gains `jaegerUrl(traceId)` (builds the URL with encodeURIComponent) and `filterByActor(hash)` (mutates the actor filter + offset + re-fetches). Imports `environment.jaegerBaseUrl`. * `audit.html` swaps the trace cell to an anchor (or dash) and the actor cell to a clickable button (or anonymous text), each with a `title` attribute for hover hint. * `audit.scss` adds `.actor-hash--clickable` (button reset + dotted-underline hover) and `.trace-link` (brand-coloured underlined link), both with focus rings + dark-mode swaps. Verification: * 54 portal-admin specs pass (was 50; +4 for the four new behaviours: trace anchor + dash + actor pivot + anonymous plain text). * `pnpm nx build portal-admin` clean; lazy `audit` chunk grows marginally (4.36 → 4.44 KB gzip) — well under the per-chunk budget.