Audits / Changelog
Audit source
We track selected backend events, e.g. Patch events, via the changelog table.
See Analytics/Changelog table for more.
Audit routes
- Content changelog:
api/routes/content/audit - Nudge changelog:
api/routes/nudges/audit - Skills changelog:
api/routes/skills/audit - Team changelog:
api/routes/teams/audit - User changelog:
api/routes/users/audit - User import log:
api/routes/import/users/audit
Audits in the hub
Audits, also known as Logs or Changelogs can be accessed via the Logs menu in the Admin Dashboard.

Each audit can be viewed directly in the hub as well as downloaded in CSV format.

The level of information and details shown per audit differs depending on the use case.
Audit details
Some audit entries include a details field — a JSON object describing what changed. Each audit route is responsible for transforming this raw JSON into a structured list of { property, value } pairs before serving it to the frontend.
Human-readable labels for each property key are defined in two places that must stay in sync:
- Frontend (UI):
frontend/src/admin/pages/logs/common/propertyLabels.ts— maps keys to i18n translation keys - API (CSV download):
api/utils/formatChangelogDetailsForDownload.js— maps keys to plain English strings
When adding new changelog detail keys, both files must be updated.
Gotchas
Object values must be explicitly expanded
The default fallback in audit route formatters returns { property, value } as-is, which only works for primitive values (string, number, boolean). If a detail property's value is an object (e.g. a nested set of fields), it must be explicitly expanded into a { property, value }[] array before being returned. Without this, the UI renders [object Object] and the CSV export is broken.
Example from api/routes/content/audit/get.js:
if (changedProp === 'eventSessionDetails' || changedProp === 'changedEventSessionDetails') {
return {
property: changedProp,
value: Object.entries(newValue)
.filter(([, v]) => v !== null && v !== undefined)
.map(([k, v]) => ({property: k, value: v})),
};
}
Internally-fetched fields can leak into the changelog
The modelsRest/asset.js bulk patch handler fetches extra fields from the DB for internal logic (e.g. type is always fetched to identify event assets for email sending). Because the changelog details is built by spreading the full fetched row into the patch body, those extra fields end up in details even when they were not part of the request.
Any field fetched for internal reasons must be explicitly removed from details if it was not in the original request body. The pattern used for both parent_id and type:
if (!('type' in body)) {
delete details.type;
}