Audit Log
Who did what to which thing, when. Denied and errored attempts are kept and marked rather than filtered out — a log of successes only tells you nothing about the attack you are looking for.
src/components/ui/audit-log.tsxAbout
From the component's own source, where it was written.
An append-only record of who did what.
There is no edit or delete affordance anywhere in this component, by design. An audit log that can be changed from the interface that displays it is not evidence, and the absence of those buttons is the feature.
Timestamps show both the relative and the absolute form. Relative is how people scan; absolute is what gets quoted in an incident review, and having to hover for it wastes the reviewer's time.
A failed action is recorded and shown. An attempt that was denied is often the most interesting row on the page.
Install
npx astralyx-ui add audit-log
Usage
import { AuditLog } from '@/components/ui/audit-log'
<AuditLog events={events} searchable />Composer
Change the props and the source below follows.
- a.okafor@example.comOK
a.okafor@example.com updated role “Support agent”
81.2.69.142
- systemOK
system rotated API key sk_live_••4f21
- m.laurent@example.comDenied
m.laurent@example.com attempted to delete account 88213
45.83.220.11
- a.okafor@example.comOK
a.okafor@example.com exported 12,408 customer records
81.2.69.142
- billing-workerError
billing-worker failed to charge subscription sub_9912
<AuditLog events={events} searchable />Examples
Who did what
<AuditLog events={events} now={now} />API
Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.
AuditLog
Extends ComponentProps<'div'>.
| Prop | Type | Default | Description |
|---|---|---|---|
| events | AuditEvent[] | required | { id, actor, action, target?, at, ip?, outcome?, detail? }. The action is a verb, so a row reads as a sentence. |
| searchable | boolean | true | Filters across actor, action and target at once. Which field a term belongs to is not something the reader should have to decide. |
| now | Date | — | — |
| locale | string | 'en-GB' | — |
| filterPlaceholder | string | 'Filter by actor, action or IP' | — |
| filterLabel | string | 'Filter audit log' | Accessible name for the filter field. |
| toggleDetailLabel | (open: boolean) => string | DEFAULT_TOGGLE_DETAIL_LABEL | Accessible name for a row's detail toggle. |
| emptyMessage | ReactNode | 'No events match.' | — |
Types
AuditEvent
An append-only record of who did what.
| Field | Type | Description |
|---|---|---|
| id | string | — |
| actor | ReactNode | — |
| actorAvatar? | ReactNode | — |
| action | string | Verb — "updated", "deleted", "signed in". |
| target? | ReactNode | — |
| at | Date | — |
| ip? | string | — |
| outcome? | 'success' | 'denied' | 'error' | Marked, never filtered. A denied attempt is often the most interesting row on the page. |
| detail? | unknown | Anything structured — a before/after pair, a payload. Rendered as an expandable JSON view, collapsed by default. |
Behaviour
| Topic | Detail | Description |
|---|---|---|
| timestamps | both | Relative for scanning, exact on hover and in the <time> element — an audit log with only "2 hours ago" is not evidence. |