Activity Logs
Activity Logs capture what admins do in your workspace. Every create, update, and delete is recorded automatically, along with the actor, the resource, the IP address, and a field-level before/after snapshot. The page is built for two situations: producing evidence in a legal or regulatory matter, and reconstructing the timeline of an internal investigation.
It’s not analytics. Analytics are aggregate and anonymous by design — a 5-person floor keeps individuals from being identifiable. Activity Logs are the opposite. Every entry names the actor. They have to.
Where to find them
Section titled “Where to find them”Open Activity Logs from the User Management sidebar. The page lives at /activity-logs and requires the VIEW_AUDIT_LOGS permission, which only Admin holds. Other roles can’t see the link or hit the endpoint.
What gets recorded automatically
Section titled “What gets recorded automatically”Any authenticated POST, PUT, PATCH, or DELETE that returns a successful response gets logged. You don’t have to wire anything up; new endpoints get audited the day they ship.
In practice that covers:
- User changes: invites, role assignments, deactivations, deletions, profile edits.
- Policy changes: drafts, edits, publishes, archives, deletes.
- Policy areas and Divisions: creates, edits, point-of-contact changes.
- Sign-ins and other auth events: password sign-in, magic link, OAuth, registration, email verification, password reset.
Each entry stores:
| Field | What it is |
|---|---|
Actor | The user who took the action. Name, email, avatar. |
Action | A canonical slug like USERS_DEACTIVATE or POLICIES_UPDATED, color-coded by category. |
Resource | The thing the action targeted, resolved to its real name. For users this is Display Name <email> so investigators always have a durable identifier. |
Summary | A one-liner: status: "ACTIVE" → "DEACTIVATED", or for a policy edit, severity: 2 → 4; text (text changed, +120 chars). |
When | Relative time in the table, full timestamp on hover. |
IP address and User agent | Captured from the request, visible in the details panel. |
What the diff captures
Section titled “What the diff captures”Click any row to open the details panel. For most edits you’ll see a What changed section listing each field that moved, with the value before on the left and after on the right. Long text fields use a stacked layout so you can read the full content without truncation.
The diff is computed by snapshotting the resource just before the handler runs and again after it completes. A few things to know:
- Side-effects are captured too. Promoting a user to a new role automatically syncs their permissions array. Both the role change and the permissions change show up in the diff, even though the actor only submitted the role.
- No-op updates are still recorded. If an admin saves a form without actually changing anything, the entry exists but the diff is empty. That’s accurate, even if it isn’t satisfying.
- Old entries don’t get a diff applied retroactively. Anything written before the snapshot logic shipped just shows the request payload. We can’t reconstruct historical state we never recorded.
For resources without a snapshot loader (workspace bulk operations, evaluations, etc.), the panel falls back to showing the values the actor submitted.
What’s intentionally not stored
Section titled “What’s intentionally not stored”Sensitive fields are redacted before the entry is written: passwords, tokens, OAuth authorization codes, and client secrets. If one of those appears in a request body, the entry shows [redacted] in its place. Long strings are truncated past a sensible limit so a single row doesn’t blow up storage.
A handful of routes are excluded entirely because logging them would be noise: token refresh, magic-link send, SSO initiate, the /users/me heartbeat, internal service-to-service traffic, file uploads, and policy evaluations. Sign-ins themselves are recorded — only the housekeeping bits around auth are skipped.
Filtering, search, and the details panel
Section titled “Filtering, search, and the details panel”The pill row above the table filters by resource: All, Policies, Users, Policy areas, Sign-ins. The search box matches actor name, actor email, action slug, and the readable summary. Use it when you need every action a person took, or every change to a particular policy.
Clicking a row opens the details panel with the full record: actor, action badge, resolved resource name plus its raw ID, summary, the original endpoint, IP, user agent, and the field-level diff if there is one. The footer reads:
Snapshot taken before and after the action. Values are exactly as stored at the time. This record is the source of truth for audit and forensic review.
If a row’s integrity is ever questioned, that’s the claim — the snapshot is the row’s state at the moment the action ran, not a reconstruction after the fact.
Exporting for evidence and forensics
Section titled “Exporting for evidence and forensics”The Export CSV button in the page header downloads the current filter set. One thing to flag about the format: it emits one row per changed field, not one row per event. A single edit that touched three fields produces three rows, each sharing the same timestamp/actor/action/resource/IP but with their own field, before, and after columns.
That makes the file grep-able and pivot-friendly. A common pattern is to filter the field column for severity to see every severity change across a year, or to pivot by actor_email to see what one admin touched.
The export honors whatever filter you have applied. If you’ve narrowed to “Sign-ins” and searched for sarah.lee, that’s what gets exported. The cap is 5,000 rows; for larger pulls, narrow the filter or pull successive batches by date range.
Common forensic queries
Section titled “Common forensic queries”| Question | How to answer it |
|---|---|
| When was this policy first published, and by whom? | Filter to Policies, search the title, find the POLICIES_PUBLISH row. |
| Who deactivated this user, and when? | Filter to Users, search the email. The deactivation row shows status: "ACTIVE" → "DEACTIVATED". |
| What did this admin do over the last week? | Search the admin’s email and scroll the timeline. |
| Did the harassment policy ever say something different? | Filter to Policies, search the title, click any POLICIES_UPDATED row to see the text before and after. |
| What sign-ins came from an unfamiliar IP? | Export to CSV and filter the ip_address column. Cross-reference with actor_email. |
Permissions and access
Section titled “Permissions and access”VIEW_AUDIT_LOGS ships with the Admin role and nothing else. Other roles get a 403 from the API and don’t see the sidebar link. There’s no per-page or per-resource scoping — any Admin can read every entry in the tenant. An audit log a user can hide from their own admins isn’t an audit log.
The API endpoint (GET /api/v1/activity-logs) is gated by the same permission, so exporting via the API returns the same data the UI shows.
What Activity Logs don’t cover
Section titled “What Activity Logs don’t cover”- End-user activity in the browser extension or Mac app. Those events feed Analytics and are non-attributable by design.
- Policy revision history. The per-policy revision history is the right tool when you want to read every version of a policy text — Activity Logs link to the same edits but aren’t optimized for narrative reading.
- Real-time alerting. The page polls. If you need notifications on specific actions, that’s a different surface.
Related
Section titled “Related”- Users and roles — the role model that decides who can see this page.
- Roles & permissions matrix — exact mapping of
VIEW_AUDIT_LOGSto roles. - Policy revision history — the per-policy view of edits over time.
- Analytics privacy — why the analytics dashboard is anonymous and Activity Logs aren’t.