EduShade
Audit Module

Activity Logs

Admin-visible trail of all user activity — logins, enrollments, HTTP calls, and impersonation

Activity Logs

Activity logs answer the question "what did users do?" Where audit logs record privileged mutations, activity logs record the broader stream of user behaviour: logins, enrolments, quiz attempts, profile edits, and the underlying HTTP requests that drove them.

Where to Find Them

Sign in as an admin with audit.read permission and go to Admin → Audit → Activity Logs (/admin/activity-logs).

End users can also see their own activity from inside their account — see My Activity Logs.

What Each Row Captures

Activity logs are stored in the activity_logs table.

FieldTypeNotes
idUUIDPrimary key
tenant_idUUIDNullable — system events without a tenant context can be logged here, but read APIs still require tenant scope
user_idUUIDThe acting user (nullable for fully anonymous events)
impersonated_byUUIDIf an admin was acting through impersonation/masquerade, their ID is here
titlestringHuman-readable headline like "User logged in" or "Submitted quiz"
actionstringAction code — login, logout, enroll, submit_quiz, etc.
modulestringSource service: auth, learning, quiz, billing, notification, engagement, ecommerce, api, web
descriptiontextOptional extra detail
endpointstringAPI path that produced the event (when applicable)
methodstringHTTP method: GET, POST, PUT, PATCH, DELETE
status_codeintHTTP response status (validated 100–599)
ip_addressINETClient IP at the time
user_agenttextClient user agent at the time
metadataJSONBFree-form context
created_attimestamptzServer timestamp at insert (or Timestamp from the source event if set)

Both actors enriched. The handler (loadUsersForActivityLogs in activity_log_handler.go) collects every distinct user_id and impersonated_by across the page, de-duplicates them, and hits the RBAC database with a single UserAssociationService.GetUsersByIDs call. The returned map then populates the user and impersonated_as blocks on the response — so a page of 50 rows costs one lookup regardless of how many impersonation events are mixed in.

What the UI Shows

The admin list view (src/components/admin/audit/activity-logs-table.tsx) renders these columns:

ColumnBehavior
TimestampHuman-formatted, sortable
TitleBold, the headline of the event
ActionCapitalised action code
UserName + email if loaded; otherwise truncated user_id
MethodColoured HTTP method badge — blue/green/yellow/red for GET/POST/PUT+PATCH/DELETE
EndpointMonospace, truncated — full path is in the detail drawer
StatusColoured status badge — green for 2xx, yellow for 4xx, red for 5xx, gray for everything else (including 3xx)
ModuleCapitalised module name

Click a row to open the detail drawer (activity-log-detail.tsx) which lays out every field, including the impersonator if present and the full metadata JSON in a pretty-printed code block.

Built-in Filters

The toolbar exposes:

FilterOptions
MethodGET, POST, PUT, PATCH, DELETE
Moduleauth, learning, quiz, billing, notification, engagement, ecommerce, api, web

The underlying API also supports filtering by action, status_code, start_date/end_date, and user_id — see Filters & Search. A bulk Export button is also present in the toolbar (currently routes through the same generic export plumbing as audit logs).

API

MethodPathPurpose
GET/v1/admin/audit/activity-logsPaginated list across all users in the tenant
GET/v1/admin/audit/activity-logs/:idSingle activity log by ID (admin can fetch any row)
GET/v1/user/audit/activity-logsSame shape, but scoped to the calling user — see My Activity Logs
GET/v1/user/audit/activity-logs/:idSingle row, scoped to the calling user

Admin endpoints require audit.read. User endpoints require only a valid JWT — they enforce a WHERE user_id = <caller> filter.

Pagination Defaults

  • Default page size: 50 rows
  • Default sort: created_at DESC
  • sort_by accepts any column name; if it doesn't already contain a ., the alias acl. is automatically prepended

Impersonation

If an admin acts through Impersonation (read-only) or Masquerade (full access), the resulting activity row carries:

  • user_id — the impersonated user (the one whose perspective the actions appeared to be from)
  • impersonated_by — the admin who initiated the impersonation

The detail drawer surfaces both clearly, so audit reviewers can always answer "was this really the user, or an admin acting as them?".

See User Impersonation for how impersonation sessions are issued.

How a Row Gets Created

Like audit logs, activity logs are inserted only by the audit-service after consuming an event from the activity.events topic. Publishing services build an ActivityEventPayload and call events.PublishActivityEvent(...). The most common publishers are:

  • auth-service — login, logout, register, password change, OAuth events, impersonation transitions
  • learning-service — enrolments, course completion, lesson views
  • quiz-service — quiz starts, quiz submissions
  • Any service using the AuditMiddleware will also publish to the activity stream where appropriate (the middleware is primarily an audit publisher; activity logging stays on the explicit-publish path)

See Event Ingestion for the full payload contract and retry semantics.

Indexes & Query Behaviour

The activity_logs table is indexed for the dominant access patterns:

IndexPurpose
idx_activity_logs_tenant_idTenant-leading scans
idx_activity_logs_user_idPer-user history lookups
idx_activity_logs_actionFilter by action code
idx_activity_logs_modulePer-module filtering
idx_activity_logs_created_atTime-range scans
idx_activity_logs_tenant_created (tenant_id, created_at DESC)Hot path — admin list view default sort + tenant filter
idx_activity_logs_tenant_user (tenant_id, user_id)Hot path — "show me this user's activity"

Troubleshooting

IssueSolution
Login happened but no activity rowCheck auth-service logs for publish failures; Redis may have been briefly unreachable
User column shows only an IDThe user was hard-deleted from edushade_auth — row is preserved but enrichment can't resolve
Status code is missing on some rowsstatus_code is only populated for events that originate from an HTTP handler; pure domain events (e.g. published from a worker) won't have one
Impersonator is set but user is missingThe acting admin is loaded into impersonated_as; the impersonated user's row may be missing from RBAC if they were deleted
Cannot see Activity Logs in the sidebarYou lack the audit.read permission; ask an admin to grant it

On this page