Found while documenting the record-view audit surface for #9517 (PR pending on branch claude/issue-9517-readme-read-audit). Not fixed there — that PR is docs-only and this is a code change to a landed feature, so it does not meet the in-place bar.
What is wrong
packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts declares the record_views list view with these columns:
columns: ['created_at', 'user_id', 'object_name', 'record_id', 'ip_address'],
filter: [{ field: 'action', operator: 'in', value: ['read'] }],
The view is filtered to action: 'read' rows only. But the writer that produces those rows — buildRow in packages/plugins/plugin-audit/src/read-audit.ts — sets exactly:
action, created_at, user_id, object_name, record_id, old_value, new_value, tenant_id, and conditionally organization_id and actor.
ip_address is not among them, and no other writer emits a read row. ⇒ The last column of this view is empty on every row it can ever show.
Reproduce without running anything:
git grep -n "ip_address" -- packages/plugins/plugin-audit/src/read-audit.ts # no hits
git grep -n "'ip_address'" -- packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts
The second prints the record_views column list. The first prints nothing.
Why it is worth a card
This is the same defect class #7675 / #8147 / #8315 spent three cards closing on this exact object, one layer down: those retired enum values with no writer; this is a column with no writer, on the view shipped to surface the newest audit action. The stated principle there was 审计面宁窄勿谎 — a compliance surface should be narrow rather than untruthful — and an always-blank IP Address column on the "who viewed this record" screen reads to an auditor as "we captured the client fingerprint and this request had none", which is a stronger and wrong claim.
It is also live on the same screen plugin-audit contributes into the Setup app's group_diagnostics navigation.
Note on the existing field semantics
ip_address / user_agent on sys_audit_log are populated on auth events only — the record-level CRUD writer does not stamp them either. So the column is not "broken for reads"; it was never a column a non-auth row carries. The record_views view is the only place a non-auth-filtered view puts it in the column list.
Options (not deciding)
- Drop
ip_address from the record_views columns. Smallest change; makes the view show only what the row carries. Candidate replacement column: actor, which the read writer does stamp and which is the one that attributes a service principal. - Stamp it in the read writer. The
afterFind hook has ctx.session; whether a client fingerprint is reachable there — and whether an audit ledger of views should carry one — is a product call, not a mechanical fix. ⚠️ Note the read writer is deliberately allocation-only on the request path, so anything added there is on the read's critical section.
⇒ Option 1 unless someone wants the capability; the point of this card is that the current state claims a capability neither option has delivered yet.
Refs: #9517 (the README card that surfaced it) · #8992 / PR #9515 (the read-audit MVP) · #7675 / #8147 / #8315 (the same defect class on the enum).
Found while documenting the record-view audit surface for #9517 (PR pending on branch
claude/issue-9517-readme-read-audit). Not fixed there — that PR is docs-only and this is a code change to a landed feature, so it does not meet the in-place bar.What is wrong
packages/plugins/plugin-audit/src/objects/sys-audit-log.object.tsdeclares therecord_viewslist view with these columns:The view is filtered to
action: 'read'rows only. But the writer that produces those rows —buildRowinpackages/plugins/plugin-audit/src/read-audit.ts— sets exactly:action,created_at,user_id,object_name,record_id,old_value,new_value,tenant_id, and conditionallyorganization_idandactor.ip_addressis not among them, and no other writer emits areadrow. ⇒ The last column of this view is empty on every row it can ever show.Reproduce without running anything:
The second prints the
record_viewscolumn list. The first prints nothing.Why it is worth a card
This is the same defect class #7675 / #8147 / #8315 spent three cards closing on this exact object, one layer down: those retired enum values with no writer; this is a column with no writer, on the view shipped to surface the newest audit action. The stated principle there was 审计面宁窄勿谎 — a compliance surface should be narrow rather than untruthful — and an always-blank
IP Addresscolumn on the "who viewed this record" screen reads to an auditor as "we captured the client fingerprint and this request had none", which is a stronger and wrong claim.It is also live on the same screen
plugin-auditcontributes into the Setup app'sgroup_diagnosticsnavigation.Note on the existing field semantics
ip_address/user_agentonsys_audit_logare populated on auth events only — the record-level CRUD writer does not stamp them either. So the column is not "broken for reads"; it was never a column a non-auth row carries. Therecord_viewsview is the only place a non-auth-filtered view puts it in the column list.Options (not deciding)
ip_addressfrom therecord_viewscolumns. Smallest change; makes the view show only what the row carries. Candidate replacement column:actor, which the read writer does stamp and which is the one that attributes a service principal.afterFindhook hasctx.session; whether a client fingerprint is reachable there — and whether an audit ledger of views should carry one — is a product call, not a mechanical fix.⇒ Option 1 unless someone wants the capability; the point of this card is that the current state claims a capability neither option has delivered yet.
Refs: #9517 (the README card that surfaced it) · #8992 / PR #9515 (the read-audit MVP) · #7675 / #8147 / #8315 (the same defect class on the enum).