Check Performance Data - Audit log (AB#294592)
The admin Audit log (GET /admin/audit-log, root tile "Audit log", grant audit-log) lists every
row of AuditEntries newest first, with data egress transfers distinguishable among them, three
cumulative filters, 20 rows a page, and a CSV export of the filtered set.
What is audited
-
Generic capture.
PortalDbContext.SaveChangesAsyncwrites oneInsert/Update/Deleterow per tracked entity change:EntityTypeis the CLR type name,EntityIdthe primary key,UserIdthe DfE Sign-In subject (systemoutside a request,rules-engine-workerin the worker). Excluded as telemetry or bulk-derived data: queue metrics, search events and hit rows, organisation logins, the egress learner rows andEgressRunOutput. Not excluded: everyAppLogrow the database log sink persists, so "App log / Insert" rows make up most of an unfiltered log (about 70% in a dev database). Whether to exclude them is an open product question. -
Deliberate rows written by hand:
ContentBundle/Import,DlqMessage/…(redrive, purge, full-payload view),SearchSession/SearchSessionDelete,SearchAnalyticsSink/…, rules-config actions, and the two egress rows below. -
Egress pulls. The run row itself is not excluded from the generic capture, so every pull is
also an
EgressRun/Insertrow (PascalCase payload with the window id and the starter's name). The log shows it as Data egress · Run started, naming the person who pulled, with no status; the status filter never matches it. -
Egress transfers.
EgressRunRepository.MarkTransferredAsyncandMarkTransferFailedAsynceach write oneEgressRunrow —TransferorTransferFailed— inside the same transaction as the guarded status flip, so a run that reached transfer always has exactly one row per attempt and a write that lost its race has none. Both payloads (NewValues, camelCase JSON) carryoutcome,windowId,outputTypesandtransferredBy; success adds the files, hashes, container and time; failure adds thereason. A run that failed in preprocessing or was abandoned never left the service and is not an audit row — the runs history shows those. -
Early closure of a checking exercise (AB#301022).
WindowRepository.CloseExerciseEarlyAsyncwrites oneWindowAdmin/ClosedEarlyrow inside the same transaction as the guarded end-date move, so an exercise closed early always has exactly one row and a close that lost its race has none.EntityIdis the window id,UserIdthe admin's subject,Timestampthe UTC instant of the close. The payload (NewValues, camelCase JSON) carrieswindowId,windowTitle,exerciseType,scheduledEnd(the end date it had),newEndDate,closedEarly,closedByandclosedAtUtc. The end-date move itself is anExecuteUpdate, so the generic capture writes noCheckingExercise/Updaterow for it — this row is the record. -
Automatic hand-over of a checking exercise's requests (AB#302158).
WindowAdminAuditWriterwrites oneWindowAdmin/RequestsSentAutomaticallyrow when the service hands an exercise's requests over by itself, two hours after the exercise ends — but only for a run that sent or cancelled something, so the day of repeat runs that follows leaves no rows.EntityIdis the window id,UserIdis null (nobody did it; the screen says "System"),Timestampthe UTC instant of the run. The payload carrieswindowId,windowTitle,exerciseType,exerciseEnd,requestsSent,draftsCancelled,automaticandranAtUtc. It is written after the sweep rather than in a transaction with it, so a failed write loses the row, not the hand-over.
The screen
| Column | Egress row | Window admin row | Any other row |
|---|---|---|---|
| User |
transferredBy from the payload (the subject id for a pull row) |
closedBy from the payload |
the UserId subject (there is no user directory) |
| Activity | turquoise Data egress tag ("Run started" beneath for a pull row) | orange Window admin tag | grey tag with the entity type's label, the action beneath |
| Checking window | the run's window title, output types beneath | the window title, "{exercise} closed early, before scheduled end" beneath | the window title for a CheckingWindow row; otherwise empty |
| Time |
d MMM yyyy and HH:mm:ss UTC
|
same | same |
| Status | green Success / red Failed | green Success | none |
An automatic hand-over is a window-admin row as well: System as the user, the orange Window admin tag, the window title with "{exercise} requests sent for processing automatically: N requests sent, M drafts cancelled" beneath, and green Success.
Filters (a plain GET form, no script): activity = the row's EntityType (options are the
distinct types present, plus EgressRun always; ?activity=EgressRun isolates egress); checking
window = egress rows whose run belongs to the window (resolved through egress_runs, so it never
parses JSON in SQL) or CheckingWindow and WindowAdmin rows whose id is the window; status =
egress rows whose action is Transfer (Success) or TransferFailed (Failed), plus
WindowAdmin/ClosedEarly and WindowAdmin/RequestsSentAutomatically rows (Success only).
Filters AND together; an unknown value is no filter. Page links and the export carry every filter.
Export
GET /admin/audit-log/export?activity=&windowId=&status= streams every matching row as
text/csv (audit-log-{yyyyMMdd-HHmmss}.csv): Timestamp (UTC),User,Activity,Action,Entity type, Entity id,Checking window,Output types,Status. No cap; rows stream straight to the response.
Immutability, retention, privacy
- A
BEFORE UPDATE OR DELETEtrigger onAuditEntriesraises, so nothing — including the app — can amend or delete a row; the page has no such control. Pinned byAuditEntryActionLengthTests.AuditEntries_AreImmutable_UpdateAndDeleteRaise. - Nothing purges
AuditEntries; there is no retention job for them. -
OldValues/NewValues/ChangedColumnsare never rendered or exported: the generic capture stores pupil-bearing entities (ChangeRequest) in them. The query projectsNewValuesonly forEgressRunandWindowAdminrows (two hand-written payloads with no pupil data), andAuditLogRowhas no payload member. The export shows a window-admin row's exercise in the Action column, asClosedEarly (PupilData)orRequestsSentAutomatically (PupilData).
Known limits
- Non-egress rows show the sign-in subject id, not a name.
- Egress failure rows written before AB#294592 carry no
windowId/outputTypes/transferredBy(dev and review databases only): they show "Unknown window", no files and the subject id, but still filter by window through their run. - A dev "Reset seed data" or
/dev/egress/cleanupdeletes the runs; their audit rows remain and still show the window's title (the payload carries the window id), but they no longer match the checking-window filter, which resolves throughegress_runs. Production never deletes runs.