Activity log

What CaptainCore records in its audit trail, the shape of a log row, and every action and entity type currently written.

CaptainCore records user actions across the system for auditing. Administrators see everything from the Activity Logs page in the sidebar. Account members see their own account’s activity from that account’s Activity tab, or over the API at GET /activity-logs.

Rows live in captaincore_activity_logs. They are written by CaptainCore\ActivityLog::log() and read by CaptainCore\ActivityLog::fetch(), both in app/ActivityLog.php.

What a row contains

ActivityLog::log( $action, $entity_type, $entity_id, $entity_name, $description, $context, $account_id );
Column Source Notes
activity_log_id Auto increment Primary key
user_id get_current_user_id() 0 for system and cron writes, rendered as “System”
account_id Passed in Nullable. This is what scopes the row to a customer. Indexed
action Passed in varchar(100). See the table below
entity_type Passed in varchar(50). Indexed
entity_id Passed in Nullable. The site, domain or environment id
entity_name Passed in varchar(255). A snapshot of the name at the time, so the row still reads correctly after the resource is deleted
description Passed in Human sentence, already interpolated
context Passed in JSON. Only written when the array is non-empty. Holds details such as old and new values
ip_address $_SERVER['REMOTE_ADDR'] varchar(45), wide enough for IPv6. Only written when present
created_at Write time Indexed

Optional columns are only added to the insert when they have a value, so a row from cron carries no ip_address key at all rather than a null.

What reading a row adds

ActivityLog::fetch() enriches each item before returning it:

  • user_name is resolved from display_name, or the literal string System when user_id is 0.
  • avatar_url is a 48px Gravatar with an identicon fallback, empty for system rows.
  • context is decoded from JSON into an object.
  • created_at_raw keeps the original datetime string, and created_at is replaced with a Unix timestamp.

The response envelope is { items, total, page, pages }.

Filtering

GET /activity-logs accepts action, entity_type, user_id, account_id, date_from, date_to, page and per_page. per_page defaults to 50 and is clamped between 1 and 200. Dates are inclusive YYYY-MM-DD, expanded internally to cover the whole day.

Scoping for non-administrators happens in the endpoint, not the model:

  • Asking for an account_id you do not belong to returns 403.
  • Asking for nothing in particular applies an account_ids IN (...) filter for every account you belong to.
  • Belonging to no accounts returns an empty result set.

That last case is deliberate. Inside fetch(), an empty account_ids array produces a 1 = 0 clause rather than dropping the condition, because dropping it would return every row in the table.

curl -u user:app-password \
  "https://your-manager.example.com/wp-json/captaincore/v1/activity-logs?entity_type=domain&action=updated&per_page=25"

Tracked actions

Every ActivityLog::log() call in the plugin, grouped by entity type.

Sites

Action Trigger
created A site is created, a site is imported from a provider, a site is provisioned by the provisioning chain, an environment is added, or an existing staging environment is linked
updated Site settings saved, site details edited, account assignments changed, environment connection settings changed, a provider_site_id auto-resolved, or a remote sync corrected the connection
deleted A site is marked inactive, or an environment record is deleted
shared A site is shared with an account
unshared A site is removed from an account
requested_removal A customer requests removal, by setting details.removed to true
cancelled_removal That request is cancelled
final_backup_requested Final Kinsta backups requested before a delete
failed Provisioning failed at the provider, with the provider’s reason in the description
requested_audit A site audit is requested
cancelled_audit A queued audit request is cancelled
monitor_on / monitor_off The uptime monitor is toggled on an environment
added_label / removed_label A site label is added or cleared by wp captaincore site-label

Domains

Action Trigger
locked / unlocked Registrar transfer lock toggled
enabled / disabled WHOIS privacy toggled
updated WHOIS contacts updated, or nameservers changed. The nameserver row carries the new list in context
executed The transfer auth code was retrieved
deleted Domain deleted. context carries removed and warnings

DNS records

Action Trigger
created Record added. context carries type and name
updated Record modified. Same context
deleted Record removed. Same context

These are logged from the bulk DNS handler as well as the single-record routes, so a zone import produces one row per record.

Email forwarding

Action Trigger
created Email forwarding activated for a domain, entity type email_forward

Environments

Action Trigger
deployed An environment was pushed onto another, staging deployed to production, or production deployed to staging
session_anomaly The session snapshot ingest detected anomalies. The description lists each one as [severity] detail, and context carries site_id, environment_id and the full anomalies array

Files

Action Trigger
viewed A file was read through the file browser. context carries path and environment
deleted A file was deleted through the file browser. Same context

Accounts

Action Trigger
invited A user was invited to an account, whether the invite was sent or access was granted to an existing user

Notes on accuracy

A few things are easy to get wrong when reading this table.

The session_anomaly row is written during ingest, but no email is sent at that moment. High and critical alerts are delivered as an hourly digest by wp captaincore session-alerts, which reads unalerted rows through the alerted_at column on captaincore_session_snapshots. That keeps SMTP off the ingest request path.

monitor_on and monitor_off are built by string concatenation from the resolved state, so they will not appear in a grep for a quoted literal.

Rows written from WP-CLI, notably the site-label commands, carry a real user_id only when a user context exists. From cron they are attributed to System.

There is no transferred action and no toggled action in the current source, despite older documentation listing them. Environment toggles are recorded as monitor_on / monitor_off on the site, and a domain transfer records executed when the auth code is retrieved.