Skip to content

POC: Implement activity logging functionality with dedicated logger and handler classes - #1355

Closed
SteveJonesDev wants to merge 1 commit into
developfrom
steve/no-issue/activity-log-poc
Closed

POC: Implement activity logging functionality with dedicated logger and handler classes#1355
SteveJonesDev wants to merge 1 commit into
developfrom
steve/no-issue/activity-log-poc

Conversation

@SteveJonesDev

@SteveJonesDev SteveJonesDev commented Jan 28, 2026

Copy link
Copy Markdown
Member

This pull request introduces a comprehensive activity logging system to the Accessibility Checker plugin, enabling detailed tracking of key user actions such as scanning posts, clearing issues, and ignoring issues. The system includes database schema changes, new logging classes, integration with plugin actions, and automated tests to ensure reliability.

Activity Logging System Implementation:

  • Added a new Logger class for recording activities into a dedicated accessibility_checker_activity_log database table, and for retrieving log entries with various filters.
  • Introduced an Activity_Log_Handler class that hooks into plugin actions (edac_after_post_scan, edac_before_clear_issues, edac_after_ignore_issue) to automatically log relevant events.
  • Integrated the activity log handler into the plugin's global initialization to ensure logging works across all contexts. [1] [2]

Database and Action Hook Enhancements:

  • Created a new database table (accessibility_checker_activity_log) for storing activity logs, with appropriate indices for efficient querying. The table is created during the database update routine. [1] [2]
  • Updated the plugin database version to 1.0.6 to reflect the schema change.
  • Added and documented new action hooks (edac_after_post_scan, edac_before_clear_issues, edac_after_ignore_issue) at strategic points in the codebase to trigger activity logging after scans, before clearing issues, and after ignoring issues. [1] [2] [3]

Testing:

  • Added PHPUnit tests for the Logger class to verify activity logging, retrieval, filtering, and ordering.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added activity logging to track accessibility scans, issue clearing, and issue ignoring actions with details and timestamps.
  • Chores

    • Bumped version from 1.0.5 to 1.0.6.
  • Tests

    • Added tests for activity logging and retrieval functionality.

✏️ Tip: You can customize this high-level summary in your review settings.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @SteveJonesDev, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request establishes a foundational activity logging system within the Accessibility Checker plugin. Its primary goal is to provide a detailed audit trail of significant user interactions, such as scanning posts for accessibility issues, clearing identified issues, and ignoring specific issues. This system enhances the plugin's ability to monitor and understand user behavior, offering valuable insights into how accessibility issues are managed over time.

Highlights

  • Activity Logging System: Implemented a new activity logging system to track key user actions such as scanning posts, clearing issues, and ignoring issues within the plugin.
  • Dedicated Classes: Introduced new Logger and Activity_Log_Handler classes. The Logger handles database interactions for storing and retrieving logs, while the Activity_Log_Handler integrates with existing plugin actions to trigger logging.
  • Database Schema Changes: A new database table, accessibility_checker_activity_log, has been created to store activity entries, complete with appropriate indices for efficient querying. The plugin's database version has been updated to 1.0.6.
  • Action Hook Integration: New action hooks (edac_after_post_scan, edac_before_clear_issues, edac_after_ignore_issue) have been added at strategic points in the codebase to ensure relevant events are logged automatically.
  • Automated Testing: Comprehensive PHPUnit tests have been added for the Logger class, verifying the functionality of activity logging, retrieval, filtering, and ordering.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai

coderabbitai Bot commented Jan 28, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This PR introduces an activity logging system that captures accessibility checker operations (post scans, issue clearing, issue ignoring) through action hooks, centralizes them in a new handler class, and persists logs to a newly created database table via a dedicated Logger class.

Changes

Cohort / File(s) Summary
Version Update
accessibility-checker.php
Bumped EDAC_DB_VERSION from '1.0.5' to '1.0.6' to reflect database schema changes.
Activity Hook Integration
admin/class-ajax.php, admin/class-purge-post-data.php, includes/classes/class-rest-api.php
Added three new action hooks: edac_after_ignore_issue (after ignore updates), edac_before_clear_issues (before clearing post issues), and edac_after_post_scan (after REST API scan completion). Hooks pass operation context (IDs, action type, post ID) for downstream logging.
Database Infrastructure
admin/class-update-database.php
Added create_activity_log_table() private method to create accessibility_checker_activity_log table with user activity tracking fields (user_id, created, action, message, post_id, issue_id, rule_type, siteid) and indexes. Method invoked during edac_update_database().
Activity Logging Handler
includes/classes/ActivityLog/Activity_Log_Handler.php
New class listening to three hooks: edac_after_post_scan, edac_before_clear_issues, edac_after_ignore_issue. Aggregates issue counts, resolves post metadata, derives human-readable rule names, constructs localized messages, and delegates persistence to Logger.
Activity Logging Persistence
includes/classes/ActivityLog/Logger.php
New static utility class providing log() (insert activity to table with metadata) and get_logs() (retrieve filtered/ordered activity records). Uses prepared statements and sanitizes inputs.
Plugin Initialization
includes/classes/class-plugin.php
Instantiates and initializes Activity_Log_Handler in constructor prior to admin/non-admin branching, ensuring activity logging is available in all contexts.
Logging Tests
tests/phpunit/includes/classes/ActivityLog/LoggerTest.php
PHPUnit tests for Logger class covering basic/full metadata insertion, retrieval, filtering by action and post_id, and result ordering.

Sequence Diagram

sequenceDiagram
    participant Op as Operation<br/>(Scan/Clear/Ignore)
    participant Hook as WordPress<br/>Action Hook
    participant Handler as Activity_Log_Handler
    participant Query as wpdb Query
    participant Table as activity_log<br/>Table
    
    Op->>Hook: do_action('edac_after_*')
    Hook->>Handler: Invoke registered callback
    Handler->>Query: Query post/issue metadata
    Query-->>Handler: Return counts & details
    Handler->>Handler: Construct localized message
    Handler->>Handler: Call Logger::log()
    Handler->>Query: INSERT into activity_log
    Query->>Table: Write record
    Table-->>Query: Confirm insert
    Query-->>Handler: Return log_id
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested labels

codex

Suggested reviewers

  • pattonwebz

Poem

🐰 A logger hops through WordPress hooks so bright,
Capturing scans and clears in database night,
From AJAX to REST, each action takes flight,
Recording activities with metadata tight,
Version one-point-six shines with pure delight! 🌟

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'POC: Implement activity logging functionality with dedicated logger and handler classes' directly and accurately describes the main changes: adding activity logging with Logger and Activity_Log_Handler classes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@includes/classes/ActivityLog/Activity_Log_Handler.php`:
- Around line 196-204: The current message construction inside
Activity_Log_Handler builds a post-specific "Unignored %1$s in "%2$s" (%3$d
issues)" even when ignore_global === 1 and the action is a global unignore;
update the conditional that sets $message to detect ignore_global === 1 for the
disable/unignore path and use a global variant like "Unignored %1$s globally
(%3$d issues)" (omit $post_title) while still using $rule_name and count($ids)
so global operations don't reference $post_title misleadingly.

In `@includes/classes/ActivityLog/Logger.php`:
- Around line 141-145: The ORDER BY clause in ActivityLog\Logger::$query
construction uses the `created` timestamp which can produce non-deterministic
ordering for rapid inserts; change the ORDER BY expression to use the sequential
`id` column instead. Update the SQL string in the $wpdb->prepare call that
currently contains "ORDER BY created {$order}" to "ORDER BY id {$order}"
(keeping $where_sql, $order and ...$prepare_args intact) so results are
consistently ordered by id as expected by the tests.
- Around line 48-62: The code sets nullable keys in $data to null but always
appends fixed format placeholders in $format, causing nulls to be coerced to
0/empty; update the logic that builds $data and $format (the arrays named $data
and $format in the ActivityLog\Logger class) to only add 'post_id', 'issue_id',
and 'rule_type' entries (and their corresponding '%d'/'%s' formats) when the
sanitized $metadata values are !== null, so the final $wpdb->insert($table,
$data, $format) call omits those columns entirely instead of inserting 0/empty.
Ensure you adjust any downstream assumptions about array keys accordingly.

In `@tests/phpunit/includes/classes/ActivityLog/LoggerTest.php`:
- Around line 16-17: The test failures are caused by the missing activity log
table; add a setUp() method to the LoggerTest class that creates the
accessibility_checker_activity_log table schema before each test runs so
Logger::log() and get_logs() operate against a real table; locate LoggerTest and
implement setUp() to use the WordPress testing DB functions or $wpdb to create
the table with the same columns/indexes the plugin expects (and optionally add
tearDown() to drop the table after each test).
🧹 Nitpick comments (9)
admin/class-ajax.php (1)

756-784: Inconsistent hook parameter counts may cause confusion.

The edac_after_ignore_issue hook is fired with 4 parameters in the large batch path (Line 767) but only 3 parameters in the small batch path (Line 784). While the docblocks clearly document this difference, handlers registered with add_action may need to be aware of the varying arity.

Per the Activity_Log_Handler context, init() registers with 3 accepted arguments. The 4th parameter ($object) in the large batch case would be silently ignored unless handlers explicitly request it. Consider documenting this in the handler or normalizing the hook signature by always passing the 4th parameter (or null for small batches).

♻️ Optional: Normalize hook signature for consistency
 		} else {
 			// For small batches of IDs, we can just loop through.
 			foreach ( $ids as $id ) {
 				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Safe variable used for table name, caching not required for one time operation.
 				$wpdb->query( $wpdb->prepare( 'UPDATE %i SET ignre = %d, ignre_user = %d, ignre_date = %s, ignre_comment = %s, ignre_global = %d WHERE siteid = %d and id = %d', $table_name, $ignre, $ignre_user, $ignre_date, $ignre_comment, $ignore_global, $siteid, $id ) );
 			}

 			/**
 			 * Fires after ignoring issues for activity logging.
 			 *
 			 * `@since` 1.36.0
 			 *
 			 * `@param` array  $ids           Array of issue IDs.
 			 * `@param` string $action        The action (enable or disable).
 			 * `@param` int    $ignore_global Whether this is a global ignore.
+			 * `@param` string|null $object   The object/HTML (null for small batches).
 			 */
-			do_action( 'edac_after_ignore_issue', $ids, $action, $ignore_global );
+			do_action( 'edac_after_ignore_issue', $ids, $action, $ignore_global, null );
 		}
admin/class-update-database.php (2)

83-84: Consider gating table creation with a version check.

The create_activity_log_table() method is called unconditionally on every admin_init, which triggers dbDelta() on every admin page load. While dbDelta() is idempotent, it still performs table inspection queries. Consider moving this call inside the version check block or adding a separate version tracking mechanism for the activity log table.

♻️ Proposed fix to gate table creation
 		// Run migration for selector-based unique identifiers if upgrading from older versions.
 		if ( version_compare( $db_version, '1.0.5', '<' ) ) {
 			$this->migrate_to_selector_based_unique_id();
 		}

-		// Create activity log table.
-		$this->create_activity_log_table();
+		// Create activity log table (introduced in 1.0.6).
+		if ( version_compare( $db_version, '1.0.6', '<' ) ) {
+			$this->create_activity_log_table();
+		}

 		// Update database version option.
 		update_option( 'edac_db_version', sanitize_text_field( EDAC_DB_VERSION ) );
+	}

115-146: Minor: @since tag should reference plugin version, not DB version.

Per WordPress documentation standards, @since tags typically reference the plugin version when the feature was introduced (e.g., 1.36.0), not the database schema version (1.0.6). This helps developers understand which plugin release introduced the functionality.

Additionally, if queries will filter by issue_id, consider adding an index for it. Otherwise, the schema and indexes look appropriate for the activity logging use case.

♻️ Proposed changes
 	/**
 	 * Create activity log table.
 	 *
-	 * `@since` 1.0.6
+	 * `@since` 1.36.0
 	 * `@return` void
 	 */
 	private function create_activity_log_table() {

If issue_id queries are expected:

 			KEY post_id_index (post_id),
+			KEY issue_id_index (issue_id),
 			KEY siteid_index (siteid)
tests/phpunit/includes/classes/ActivityLog/LoggerTest.php (5)

22-26: Fix equals sign alignment per WPCS.

Static analysis reports alignment issues on these lines.

♻️ Proposed fix
 	public function test_log_activity() {
-		$action  = 'post_scan';
-		$message = 'Test scan activity';
+		$action   = 'post_scan';
+		$message  = 'Test scan activity';
 		$metadata = [
 			'post_id' => 1,
 		];

38-44: Fix equals sign alignment per WPCS.

♻️ Proposed fix
 	public function test_log_activity_with_full_metadata() {
-		$action  = 'ignore_issue';
-		$message = 'Test ignore activity';
+		$action   = 'ignore_issue';
+		$message  = 'Test ignore activity';
 		$metadata = [
 			'post_id'   => 1,
 			'issue_id'  => 42,

77-77: Multi-item associative arrays must have each value on a new line per WPCS.

♻️ Proposed fix
-		$logs = Logger::get_logs( [ 'action' => 'post_scan', 'limit' => 10 ] );
+		$logs = Logger::get_logs(
+			[
+				'action' => 'post_scan',
+				'limit'  => 10,
+			]
+		);

94-94: Multi-item associative arrays must have each value on a new line per WPCS.

♻️ Proposed fix
-		$logs = Logger::get_logs( [ 'post_id' => 1, 'limit' => 10 ] );
+		$logs = Logger::get_logs(
+			[
+				'post_id' => 1,
+				'limit'   => 10,
+			]
+		);

105-119: Avoid sleep() in tests; ordering by ID is deterministic.

Using sleep(1) slows down the test suite unnecessarily. Since the Logger::get_logs() method orders by created timestamp but IDs are sequential, consider ordering by id in the Logger class, or simply rely on the fact that sequential inserts will have sequential timestamps without needing a delay.

Also, fix the array formatting per WPCS on lines 112 and 117.

♻️ Proposed fix
 	public function test_get_logs_ordering() {
 		// Create test log entries.
 		$id1 = Logger::log( 'post_scan', 'First', [ 'post_id' => 1 ] );
-		sleep( 1 );
 		$id2 = Logger::log( 'post_scan', 'Second', [ 'post_id' => 1 ] );
 
 		// Test DESC ordering (default).
-		$logs = Logger::get_logs( [ 'limit' => 2, 'order' => 'DESC' ] );
+		$logs = Logger::get_logs(
+			[
+				'limit' => 2,
+				'order' => 'DESC',
+			]
+		);
 		$this->assertEquals( $id2, (int) $logs[0]['id'] );
 		$this->assertEquals( $id1, (int) $logs[1]['id'] );
 
 		// Test ASC ordering.
-		$logs = Logger::get_logs( [ 'limit' => 2, 'order' => 'ASC' ] );
+		$logs = Logger::get_logs(
+			[
+				'limit' => 2,
+				'order' => 'ASC',
+			]
+		);
 		$this->assertEquals( $id1, (int) $logs[0]['id'] );
 		$this->assertEquals( $id2, (int) $logs[1]['id'] );
 	}
includes/classes/ActivityLog/Activity_Log_Handler.php (1)

206-217: Logging the same message for each issue ID creates redundant entries.

The loop logs identical messages for each issue in $ids, which creates database bloat for batch operations. Consider logging a single entry for the batch operation instead.

♻️ Proposed fix: Log once per batch operation
-				// Log each issue individually.
-				foreach ( $ids as $id ) {
-					Logger::log(
-						'ignore_issue',
-						$message,
-						[
-							'post_id'   => $issue['postid'],
-							'issue_id'  => $id,
-							'rule_type' => $issue['ruletype'],
-						]
-					);
-				}
+				// Log once for the batch operation.
+				Logger::log(
+					'ignore_issue',
+					$message,
+					[
+						'post_id'   => $issue['postid'],
+						'issue_id'  => $ids[0],
+						'rule_type' => $issue['ruletype'],
+					]
+				);

If individual tracking per issue is required for audit purposes, consider adding a batch ID or storing issue IDs as JSON in the message.

Comment on lines +196 to +204
} else {
$message = sprintf(
/* translators: %1$s: Rule name, %2$s: Post title, %3$d: Number of issues. */
__( 'Unignored %1$s in "%2$s" (%3$d issues)', 'accessibility-checker' ),
$rule_name,
$post_title,
count( $ids )
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing global unignore message variant.

The global ignore case (ignore_global === 1) only handles the enable action. When globally unignoring (disable with ignore_global === 1), the message still includes $post_title which may be misleading for a global operation.

🛠️ Proposed fix
 				} else {
-					$message = sprintf(
-						/* translators: %1$s: Rule name, %2$s: Post title, %3$d: Number of issues. */
-						__( 'Unignored %1$s in "%2$s" (%3$d issues)', 'accessibility-checker' ),
-						$rule_name,
-						$post_title,
-						count( $ids )
-					);
+					if ( 1 === $ignore_global ) {
+						$message = sprintf(
+							/* translators: %1$s: Rule name, %2$d: Number of issues. */
+							__( 'Globally unignored %1$s (%2$d issues)', 'accessibility-checker' ),
+							$rule_name,
+							count( $ids )
+						);
+					} else {
+						$message = sprintf(
+							/* translators: %1$s: Rule name, %2$s: Post title, %3$d: Number of issues. */
+							__( 'Unignored %1$s in "%2$s" (%3$d issues)', 'accessibility-checker' ),
+							$rule_name,
+							$post_title,
+							count( $ids )
+						);
+					}
 				}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else {
$message = sprintf(
/* translators: %1$s: Rule name, %2$s: Post title, %3$d: Number of issues. */
__( 'Unignored %1$s in "%2$s" (%3$d issues)', 'accessibility-checker' ),
$rule_name,
$post_title,
count( $ids )
);
}
} else {
if ( 1 === $ignore_global ) {
$message = sprintf(
/* translators: %1$s: Rule name, %2$d: Number of issues. */
__( 'Globally unignored %1$s (%2$d issues)', 'accessibility-checker' ),
$rule_name,
count( $ids )
);
} else {
$message = sprintf(
/* translators: %1$s: Rule name, %2$s: Post title, %3$d: Number of issues. */
__( 'Unignored %1$s in "%2$s" (%3$d issues)', 'accessibility-checker' ),
$rule_name,
$post_title,
count( $ids )
);
}
}
🤖 Prompt for AI Agents
In `@includes/classes/ActivityLog/Activity_Log_Handler.php` around lines 196 -
204, The current message construction inside Activity_Log_Handler builds a
post-specific "Unignored %1$s in "%2$s" (%3$d issues)" even when ignore_global
=== 1 and the action is a global unignore; update the conditional that sets
$message to detect ignore_global === 1 for the disable/unignore path and use a
global variant like "Unignored %1$s globally (%3$d issues)" (omit $post_title)
while still using $rule_name and count($ids) so global operations don't
reference $post_title misleadingly.

Comment on lines +48 to +62
'post_id' => isset( $metadata['post_id'] ) ? absint( $metadata['post_id'] ) : null,
'issue_id' => isset( $metadata['issue_id'] ) ? absint( $metadata['issue_id'] ) : null,
'rule_type' => isset( $metadata['rule_type'] ) ? sanitize_text_field( $metadata['rule_type'] ) : null,
];

$format = [
'%d', // user_id.
'%s', // created.
'%s', // action.
'%s', // message.
'%d', // siteid.
'%d', // post_id.
'%d', // issue_id.
'%s', // rule_type.
];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Using %d format for nullable fields inserts 0 instead of NULL.

When post_id, issue_id, or rule_type are not provided, you set them to null, but then use %d (integer) or %s (string) formats which will convert null to 0 or empty string. This loses the semantic distinction between "not applicable" (NULL) and "zero/empty".

🛠️ Proposed fix: Conditionally include nullable fields
 	public static function log( string $action, string $message, array $metadata = [] ) {
 		global $wpdb;
 
 		$table_name = $wpdb->prefix . 'accessibility_checker_activity_log';
 		$user_id    = get_current_user_id();
 		$siteid     = get_current_blog_id();
 
 		// Prepare data for insertion.
 		$data = [
 			'user_id' => $user_id,
 			'created' => current_time( 'mysql', true ),
 			'action'  => sanitize_text_field( $action ),
 			'message' => sanitize_text_field( $message ),
 			'siteid'  => $siteid,
-			'post_id'   => isset( $metadata['post_id'] ) ? absint( $metadata['post_id'] ) : null,
-			'issue_id'  => isset( $metadata['issue_id'] ) ? absint( $metadata['issue_id'] ) : null,
-			'rule_type' => isset( $metadata['rule_type'] ) ? sanitize_text_field( $metadata['rule_type'] ) : null,
 		];
 
-		$format = [
-			'%d', // user_id.
-			'%s', // created.
-			'%s', // action.
-			'%s', // message.
-			'%d', // siteid.
-			'%d', // post_id.
-			'%d', // issue_id.
-			'%s', // rule_type.
-		];
+		$format = [ '%d', '%s', '%s', '%s', '%d' ];
+
+		// Add optional fields only if provided.
+		if ( isset( $metadata['post_id'] ) ) {
+			$data['post_id'] = absint( $metadata['post_id'] );
+			$format[]        = '%d';
+		}
+
+		if ( isset( $metadata['issue_id'] ) ) {
+			$data['issue_id'] = absint( $metadata['issue_id'] );
+			$format[]         = '%d';
+		}
+
+		if ( isset( $metadata['rule_type'] ) ) {
+			$data['rule_type'] = sanitize_text_field( $metadata['rule_type'] );
+			$format[]          = '%s';
+		}
 
 		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Logging to custom table.
 		$result = $wpdb->insert( $table_name, $data, $format );
🤖 Prompt for AI Agents
In `@includes/classes/ActivityLog/Logger.php` around lines 48 - 62, The code sets
nullable keys in $data to null but always appends fixed format placeholders in
$format, causing nulls to be coerced to 0/empty; update the logic that builds
$data and $format (the arrays named $data and $format in the ActivityLog\Logger
class) to only add 'post_id', 'issue_id', and 'rule_type' entries (and their
corresponding '%d'/'%s' formats) when the sanitized $metadata values are !==
null, so the final $wpdb->insert($table, $data, $format) call omits those
columns entirely instead of inserting 0/empty. Ensure you adjust any downstream
assumptions about array keys accordingly.

Comment on lines +141 to +145
$query = $wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table identifier is safe, WHERE clause is prepared.
"SELECT * FROM %i WHERE {$where_sql} ORDER BY created {$order} LIMIT %d OFFSET %d",
...$prepare_args
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Ordering by created timestamp may produce inconsistent results for rapid inserts.

The query orders by created but tests assert ordering by id. If multiple entries are created within the same second, their order becomes undefined. Consider ordering by id instead, which is guaranteed to be sequential.

🛠️ Proposed fix
-			"SELECT * FROM %i WHERE {$where_sql} ORDER BY created {$order} LIMIT %d OFFSET %d",
+			"SELECT * FROM %i WHERE {$where_sql} ORDER BY id {$order} LIMIT %d OFFSET %d",
🤖 Prompt for AI Agents
In `@includes/classes/ActivityLog/Logger.php` around lines 141 - 145, The ORDER BY
clause in ActivityLog\Logger::$query construction uses the `created` timestamp
which can produce non-deterministic ordering for rapid inserts; change the ORDER
BY expression to use the sequential `id` column instead. Update the SQL string
in the $wpdb->prepare call that currently contains "ORDER BY created {$order}"
to "ORDER BY id {$order}" (keeping $where_sql, $order and ...$prepare_args
intact) so results are consistently ordered by id as expected by the tests.

Comment on lines +16 to +17
class LoggerTest extends WP_UnitTestCase {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Tests fail because the activity log table doesn't exist in the test environment.

The pipeline failures show Logger::log() returns false and get_logs() returns empty arrays. This indicates the accessibility_checker_activity_log table is not being created before tests run. You need a setUp() method to create the table schema.

🛠️ Proposed fix: Add setUp method to create the table
 class LoggerTest extends WP_UnitTestCase {
 
+	/**
+	 * Set up the test environment.
+	 */
+	public function set_up(): void {
+		parent::set_up();
+		$this->create_activity_log_table();
+	}
+
+	/**
+	 * Create the activity log table for testing.
+	 */
+	private function create_activity_log_table(): void {
+		global $wpdb;
+
+		$table_name      = $wpdb->prefix . 'accessibility_checker_activity_log';
+		$charset_collate = $wpdb->get_charset_collate();
+
+		$sql = "CREATE TABLE IF NOT EXISTS {$table_name} (
+			id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+			user_id bigint(20) unsigned NOT NULL,
+			created datetime NOT NULL,
+			action varchar(50) NOT NULL,
+			message text NOT NULL,
+			siteid bigint(20) unsigned NOT NULL,
+			post_id bigint(20) unsigned DEFAULT NULL,
+			issue_id bigint(20) unsigned DEFAULT NULL,
+			rule_type varchar(50) DEFAULT NULL,
+			PRIMARY KEY (id),
+			KEY siteid (siteid),
+			KEY action (action),
+			KEY post_id (post_id)
+		) {$charset_collate};";
+
+		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
+		dbDelta( $sql );
+	}
+
 	/**
 	 * Test logging an activity.
 	 */
🤖 Prompt for AI Agents
In `@tests/phpunit/includes/classes/ActivityLog/LoggerTest.php` around lines 16 -
17, The test failures are caused by the missing activity log table; add a
setUp() method to the LoggerTest class that creates the
accessibility_checker_activity_log table schema before each test runs so
Logger::log() and get_logs() operate against a real table; locate LoggerTest and
implement setUp() to use the WordPress testing DB functions or $wpdb to create
the table with the same columns/indexes the plugin expects (and optionally add
tearDown() to drop the table after each test).

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a robust activity logging system, including new database tables, logger, and handler classes, along with comprehensive unit tests. The changes effectively integrate activity tracking for post scans, issue clearing, and issue ignoring. However, there are some inconsistencies in the parameters passed to action hooks and received by their respective handlers, which should be addressed to ensure robust and predictable behavior.

Comment thread admin/class-ajax.php
* @param int $ignore_global Whether this is a global ignore.
* @param string $object The object/HTML that was ignored.
*/
do_action( 'edac_after_ignore_issue', $ids, $action, $ignore_global, $object );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The edac_after_ignore_issue action is fired with four parameters ($ids, $action, $ignore_global, $object) when a large batch operation is performed. However, the Activity_Log_Handler::log_ignore_issue method, which hooks into this action, is only configured to receive three parameters. This inconsistency can lead to unexpected behavior or warnings if the handler is updated to expect the $object parameter in all cases, or if strict argument checking is enabled. Please ensure the action hook and its corresponding handler function have matching parameter counts and types.

do_action( 'edac_after_ignore_issue', $ids, $action, $ignore_global );

* @since 1.36.0
*/
public function init() {
add_action( 'edac_after_post_scan', [ $this, 'log_post_scan' ], 10, 2 );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The edac_after_post_scan action is registered to expect 2 arguments (10, 2), but the log_post_scan method only defines one parameter ($post_id). The second parameter, $post, which is passed from class-rest-api.php, will be ignored. Please update the log_post_scan method signature to accept the $post object if it's intended to be used, or adjust the add_action call if only $post_id is ever needed.

add_action( 'edac_after_post_scan', [ $this, 'log_post_scan' ], 10, 2 );

*
* @param int $post_id The post ID.
*/
public function log_post_scan( int $post_id ) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The log_post_scan method is hooked to edac_after_post_scan which passes two arguments ($post_id, $post). However, this method only accepts $post_id. The $post object is currently ignored. If the $post object is not needed, please update the add_action call in the init method to reflect that only one argument is expected. If it is needed, please update the method signature to accept it.

public function log_post_scan( int $post_id, $post = null ) {

public function init() {
add_action( 'edac_after_post_scan', [ $this, 'log_post_scan' ], 10, 2 );
add_action( 'edac_before_clear_issues', [ $this, 'log_clear_issues' ], 10, 1 );
add_action( 'edac_after_ignore_issue', [ $this, 'log_ignore_issue' ], 10, 3 );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The edac_after_ignore_issue action is registered to expect 3 arguments (10, 3). However, the batch operation of this action (in admin/class-ajax.php) passes 4 arguments. This mismatch can lead to unexpected behavior or warnings. Please ensure the add_action call correctly reflects the maximum number of arguments passed to the hook, or adjust the action calls to be consistent.

add_action( 'edac_after_ignore_issue', [ $this, 'log_ignore_issue' ], 10, 4 );

* @param string $action The action (enable or disable).
* @param int $ignore_global Whether this is a global ignore.
*/
public function log_ignore_issue( array $ids, string $action, int $ignore_global ) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The log_ignore_issue method is hooked to edac_after_ignore_issue. While it expects 3 arguments, the batch operation of this action (in admin/class-ajax.php) passes 4 arguments, including an $object parameter. This method should be updated to accept the fourth parameter to avoid potential issues if the action is called with it. Even if not used, accepting it makes the handler more robust.

public function log_ignore_issue( array $ids, string $action, int $ignore_global, string $object = '' ) {

* @param int $post_id The post ID.
* @param object $post The post object.
*/
do_action( 'edac_after_post_scan', $post_id, $post );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The edac_after_post_scan action is fired with two parameters: $post_id and $post. However, the Activity_Log_Handler::log_post_scan method, which hooks into this action, only accepts $post_id. The $post object is effectively ignored. This is a mismatch in the action hook signature and the handler's expected parameters. Please ensure the handler method is updated to accept the $post object if it's intended to be used, or adjust the add_action call if only $post_id is ever needed.

do_action( 'edac_after_post_scan', $post_id, $post );

Comment thread admin/class-ajax.php
* @param string $action The action (enable or disable).
* @param int $ignore_global Whether this is a global ignore.
*/
do_action( 'edac_after_ignore_issue', $ids, $action, $ignore_global );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The edac_after_ignore_issue action is fired with three parameters ($ids, $action, $ignore_global) for single operations. This is inconsistent with the batch operation (lines 757-767) which passes an additional $object parameter. For better maintainability and predictability, it's recommended to have a consistent signature for the same action hook, even if some parameters might be null or empty in certain contexts. Alternatively, consider using two distinct action hooks if the context and parameters are fundamentally different.

do_action( 'edac_after_ignore_issue', $ids, $action, $ignore_global, null );

[ $limit, $offset ]
);

// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Using spread operator for dynamic number of replacements.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber comment is used here. While the spread operator (...$prepare_args) is a valid way to handle dynamic arguments, it's good practice to ensure that the number of placeholders in the SQL query string matches the number of arguments provided to wpdb->prepare(). In this case, the WHERE clause is dynamic, so the number of %d and %s placeholders can vary. This ignore comment is acceptable given the dynamic nature of the query construction, but it's worth noting that careful testing is required to prevent SQL injection vulnerabilities when constructing dynamic queries.

// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Using spread operator for dynamic number of replacements.
$query = $wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table identifier is safe, WHERE clause is prepared.
"SELECT * FROM %i WHERE {$where_sql} ORDER BY created {$order} LIMIT %d OFFSET %d",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared comment is used for $table_name and $where_sql. While $table_name is generally safe as it's derived from $wpdb->prefix and a hardcoded string, $where_sql is constructed dynamically. Although the individual clauses are prepared, the implode operation itself doesn't re-prepare the entire string. This approach is generally safe if all components of $where_sql are already sanitized or prepared, but it's a point to be aware of for security and maintainability.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 305262cf9d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +26 to +28
// Initialize activity log handler globally (works in all contexts).
$activity_log_handler = new Activity_Log_Handler();
$activity_log_handler->init();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard logging until activity table exists

Because the activity log handler is initialized for every request, Logger::log() can run in non-admin contexts (e.g., REST scans) before Update_Database::edac_update_database() has run on admin_init. On a fresh install or immediately after update, this means the accessibility_checker_activity_log table may not exist yet, so logging attempts will error and silently drop entries; consider creating the table earlier (e.g., on activation) or adding a table-existence guard before logging.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant