# xspeed/1.1.1/includes/class-activity-log.php

xSpeed Cache: AI-Powered Performance Hub with MCP, Caching &amp; CDN, version 1.1.1. 134 lines.

- Page: https://pluginprobe.com/plugins/xspeed/1.1.1/code/includes/class-activity-log.php
- Raw: https://pluginprobe.com/plugins/xspeed/1.1.1/raw/includes/class-activity-log.php
- Modified: 2026-07-28T06:23:24+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/xspeed/1.1.1/code/includes/class-activity-log.php#L10-L20`.

```php
<?php
/**
 * Activity_Log — capped, append-only log of cache lifecycle events.
 *
 * Used by the Health module's dashboard panel + (later phases) by audit
 * surfaces. Storage: one transient `xspeed_activity_log` containing at
 * most MAX_ENTRIES events, ordered newest-first. Transient TTL is long
 * (30 days) so we don't lose history during quiet periods, but the size
 * cap keeps it bounded.
 *
 * Event shape:
 *   [ ts => int, type => string, message => string, severity => 'info' |
 *     'warn' | 'error' | 'success' ]
 *
 * Callers (from Cache.php and elsewhere):
 *   Activity_Log::record('cache_purged', 'Cache purged (settings change)');
 *   Activity_Log::record('cache_enabled_event', 'Cache enabled', 'success');
 *   Activity_Log::record('conflict_detected', 'WP Rocket activated', 'warn');
 *
 * Event type ids are snake_case — sanitize_key (called on the way in)
 * strips dots, so dotted ids would collapse silently.
 *
 * @package XSpeed
 */

declare(strict_types=1);

namespace XSpeed;

defined( 'ABSPATH' ) || exit;

final class Activity_Log {

	public const TRANSIENT_KEY = 'xspeed_activity_log';
	public const TTL           = 2592000; // 30 days
	public const MAX_ENTRIES   = 50;

	public const INFO    = 'info';
	public const WARN    = 'warn';
	public const ERROR   = 'error';
	public const SUCCESS = 'success';

	public static function record( string $type, string $message, string $severity = self::INFO ): void {
		$entries = self::entries();
		array_unshift(
			$entries,
			array(
				'ts'       => time(),
				'type'     => sanitize_key( $type ),
				'message'  => $message, // caller must produce already-safe text.
				'severity' => in_array( $severity, array( self::INFO, self::WARN, self::ERROR, self::SUCCESS ), true ) ? $severity : self::INFO,
			)
		);
		if ( count( $entries ) > self::MAX_ENTRIES ) {
			$entries = array_slice( $entries, 0, self::MAX_ENTRIES );
		}
		set_transient( self::TRANSIENT_KEY, $entries, self::TTL );
	}

	/**
	 * Newest-first entries (up to MAX_ENTRIES). Defensive shape coercion
	 * so a malformed transient never breaks the dashboard.
	 *
	 * @return array<int,array{ts:int,type:string,message:string,severity:string}>
	 */
	public static function entries(): array {
		$raw = get_transient( self::TRANSIENT_KEY );
		if ( ! is_array( $raw ) ) {
			return array();
		}
		$out = array();
		foreach ( $raw as $e ) {
			if ( is_array( $e ) && isset( $e['ts'], $e['type'], $e['message'] ) ) {
				$out[] = array(
					'ts'       => (int) $e['ts'],
					'type'     => (string) $e['type'],
					'message'  => (string) $e['message'],
					'severity' => isset( $e['severity'] ) ? (string) $e['severity'] : self::INFO,
				);
			}
		}
		return $out;
	}

	public static function clear(): void {
		delete_transient( self::TRANSIENT_KEY );
	}

	/**
	 * One-time scrub of secret values recorded by earlier versions.
	 *
	 * Settings change annotations used to include the raw value of every
	 * changed field, secrets included, and the dashboard trend endpoints
	 * serve those annotations. Redacting new writes isn't enough — the log
	 * is a 30-day transient, so entries written before the upgrade would
	 * keep exposing credentials until they aged out.
	 *
	 * Rewrites any `<secret_key> old→new` fragment to `<secret_key> changed`,
	 * preserving the rest of the entry so the causal history survives.
	 */
	public static function redact_legacy_secrets(): void {
		$entries = get_transient( self::TRANSIENT_KEY );
		if ( ! is_array( $entries ) || empty( $entries ) ) {
			return;
		}

		$changed = false;
		foreach ( $entries as $i => $entry ) {
			if ( ! is_array( $entry ) || ! isset( $entry['message'] ) ) {
				continue;
			}
			$message = (string) $entry['message'];
			// `key value→value` where key is secret-ish. Values never contain
			// a comma (describe_value truncates at 40 chars), so the fragment
			// ends at the next comma or the trailing " (via <channel>)".
			$scrubbed = preg_replace_callback(
				'/([a-z0-9_]*(?:token|password|secret|api_key|passwd|private_key|credential)[a-z0-9_]*)\s+[^,]*?→[^,]*?(?=,|\s+\(via|$)/i',
				static function ( $m ) {
					return $m[1] . ' changed';
				},
				$message
			);
			if ( null !== $scrubbed && $scrubbed !== $message ) {
				$entries[ $i ]['message'] = $scrubbed;
				$changed                  = true;
			}
		}

		if ( $changed ) {
			set_transient( self::TRANSIENT_KEY, $entries, self::TTL );
		}
	}
}

```
