feat: add structured file-based logging with admin log viewer

- New WooList_Logger class writes to wp-content/uploads/woolist-logs/woolist.log
  - INFO level: subscription events, test connection results (always recorded)
  - ERROR level: API failures, config problems (always recorded + php error_log fallback)
  - DEBUG level: full request URLs (password redacted), raw responses, step-by-step
    flow (only when "Enable debug logging" is checked in settings)
  - Auto-rotates at 1 MB; log directory protected by .htaccess
- API class: logs every request URL (redacted) and raw response body at DEBUG,
  errors at ERROR; subscribe_email_to_list logs each step (lookup/create/add)
- Hooks class: logs hook fire, skip reasons, and sync intent at DEBUG/INFO/ERROR
- Shortcode class: logs AJAX submissions, coupon generation, and failures
- Admin: new Logging section with "Enable debug logging" checkbox;
  log viewer textarea (last 300 lines, dark theme) + Clear Log button
  both visible at bottom of WooCommerce → Settings → phpList tab

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 16:25:33 +01:00
parent 0429a282bc
commit f4c9e39493
6 changed files with 309 additions and 28 deletions

View File

@@ -23,6 +23,9 @@ class WooList_Admin {
// Test connection admin-post handler.
add_action( 'admin_post_woolist_test_connection', [ $this, 'handle_test_connection' ] );
// Clear log admin-post handler.
add_action( 'admin_post_woolist_clear_log', [ $this, 'handle_clear_log' ] );
// Enqueue admin JS/CSS only on the WC settings page.
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] );
}
@@ -44,6 +47,7 @@ class WooList_Admin {
public function render_settings(): void {
woocommerce_admin_fields( $this->get_settings() );
$this->render_test_connection_button();
$this->render_log_viewer();
}
/**
@@ -79,6 +83,48 @@ class WooList_Admin {
. '</a></p>';
}
/**
* Render the log viewer panel below all settings.
*/
private function render_log_viewer(): void {
$log_content = WooList_Logger::read_recent( 300 );
$log_path = WooList_Logger::get_log_path();
$clear_url = wp_nonce_url(
admin_url( 'admin-post.php?action=woolist_clear_log' ),
'woolist_clear_log'
);
$notice = get_transient( 'woolist_clear_log_notice_' . get_current_user_id() );
if ( $notice ) {
delete_transient( 'woolist_clear_log_notice_' . get_current_user_id() );
echo '<div class="notice notice-success inline"><p>' . esc_html( $notice ) . '</p></div>';
}
echo '<hr style="margin:2em 0;">';
echo '<h2>' . esc_html__( 'Activity Log', 'woolist-phplist' ) . '</h2>';
echo '<p style="color:#646970;">';
echo esc_html__( 'Log file: ', 'woolist-phplist' );
echo '<code>' . esc_html( $log_path ) . '</code>';
echo '</p>';
if ( $log_content === '' ) {
echo '<p><em>' . esc_html__( 'Log is empty.', 'woolist-phplist' ) . '</em></p>';
} else {
echo '<div style="position:relative;">';
echo '<textarea readonly rows="20" style="width:100%;font-family:monospace;font-size:12px;background:#1e1e1e;color:#d4d4d4;padding:12px;border:1px solid #ccc;border-radius:4px;resize:vertical;white-space:pre;">'
. esc_textarea( $log_content )
. '</textarea>';
echo '</div>';
}
echo '<p>';
echo '<a href="' . esc_url( $clear_url ) . '" class="button button-secondary" '
. 'onclick="return confirm(\'' . esc_js( __( 'Clear the entire log file?', 'woolist-phplist' ) ) . '\')">'
. esc_html__( 'Clear Log', 'woolist-phplist' )
. '</a>';
echo '</p>';
}
/**
* Handle the "Test Connection" form action.
*/
@@ -89,10 +135,13 @@ class WooList_Admin {
wp_die( esc_html__( 'You do not have permission to perform this action.', 'woolist-phplist' ) );
}
WooList_Logger::info( 'Test Connection triggered by user ID ' . get_current_user_id() );
$result = $this->api->lists_get();
$user_id = get_current_user_id();
if ( is_wp_error( $result ) ) {
WooList_Logger::error( 'Test Connection failed: ' . $result->get_error_message() );
set_transient(
'woolist_test_connection_notice_' . $user_id,
[
@@ -103,6 +152,7 @@ class WooList_Admin {
);
} else {
$count = is_array( $result ) ? count( $result ) : '?';
WooList_Logger::info( 'Test Connection succeeded, found ' . $count . ' list(s).' );
set_transient(
'woolist_test_connection_notice_' . $user_id,
[
@@ -121,6 +171,29 @@ class WooList_Admin {
exit;
}
/**
* Handle the "Clear Log" admin-post action.
*/
public function handle_clear_log(): void {
check_admin_referer( 'woolist_clear_log' );
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_die( esc_html__( 'You do not have permission to perform this action.', 'woolist-phplist' ) );
}
WooList_Logger::clear();
WooList_Logger::info( 'Log cleared by user ID ' . get_current_user_id() );
set_transient(
'woolist_clear_log_notice_' . get_current_user_id(),
__( 'Log file cleared.', 'woolist-phplist' ),
60
);
wp_safe_redirect( admin_url( 'admin.php?page=wc-settings&tab=woolist' ) );
exit;
}
/**
* Enqueue admin-side assets on WC Settings → phpList tab.
*/
@@ -129,7 +202,6 @@ class WooList_Admin {
if ( ! $screen || strpos( $screen->id, 'woocommerce' ) === false ) {
return;
}
// No custom admin JS/CSS needed yet; WC handles the settings form.
}
/**
@@ -336,6 +408,25 @@ class WooList_Admin {
'type' => 'sectionend',
'id' => 'woolist_section_newsletter',
],
// ── Section 6: Logging ───────────────────────────────────────────
[
'title' => __( 'Logging', 'woolist-phplist' ),
'type' => 'title',
'desc' => __( 'Control what gets recorded in the activity log shown below.', 'woolist-phplist' ),
'id' => 'woolist_section_logging',
],
[
'title' => __( 'Enable debug logging', 'woolist-phplist' ),
'desc' => __( 'Log full API request URLs (password redacted) and raw responses. Disable on production once everything works.', 'woolist-phplist' ),
'id' => 'woolist_enable_debug_log',
'type' => 'checkbox',
'default' => 'no',
],
[
'type' => 'sectionend',
'id' => 'woolist_section_logging',
],
];
}
}