Changeset 3477165
- Timestamp:
- 03/07/2026 08:41:29 PM (4 weeks ago)
- Location:
- flowsystems-webhook-actions
- Files:
-
- 3 added
- 3 deleted
- 14 edited
-
assets/screenshot-5.png (modified) (previous)
-
trunk/.gitignore (deleted)
-
trunk/README.txt (modified) (10 diffs)
-
trunk/admin/dist/.vite/manifest.json (modified) (2 diffs)
-
trunk/admin/dist/assets/main-DjkfZCt4.js (deleted)
-
trunk/admin/dist/assets/main-oBfATGcf.js (added)
-
trunk/admin/dist/assets/style-BckjMVPE.css (added)
-
trunk/admin/dist/assets/style-CckM26yn.css (deleted)
-
trunk/flowsystems-webhook-actions.php (modified) (2 diffs)
-
trunk/src/Activation.php (modified) (5 diffs)
-
trunk/src/Api/HealthController.php (modified) (5 diffs)
-
trunk/src/Api/LogsController.php (modified) (7 diffs)
-
trunk/src/Api/QueueController.php (modified) (1 diff)
-
trunk/src/Database/Migrator.php (modified) (4 diffs)
-
trunk/src/Repositories/LogRepository.php (modified) (2 diffs)
-
trunk/src/Repositories/StatsRepository.php (added)
-
trunk/src/Services/Dispatcher.php (modified) (3 diffs)
-
trunk/src/Services/LogArchiver.php (modified) (1 diff)
-
trunk/src/Services/LogService.php (modified) (4 diffs)
-
trunk/src/Services/QueueService.php (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
flowsystems-webhook-actions/trunk/README.txt
r3472060 r3477165 1 1 === Flow Systems Webhook Actions === 2 2 Contributors: mateuszflowsystems 3 Tags: webhook, woocommerce, automation, n8n, integration3 Tags: webhook, automation, integration, n8n, api 4 4 Requires at least: 6.0 5 5 Tested up to: 6.9 6 6 Requires PHP: 8.0 7 Stable tag: 1. 1.17 Stable tag: 1.2.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html 10 10 11 Production-safe WordPress webhooks with retries, event IDs, queue processing, and full delivery observability.11 Reliable WordPress webhooks for automation workflows with retries, delivery logs, event IDs, queue processing, and replayable webhook events. 12 12 13 13 == Description == … … 63 63 No silent failures. 64 64 65 = Replay Webhook Events = 66 67 Webhook debugging is difficult when events cannot be reproduced. 68 69 Flow Systems Webhook Actions allows you to replay any webhook event directly from the delivery logs — including successful deliveries. 70 71 This makes it easy to: 72 73 - Re-run automation workflows 74 - Debug external integrations 75 - Recover from temporary endpoint failures 76 - Test webhook consumers without recreating WordPress events 77 78 Each replay uses the original payload and event metadata, ensuring consistent behavior across retries and debugging sessions. 79 65 80 = Delivery Observability = 66 81 … … 71 86 - Attempt timeline per event 72 87 - HTTP status codes and response bodies 88 - Inspect full request payloads 73 89 - Manual retry (single or bulk) 90 - Replay webhook events for debugging and testing integrations 74 91 75 92 Filter by: event UUID, target URL, date range, status … … 113 130 - Persistent queue 114 131 - Smart retry logic 132 - Webhook replay for debugging integrations 115 133 - Permanent failure state handling 116 134 - Event UUIDs and timestamps … … 162 180 Failed webhooks are automatically retried using exponential backoff. The delay increases with each attempt (e.g., 1 minute, 2 minutes, 4 minutes, 8 minutes), up to a maximum delay of 1 hour between retries. By default, 5 attempts are made before marking a job as failed. The retry behavior can be adjusted using available filters. 163 181 182 = Can I replay webhook events? = 183 184 Yes. Every webhook delivery is logged with its payload and attempt history. 185 You can replay any event directly from the logs, which is useful for debugging integrations or re-running automation workflows. 186 164 187 = What is Payload Mapping? = 165 188 … … 180 203 3. Selecting WordPress action triggers 181 204 4. Payload mapping configuration 182 5. Webhook delivery logs 205 5. Webhook delivery logs with replay and retry controls 183 206 6. Queue status overview 184 207 7. Settings configuration screen … … 186 209 == Changelog == 187 210 188 = 1.1.1 = 211 = 1.2.0 — 2026-03-07 = 212 - Added persistent delivery stats table (`fswa_stats`) for long-term aggregation 213 - Added replay button for successful log entries 214 - Added "Execute Now" button in replay dialog with auto-open log details 215 - Added full attempt history with response body, accordion UI, and next attempt countdown 216 - Replaced browser `confirm()` dialogs with modal confirmations 217 - Fixed queue stats — removed stale `failed` status, added `permanently_failed` 218 - Fixed retry eligibility check to use log status instead of queue job status 219 - Fixed "Execute Now" button visibility to only show for pending jobs 220 221 = 1.1.1 — 2026-03-01 = 189 222 - Fixed `permanently_failed` entries being excluded from total and error delivery statistics in `getStats()`, `getAllTimeStats()`, and `LogArchiver::aggregateStatsBeforeDeletion()` 190 223 191 = 1.1.0 =224 = 1.1.0 — 2026-02-28 = 192 225 - Added event identity: each trigger dispatch generates a shared UUID and timestamp sent as `X-Event-Id` / `X-Event-Timestamp` headers and embedded in the payload under `event.{id,timestamp,version}` 193 226 - Added smart retry routing: 5xx and 429 responses trigger an automatic retry with exponential backoff; 4xx and 3xx responses are immediately marked as permanently failed … … 202 235 - Updated footer with a review prompt linking to WordPress.org 203 236 204 = 1.0.1 =237 = 1.0.1 — 2026-02-18 = 205 238 - Fixed preview freezing when mapping fields from objects with numeric string keys (e.g. WooCommerce line_items) 206 239 - Fixed orphaned pending log entries caused by logPending() silently failing — queue jobs now carry mapping metadata and recover a proper log entry if the original ID was lost … … 209 242 - Improved log details display with word break for long trigger names and dates 210 243 211 = 1.0.0 =244 = 1.0.0 — 2026-02-16 = 212 245 - Initial release 213 246 - Webhook dispatching from WordPress actions … … 224 257 This release adds new database columns (`event_uuid`, `event_timestamp`, `attempt_history`, `next_attempt_at` on logs; `log_id` on queue). The migration runs automatically on plugin activation or update. No manual steps required. 225 258 226 = 1.0.0 =259 = 1.0.0 — 2026-02-16 = 227 260 Initial stable release. -
flowsystems-webhook-actions/trunk/admin/dist/.vite/manifest.json
r3472060 r3477165 1 1 { 2 2 "src/main.js": { 3 "file": "assets/main- DjkfZCt4.js",3 "file": "assets/main-oBfATGcf.js", 4 4 "name": "main", 5 5 "src": "src/main.js", … … 7 7 }, 8 8 "style.css": { 9 "file": "assets/style- CckM26yn.css",9 "file": "assets/style-BckjMVPE.css", 10 10 "src": "style.css" 11 11 } -
flowsystems-webhook-actions/trunk/flowsystems-webhook-actions.php
r3472060 r3477165 4 4 * Plugin URI: https://flowsystems.pl/wordpress-webhook-actions 5 5 * Description: Trigger HTTP webhooks from WordPress actions (do_action). Easily connect WordPress with n8n, Zapier, Make, or custom workflows. 6 * Version: 1. 1.16 * Version: 1.2.0 7 7 * Author: Mateusz Skorupa 8 8 * Author URI: https://flowsystems.pl … … 17 17 defined('ABSPATH') || exit; 18 18 19 define('FSWA_VERSION', '1. 1.1');19 define('FSWA_VERSION', '1.2.0'); 20 20 define('FSWA_FILE', __FILE__); 21 21 -
flowsystems-webhook-actions/trunk/src/Activation.php
r3471792 r3477165 24 24 $webhooksTable = $wpdb->prefix . 'fswa_webhooks'; 25 25 $triggersTable = $wpdb->prefix . 'fswa_webhook_triggers'; 26 $logsTable = $wpdb->prefix . 'fswa_logs'; 27 $queueTable = $wpdb->prefix . 'fswa_queue'; 26 $logsTable = $wpdb->prefix . 'fswa_logs'; 27 $queueTable = $wpdb->prefix . 'fswa_queue'; 28 $statsTable = $wpdb->prefix . 'fswa_stats'; 28 29 29 30 require_once ABSPATH . 'wp-admin/includes/upgrade.php'; … … 73 74 attempt_history LONGTEXT DEFAULT NULL, 74 75 next_attempt_at DATETIME DEFAULT NULL, 76 stats_recorded TINYINT(1) NOT NULL DEFAULT 0, 75 77 created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 76 78 PRIMARY KEY (id), … … 79 81 KEY idx_created (created_at), 80 82 KEY idx_webhook_created (webhook_id, created_at), 81 KEY idx_event_uuid (event_uuid) 83 KEY idx_event_uuid (event_uuid), 84 KEY idx_stats_recorded (stats_recorded) 82 85 ) {$charsetCollate};"; 83 86 … … 107 110 dbDelta($sqlQueue); 108 111 109 update_option('fswa_db_version', '1.1.0'); 112 // Persistent delivery stats table 113 $sqlStats = "CREATE TABLE {$statsTable} ( 114 `date` DATE NOT NULL, 115 `webhook_id` BIGINT UNSIGNED NOT NULL DEFAULT 0, 116 `trigger_name` VARCHAR(255) NOT NULL DEFAULT '', 117 `success` INT UNSIGNED NOT NULL DEFAULT 0, 118 `permanently_failed` INT UNSIGNED NOT NULL DEFAULT 0, 119 `sum_duration_ms` BIGINT UNSIGNED NOT NULL DEFAULT 0, 120 `count_with_duration` INT UNSIGNED NOT NULL DEFAULT 0, 121 `http_2xx` INT UNSIGNED NOT NULL DEFAULT 0, 122 `http_4xx` INT UNSIGNED NOT NULL DEFAULT 0, 123 `http_5xx` INT UNSIGNED NOT NULL DEFAULT 0, 124 PRIMARY KEY (`date`, `webhook_id`, `trigger_name`) 125 ) {$charsetCollate};"; 126 127 dbDelta($sqlStats); 128 129 update_option('fswa_db_version', '1.2.0'); 110 130 } 111 131 … … 156 176 // Drop tables (order matters for foreign key constraints) 157 177 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared 178 $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}fswa_stats"); 158 179 $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}fswa_queue"); 159 180 $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}fswa_trigger_schemas"); -
flowsystems-webhook-actions/trunk/src/Api/HealthController.php
r3471792 r3477165 9 9 use WP_REST_Response; 10 10 use FlowSystems\WebhookActions\Repositories\LogRepository; 11 use FlowSystems\WebhookActions\Repositories\StatsRepository; 11 12 use FlowSystems\WebhookActions\Repositories\WebhookRepository; 12 13 use FlowSystems\WebhookActions\Services\QueueService; … … 17 18 protected $rest_base = 'health'; 18 19 19 private LogRepository $logRepository;20 private LogRepository $logRepository; 20 21 private WebhookRepository $webhookRepository; 21 private QueueService $queueService; 22 private StatsService $statsService; 22 private QueueService $queueService; 23 private StatsService $statsService; 24 private StatsRepository $statsRepository; 23 25 24 26 public function __construct() { 25 $this->logRepository = new LogRepository();27 $this->logRepository = new LogRepository(); 26 28 $this->webhookRepository = new WebhookRepository(); 27 $this->queueService = new QueueService(); 28 $this->statsService = new StatsService(); 29 $this->queueService = new QueueService(); 30 $this->statsService = new StatsService(); 31 $this->statsRepository = new StatsRepository(); 29 32 } 30 33 … … 53 56 */ 54 57 public function getStats($request): WP_REST_Response { 55 // Get current log stats (what's in the database)56 $ currentLogStats = $this->logRepository->getAllTimeStats();58 // Get persistent stats (fswa_stats — not affected by log deletion or replay) 59 $persistentStats = $this->statsRepository->getAllTimeStats(); 57 60 58 // Get archived stats (from logs that were deleted during retention)61 // Add legacy archived stats (pre-v1.2.0 logs deleted before the new table existed) 59 62 $archivedStats = $this->statsService->getArchivedStats(); 60 63 61 // Calculate totals: current logs + archived 62 $totalSent = $currentLogStats['total_sent'] + $archivedStats['total_sent']; 63 $totalSuccess = $currentLogStats['total_success'] + $archivedStats['total_success']; 64 $totalError = $currentLogStats['total_error'] + $archivedStats['total_error']; 64 $totalSent = $persistentStats['total_sent'] + $archivedStats['total_sent']; 65 $totalSuccess = $persistentStats['total_success'] + $archivedStats['total_success']; 66 $totalError = $persistentStats['total_error'] + $archivedStats['total_error']; 65 67 66 68 // Calculate success rate from all-time data … … 71 73 : 0.0; 72 74 73 // Get recent log stats (last 7 days) for the logs view 74 $recentLogStats = $this->logRepository->getStats(null, 7); 75 // Get recent stats (persistent + transient) for the logs view 76 $recentPersistent = $this->statsRepository->getPeriodStats(null, 7); 77 $recentTransient = $this->logRepository->getTransientStats(null, 7); 78 $recentLogStats = [ 79 'success' => $recentPersistent['success'], 80 'permanently_failed' => $recentPersistent['permanently_failed'], 81 'error' => $recentTransient['error'], 82 'retry' => $recentTransient['retry'], 83 'pending' => $recentTransient['pending'], 84 'total' => $recentPersistent['success'] + $recentTransient['error'] + $recentPersistent['permanently_failed'], 85 ]; 75 86 76 87 // Get webhook counts … … 106 117 'processing' => $queueStats['processing'], 107 118 'completed' => $queueStats['completed'], 108 'failed' => $queueStats['failed'],109 119 'permanently_failed' => $queueStats['permanently_failed'] ?? 0, 110 120 'total' => $queueStats['total'], -
flowsystems-webhook-actions/trunk/src/Api/LogsController.php
r3471792 r3477165 12 12 use FlowSystems\WebhookActions\Repositories\LogRepository; 13 13 use FlowSystems\WebhookActions\Repositories\QueueRepository; 14 use FlowSystems\WebhookActions\Repositories\StatsRepository; 15 use FlowSystems\WebhookActions\Repositories\WebhookRepository; 14 16 use FlowSystems\WebhookActions\Services\QueueService; 15 17 … … 18 20 protected $rest_base = 'logs'; 19 21 20 private LogRepository $repository;22 private LogRepository $repository; 21 23 private QueueRepository $queueRepository; 22 private QueueService $queueService; 24 private WebhookRepository $webhookRepository; 25 private QueueService $queueService; 26 private StatsRepository $statsRepository; 23 27 24 28 public function __construct() { 25 $this->repository = new LogRepository();29 $this->repository = new LogRepository(); 26 30 $this->queueRepository = new QueueRepository(); 27 $this->queueService = new QueueService($this->queueRepository); 31 $this->webhookRepository = new WebhookRepository(); 32 $this->queueService = new QueueService($this->queueRepository); 33 $this->statsRepository = new StatsRepository(); 28 34 } 29 35 … … 76 82 ], 77 83 ]); 84 85 // Replay a log entry as a fresh queue job 86 register_rest_route($this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)/replay', [[ 87 'methods' => WP_REST_Server::CREATABLE, 88 'callback' => [$this, 'replayItem'], 89 'permission_callback' => [$this, 'getItemPermissionsCheck'], 90 ]]); 78 91 79 92 // Bulk retry queue jobs associated with multiple logs … … 294 307 } 295 308 296 if (!in_array($ job['status'], ['failed', 'permanently_failed'], true)) {309 if (!in_array($log['status'], ['error', 'permanently_failed'], true)) { 297 310 return new WP_Error( 298 311 'rest_job_not_retryable', 299 __('Only failed or permanently failed jobs can be retried.', 'flowsystems-webhook-actions'),312 __('Only failed log entries can be retried.', 'flowsystems-webhook-actions'), 300 313 ['status' => 409] 301 314 ); … … 315 328 'success' => true, 316 329 'job_id' => (int) $job['id'], 330 ]); 331 } 332 333 /** 334 * Replay a log entry as a brand-new queue job 335 */ 336 public function replayItem($request): WP_REST_Response|WP_Error { 337 $logId = (int) $request->get_param('id'); 338 339 $log = $this->repository->find($logId); 340 341 if (!$log) { 342 return new WP_Error( 343 'rest_log_not_found', 344 __('Log not found.', 'flowsystems-webhook-actions'), 345 ['status' => 404] 346 ); 347 } 348 349 if ($log['status'] !== 'success') { 350 return new WP_Error( 351 'rest_log_not_replayable', 352 __('Only successful log entries can be replayed.', 'flowsystems-webhook-actions'), 353 ['status' => 409] 354 ); 355 } 356 357 $webhook = $this->webhookRepository->find((int) $log['webhook_id']); 358 359 if (!$webhook) { 360 return new WP_Error( 361 'rest_webhook_not_found', 362 __('Webhook not found.', 'flowsystems-webhook-actions'), 363 ['status' => 404] 364 ); 365 } 366 367 $payload = [ 368 'webhook' => $webhook, 369 'payload' => $log['request_payload'], 370 'log_id' => $logId, 371 'mapping_applied' => (bool) $log['mapping_applied'], 372 'original_payload' => $log['original_payload'], 373 ]; 374 375 // Pass $logId as both the payload field and the queue log_id column so 376 // processJob() reuses the existing log entry instead of creating a new one. 377 $jobId = $this->queueService->enqueue((int) $log['webhook_id'], $log['trigger_name'], $payload, null, $logId); 378 379 return rest_ensure_response([ 380 'success' => true, 381 'job_id' => $jobId, 317 382 ]); 318 383 } … … 330 395 $job = $this->queueRepository->findByLogId($logId); 331 396 332 if (!$job || !in_array($job['status'], ['failed', 'permanently_failed'], true)) { 397 $log = $this->repository->find($logId); 398 399 if (!$log || !in_array($log['status'], ['error', 'permanently_failed'], true) || !$job) { 333 400 $skipped++; 334 401 continue; … … 370 437 */ 371 438 public function getStats($request): WP_REST_Response { 372 $days = (int) ($request->get_param('days') ?: 7);439 $days = (int) ($request->get_param('days') ?: 7); 373 440 $webhookId = $request->get_param('webhook_id') ? (int) $request->get_param('webhook_id') : null; 374 441 375 $stats = $this->repository->getStats($webhookId, $days); 376 377 return rest_ensure_response($stats); 442 // Terminal outcomes from persistent stats table (not affected by log deletion or replay) 443 $persistent = $this->statsRepository->getPeriodStats($webhookId, $days); 444 445 // Transient states from live log table (error/retry/pending are short-lived) 446 $transient = $this->repository->getTransientStats($webhookId, $days); 447 448 $result = [ 449 'success' => $persistent['success'], 450 'permanently_failed' => $persistent['permanently_failed'], 451 'error' => $transient['error'], 452 'retry' => $transient['retry'], 453 'pending' => $transient['pending'], 454 'avg_duration_ms' => $persistent['avg_duration_ms'], 455 'http_2xx' => $persistent['http_2xx'], 456 'http_4xx' => $persistent['http_4xx'], 457 'http_5xx' => $persistent['http_5xx'], 458 ]; 459 460 $result['total'] = $result['success'] + $result['error'] + $result['permanently_failed']; 461 462 return rest_ensure_response($result); 378 463 } 379 464 -
flowsystems-webhook-actions/trunk/src/Api/QueueController.php
r3471792 r3477165 335 335 } 336 336 337 if (!in_array($job['status'], [' failed', 'permanently_failed'], true)) {337 if (!in_array($job['status'], ['pending', 'permanently_failed'], true)) { 338 338 return new WP_Error( 339 339 'rest_job_not_retryable', 340 __('Only failedor permanently failed jobs can be retried.', 'flowsystems-webhook-actions'),340 __('Only pending or permanently failed jobs can be retried.', 'flowsystems-webhook-actions'), 341 341 ['status' => 409] 342 342 ); -
flowsystems-webhook-actions/trunk/src/Database/Migrator.php
r3471792 r3477165 5 5 class Migrator { 6 6 private const OPTION_KEY = 'fswa_db_version'; 7 private const CURRENT_VERSION = '1. 1.0';7 private const CURRENT_VERSION = '1.2.0'; 8 8 9 9 /** … … 44 44 $wpdb->prefix . 'fswa_queue', 45 45 $wpdb->prefix . 'fswa_trigger_schemas', 46 $wpdb->prefix . 'fswa_stats', 46 47 ]; 47 48 … … 66 67 '1.0.0' => [self::class, 'migration_1_0_0'], 67 68 '1.1.0' => [self::class, 'migration_1_1_0'], 69 '1.2.0' => [self::class, 'migration_1_2_0'], 68 70 ]; 69 71 } … … 223 225 224 226 /** 227 * Migration 1.2.0 - Add persistent stats table and stats_recorded flag on logs 228 */ 229 public static function migration_1_2_0(): void { 230 global $wpdb; 231 232 $charsetCollate = $wpdb->get_charset_collate(); 233 $statsTable = $wpdb->prefix . 'fswa_stats'; 234 $logsTable = $wpdb->prefix . 'fswa_logs'; 235 236 require_once ABSPATH . 'wp-admin/includes/upgrade.php'; 237 238 $sqlStats = "CREATE TABLE {$statsTable} ( 239 `date` DATE NOT NULL, 240 `webhook_id` BIGINT UNSIGNED NOT NULL DEFAULT 0, 241 `trigger_name` VARCHAR(255) NOT NULL DEFAULT '', 242 `success` INT UNSIGNED NOT NULL DEFAULT 0, 243 `permanently_failed` INT UNSIGNED NOT NULL DEFAULT 0, 244 `sum_duration_ms` BIGINT UNSIGNED NOT NULL DEFAULT 0, 245 `count_with_duration` INT UNSIGNED NOT NULL DEFAULT 0, 246 `http_2xx` INT UNSIGNED NOT NULL DEFAULT 0, 247 `http_4xx` INT UNSIGNED NOT NULL DEFAULT 0, 248 `http_5xx` INT UNSIGNED NOT NULL DEFAULT 0, 249 PRIMARY KEY (`date`, `webhook_id`, `trigger_name`) 250 ) {$charsetCollate};"; 251 252 dbDelta($sqlStats); 253 254 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter 255 $exists = $wpdb->get_var($wpdb->prepare( 256 "SHOW COLUMNS FROM {$logsTable} LIKE %s", 257 'stats_recorded' 258 )); 259 if (!$exists) { 260 $wpdb->query("ALTER TABLE {$logsTable} ADD COLUMN stats_recorded TINYINT(1) NOT NULL DEFAULT 0"); 261 $wpdb->query("ALTER TABLE {$logsTable} ADD KEY idx_stats_recorded (stats_recorded)"); 262 } 263 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter 264 } 265 266 /** 225 267 * Get current database version 226 268 */ -
flowsystems-webhook-actions/trunk/src/Repositories/LogRepository.php
r3472060 r3477165 260 260 } 261 261 262 if (isset($data['stats_recorded'])) { 263 $updateData['stats_recorded'] = (int) $data['stats_recorded']; 264 $format[] = '%d'; 265 } 266 262 267 if (empty($updateData)) { 263 268 return true; … … 395 400 396 401 /** 402 * Get counts of transient (non-terminal) statuses from the live log table. 403 * Used alongside StatsRepository for the full stats picture. 404 * 405 * @param int|null $webhookId 406 * @param int $days 407 * @return array{error: int, retry: int, pending: int} 408 */ 409 public function getTransientStats(?int $webhookId = null, int $days = 7): array { 410 global $wpdb; 411 412 $dateFrom = gmdate('Y-m-d H:i:s', strtotime("-{$days} days")); 413 $whereWebhook = $webhookId 414 ? $wpdb->prepare('AND webhook_id = %d', $webhookId) 415 : ''; 416 417 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter 418 $rows = $wpdb->get_results( 419 $wpdb->prepare( 420 "SELECT status, COUNT(*) as count 421 FROM {$this->logsTable} 422 WHERE status IN ('error', 'retry', 'pending') 423 AND created_at >= %s {$whereWebhook} 424 GROUP BY status", 425 $dateFrom 426 ), 427 ARRAY_A 428 ); 429 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter 430 431 $result = ['error' => 0, 'retry' => 0, 'pending' => 0]; 432 foreach ($rows as $row) { 433 if (array_key_exists($row['status'], $result)) { 434 $result[$row['status']] = (int) $row['count']; 435 } 436 } 437 438 return $result; 439 } 440 441 /** 397 442 * Count total logs 398 443 * -
flowsystems-webhook-actions/trunk/src/Services/Dispatcher.php
r3471792 r3477165 349 349 'status' => 'error', 350 350 'error_message' => (string) $errorMessage, 351 'response_body' => null, 351 352 'duration_ms' => $durationMs, 352 353 'should_retry' => true, … … 380 381 381 382 if ($logId !== null) { 383 $parsedBody = json_decode($responseBody, true); 382 384 $this->logService->appendAttemptHistory($logId, [ 383 385 'attempt' => $attemptNumber, … … 386 388 'status' => $success ? 'success' : 'error', 387 389 'error_message' => $success ? null : sprintf("HTTP %d", $responseCode), 390 'response_body' => $parsedBody !== null ? $parsedBody : ($responseBody !== '' ? $responseBody : null), 388 391 'duration_ms' => $durationMs, 389 392 'should_retry' => $shouldRetry, -
flowsystems-webhook-actions/trunk/src/Services/LogArchiver.php
r3472060 r3477165 107 107 FROM {$logsTable} 108 108 WHERE created_at < %s AND status IN ('success', 'error', 'permanently_failed') 109 AND stats_recorded = 0 109 110 GROUP BY status", 110 111 $date -
flowsystems-webhook-actions/trunk/src/Services/LogService.php
r3471792 r3477165 6 6 7 7 use FlowSystems\WebhookActions\Repositories\LogRepository; 8 use FlowSystems\WebhookActions\Repositories\StatsRepository; 8 9 9 10 class LogService { 10 private LogRepository $repository; 11 private LogRepository $repository; 12 private StatsRepository $statsRepository; 11 13 12 14 public function __construct() { 13 $this->repository = new LogRepository(); 15 $this->repository = new LogRepository(); 16 $this->statsRepository = new StatsRepository(); 14 17 } 15 18 … … 66 69 int $durationMs 67 70 ) { 68 return $this->repository->create([ 69 'webhook_id' => $webhookId, 70 'trigger_name' => $triggerName, 71 'status' => 'success', 72 'http_code' => $httpCode, 73 'request_payload' => $payload, 74 'response_body' => $responseBody, 75 'duration_ms' => $durationMs, 76 ]); 71 $logId = $this->repository->create([ 72 'webhook_id' => $webhookId, 73 'trigger_name' => $triggerName, 74 'status' => 'success', 75 'http_code' => $httpCode, 76 'request_payload' => $payload, 77 'response_body' => $responseBody, 78 'duration_ms' => $durationMs, 79 'stats_recorded' => 1, 80 ]); 81 82 if ($logId) { 83 $this->statsRepository->record( 84 gmdate('Y-m-d'), 85 $webhookId, 86 $triggerName, 87 'success', 88 $durationMs, 89 $httpCode 90 ); 91 } 92 93 return $logId; 77 94 } 78 95 … … 142 159 */ 143 160 public function updateLog(int $logId, array $data): bool { 144 return $this->repository->update($logId, $data); 161 $result = $this->repository->update($logId, $data); 162 163 // Record terminal outcome in persistent stats the first time only. 164 // stats_recorded=1 gates replays and duplicate updates. 165 if ($result && isset($data['status']) && in_array($data['status'], ['success', 'permanently_failed'], true)) { 166 $log = $this->repository->find($logId); 167 if ($log && !(bool) ($log['stats_recorded'] ?? false)) { 168 $this->statsRepository->record( 169 gmdate('Y-m-d', strtotime($log['created_at'])), 170 (int) $log['webhook_id'], 171 (string) $log['trigger_name'], 172 $data['status'], 173 isset($data['duration_ms']) ? (int) $data['duration_ms'] : null, 174 isset($data['http_code']) ? (int) $data['http_code'] : null 175 ); 176 $this->repository->update($logId, ['stats_recorded' => 1]); 177 } 178 } 179 180 return $result; 145 181 } 146 182 … … 161 197 162 198 $history = is_array($log['attempt_history']) ? $log['attempt_history'] : []; 199 $attemptData['attempt'] = empty($history) ? 0 : max(array_column($history, 'attempt')) + 1; 163 200 $history[] = $attemptData; 164 165 $maxAttempts = (int) apply_filters('fswa_max_attempts', 5);166 if (count($history) > $maxAttempts) {167 $history = array_slice($history, -$maxAttempts);168 }169 201 170 202 return $this->repository->update($logId, ['attempt_history' => $history]); -
flowsystems-webhook-actions/trunk/src/Services/QueueService.php
r3471792 r3477165 106 106 107 107 /** 108 * Mark a job as failed109 *110 * @param int $jobId111 */112 public function markFailed(int $jobId): void {113 $this->repository->update($jobId, [114 'status' => 'failed',115 'locked_at' => null,116 'locked_by' => null,117 ]);118 }119 120 /**121 108 * Mark a job as permanently failed (non-retryable or max attempts exceeded) 122 109 * … … 193 180 'processing' => 0, 194 181 'completed' => 0, 195 'failed' => 0,196 182 'permanently_failed' => 0, 197 183 'total' => 0, … … 275 261 $job = $this->repository->find($jobId); 276 262 277 if (!$job || !in_array($job['status'], [' failed', 'permanently_failed'], true)) {263 if (!$job || !in_array($job['status'], ['pending', 'permanently_failed'], true)) { 278 264 return false; 279 265 }
Note: See TracChangeset
for help on using the changeset viewer.