Plugin Directory

Changeset 3477165


Ignore:
Timestamp:
03/07/2026 08:41:29 PM (4 weeks ago)
Author:
mateuszflowsystems
Message:

Release 1.2.0

Location:
flowsystems-webhook-actions
Files:
3 added
3 deleted
14 edited

Legend:

Unmodified
Added
Removed
  • flowsystems-webhook-actions/trunk/README.txt

    r3472060 r3477165  
    11=== Flow Systems Webhook Actions ===
    22Contributors: mateuszflowsystems
    3 Tags: webhook, woocommerce, automation, n8n, integration
     3Tags: webhook, automation, integration, n8n, api
    44Requires at least: 6.0
    55Tested up to: 6.9
    66Requires PHP: 8.0
    7 Stable tag: 1.1.1
     7Stable tag: 1.2.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
    1010
    11 Production-safe WordPress webhooks with retries, event IDs, queue processing, and full delivery observability.
     11Reliable WordPress webhooks for automation workflows with retries, delivery logs, event IDs, queue processing, and replayable webhook events.
    1212
    1313== Description ==
     
    6363No silent failures.
    6464
     65= Replay Webhook Events =
     66
     67Webhook debugging is difficult when events cannot be reproduced.
     68
     69Flow Systems Webhook Actions allows you to replay any webhook event directly from the delivery logs — including successful deliveries.
     70
     71This 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
     78Each replay uses the original payload and event metadata, ensuring consistent behavior across retries and debugging sessions.
     79
    6580= Delivery Observability =
    6681
     
    7186- Attempt timeline per event
    7287- HTTP status codes and response bodies
     88- Inspect full request payloads
    7389- Manual retry (single or bulk)
     90- Replay webhook events for debugging and testing integrations
    7491
    7592Filter by: event UUID, target URL, date range, status
     
    113130- Persistent queue
    114131- Smart retry logic
     132- Webhook replay for debugging integrations
    115133- Permanent failure state handling
    116134- Event UUIDs and timestamps
     
    162180Failed 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.
    163181
     182= Can I replay webhook events? =
     183
     184Yes. Every webhook delivery is logged with its payload and attempt history.
     185You can replay any event directly from the logs, which is useful for debugging integrations or re-running automation workflows.
     186
    164187= What is Payload Mapping? =
    165188
     
    1802033. Selecting WordPress action triggers
    1812044. Payload mapping configuration
    182 5. Webhook delivery logs
     2055. Webhook delivery logs with replay and retry controls
    1832066. Queue status overview
    1842077. Settings configuration screen
     
    186209== Changelog ==
    187210
    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 =
    189222- Fixed `permanently_failed` entries being excluded from total and error delivery statistics in `getStats()`, `getAllTimeStats()`, and `LogArchiver::aggregateStatsBeforeDeletion()`
    190223
    191 = 1.1.0 =
     224= 1.1.0 — 2026-02-28 =
    192225- 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}`
    193226- 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
     
    202235- Updated footer with a review prompt linking to WordPress.org
    203236
    204 = 1.0.1 =
     237= 1.0.1 — 2026-02-18 =
    205238- Fixed preview freezing when mapping fields from objects with numeric string keys (e.g. WooCommerce line_items)
    206239- 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
     
    209242- Improved log details display with word break for long trigger names and dates
    210243
    211 = 1.0.0 =
     244= 1.0.0 — 2026-02-16 =
    212245- Initial release
    213246- Webhook dispatching from WordPress actions
     
    224257This 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.
    225258
    226 = 1.0.0 =
     259= 1.0.0 — 2026-02-16 =
    227260Initial stable release.
  • flowsystems-webhook-actions/trunk/admin/dist/.vite/manifest.json

    r3472060 r3477165  
    11{
    22  "src/main.js": {
    3     "file": "assets/main-DjkfZCt4.js",
     3    "file": "assets/main-oBfATGcf.js",
    44    "name": "main",
    55    "src": "src/main.js",
     
    77  },
    88  "style.css": {
    9     "file": "assets/style-CckM26yn.css",
     9    "file": "assets/style-BckjMVPE.css",
    1010    "src": "style.css"
    1111  }
  • flowsystems-webhook-actions/trunk/flowsystems-webhook-actions.php

    r3472060 r3477165  
    44 * Plugin URI: https://flowsystems.pl/wordpress-webhook-actions
    55 * Description: Trigger HTTP webhooks from WordPress actions (do_action). Easily connect WordPress with n8n, Zapier, Make, or custom workflows.
    6  * Version: 1.1.1
     6 * Version: 1.2.0
    77 * Author: Mateusz Skorupa
    88 * Author URI: https://flowsystems.pl
     
    1717defined('ABSPATH') || exit;
    1818
    19 define('FSWA_VERSION', '1.1.1');
     19define('FSWA_VERSION', '1.2.0');
    2020define('FSWA_FILE', __FILE__);
    2121
  • flowsystems-webhook-actions/trunk/src/Activation.php

    r3471792 r3477165  
    2424    $webhooksTable = $wpdb->prefix . 'fswa_webhooks';
    2525    $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';
    2829
    2930    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
     
    7374            attempt_history LONGTEXT DEFAULT NULL,
    7475            next_attempt_at DATETIME DEFAULT NULL,
     76            stats_recorded TINYINT(1) NOT NULL DEFAULT 0,
    7577            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    7678            PRIMARY KEY (id),
     
    7981            KEY idx_created (created_at),
    8082            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)
    8285        ) {$charsetCollate};";
    8386
     
    107110    dbDelta($sqlQueue);
    108111
    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');
    110130  }
    111131
     
    156176    // Drop tables (order matters for foreign key constraints)
    157177    // 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");
    158179    $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}fswa_queue");
    159180    $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}fswa_trigger_schemas");
  • flowsystems-webhook-actions/trunk/src/Api/HealthController.php

    r3471792 r3477165  
    99use WP_REST_Response;
    1010use FlowSystems\WebhookActions\Repositories\LogRepository;
     11use FlowSystems\WebhookActions\Repositories\StatsRepository;
    1112use FlowSystems\WebhookActions\Repositories\WebhookRepository;
    1213use FlowSystems\WebhookActions\Services\QueueService;
     
    1718  protected $rest_base = 'health';
    1819
    19   private LogRepository $logRepository;
     20  private LogRepository   $logRepository;
    2021  private WebhookRepository $webhookRepository;
    21   private QueueService $queueService;
    22   private StatsService $statsService;
     22  private QueueService    $queueService;
     23  private StatsService    $statsService;
     24  private StatsRepository $statsRepository;
    2325
    2426  public function __construct() {
    25     $this->logRepository = new LogRepository();
     27    $this->logRepository   = new LogRepository();
    2628    $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();
    2932  }
    3033
     
    5356   */
    5457  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();
    5760
    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)
    5962    $archivedStats = $this->statsService->getArchivedStats();
    6063
    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'];
    6567
    6668    // Calculate success rate from all-time data
     
    7173      : 0.0;
    7274
    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    ];
    7586
    7687    // Get webhook counts
     
    106117        'processing' => $queueStats['processing'],
    107118        'completed' => $queueStats['completed'],
    108         'failed' => $queueStats['failed'],
    109119        'permanently_failed' => $queueStats['permanently_failed'] ?? 0,
    110120        'total' => $queueStats['total'],
  • flowsystems-webhook-actions/trunk/src/Api/LogsController.php

    r3471792 r3477165  
    1212use FlowSystems\WebhookActions\Repositories\LogRepository;
    1313use FlowSystems\WebhookActions\Repositories\QueueRepository;
     14use FlowSystems\WebhookActions\Repositories\StatsRepository;
     15use FlowSystems\WebhookActions\Repositories\WebhookRepository;
    1416use FlowSystems\WebhookActions\Services\QueueService;
    1517
     
    1820  protected $rest_base = 'logs';
    1921
    20   private LogRepository $repository;
     22  private LogRepository   $repository;
    2123  private QueueRepository $queueRepository;
    22   private QueueService $queueService;
     24  private WebhookRepository $webhookRepository;
     25  private QueueService    $queueService;
     26  private StatsRepository $statsRepository;
    2327
    2428  public function __construct() {
    25     $this->repository = new LogRepository();
     29    $this->repository      = new LogRepository();
    2630    $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();
    2834  }
    2935
     
    7682      ],
    7783    ]);
     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    ]]);
    7891
    7992    // Bulk retry queue jobs associated with multiple logs
     
    294307    }
    295308
    296     if (!in_array($job['status'], ['failed', 'permanently_failed'], true)) {
     309    if (!in_array($log['status'], ['error', 'permanently_failed'], true)) {
    297310      return new WP_Error(
    298311        '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'),
    300313        ['status' => 409]
    301314      );
     
    315328      'success' => true,
    316329      '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,
    317382    ]);
    318383  }
     
    330395      $job = $this->queueRepository->findByLogId($logId);
    331396
    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) {
    333400        $skipped++;
    334401        continue;
     
    370437   */
    371438  public function getStats($request): WP_REST_Response {
    372     $days = (int) ($request->get_param('days') ?: 7);
     439    $days      = (int) ($request->get_param('days') ?: 7);
    373440    $webhookId = $request->get_param('webhook_id') ? (int) $request->get_param('webhook_id') : null;
    374441
    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);
    378463  }
    379464
  • flowsystems-webhook-actions/trunk/src/Api/QueueController.php

    r3471792 r3477165  
    335335    }
    336336
    337     if (!in_array($job['status'], ['failed', 'permanently_failed'], true)) {
     337    if (!in_array($job['status'], ['pending', 'permanently_failed'], true)) {
    338338      return new WP_Error(
    339339        'rest_job_not_retryable',
    340         __('Only failed or permanently failed jobs can be retried.', 'flowsystems-webhook-actions'),
     340        __('Only pending or permanently failed jobs can be retried.', 'flowsystems-webhook-actions'),
    341341        ['status' => 409]
    342342      );
  • flowsystems-webhook-actions/trunk/src/Database/Migrator.php

    r3471792 r3477165  
    55class Migrator {
    66  private const OPTION_KEY = 'fswa_db_version';
    7   private const CURRENT_VERSION = '1.1.0';
     7  private const CURRENT_VERSION = '1.2.0';
    88
    99  /**
     
    4444      $wpdb->prefix . 'fswa_queue',
    4545      $wpdb->prefix . 'fswa_trigger_schemas',
     46      $wpdb->prefix . 'fswa_stats',
    4647    ];
    4748
     
    6667      '1.0.0' => [self::class, 'migration_1_0_0'],
    6768      '1.1.0' => [self::class, 'migration_1_1_0'],
     69      '1.2.0' => [self::class, 'migration_1_2_0'],
    6870    ];
    6971  }
     
    223225
    224226  /**
     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  /**
    225267   * Get current database version
    226268   */
  • flowsystems-webhook-actions/trunk/src/Repositories/LogRepository.php

    r3472060 r3477165  
    260260    }
    261261
     262    if (isset($data['stats_recorded'])) {
     263      $updateData['stats_recorded'] = (int) $data['stats_recorded'];
     264      $format[] = '%d';
     265    }
     266
    262267    if (empty($updateData)) {
    263268      return true;
     
    395400
    396401  /**
     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  /**
    397442   * Count total logs
    398443   *
  • flowsystems-webhook-actions/trunk/src/Services/Dispatcher.php

    r3471792 r3477165  
    349349          'status'        => 'error',
    350350          'error_message' => (string) $errorMessage,
     351          'response_body' => null,
    351352          'duration_ms'   => $durationMs,
    352353          'should_retry'  => true,
     
    380381
    381382    if ($logId !== null) {
     383      $parsedBody = json_decode($responseBody, true);
    382384      $this->logService->appendAttemptHistory($logId, [
    383385        'attempt'       => $attemptNumber,
     
    386388        'status'        => $success ? 'success' : 'error',
    387389        'error_message' => $success ? null : sprintf("HTTP %d", $responseCode),
     390        'response_body' => $parsedBody !== null ? $parsedBody : ($responseBody !== '' ? $responseBody : null),
    388391        'duration_ms'   => $durationMs,
    389392        'should_retry'  => $shouldRetry,
  • flowsystems-webhook-actions/trunk/src/Services/LogArchiver.php

    r3472060 r3477165  
    107107         FROM {$logsTable}
    108108         WHERE created_at < %s AND status IN ('success', 'error', 'permanently_failed')
     109           AND stats_recorded = 0
    109110         GROUP BY status",
    110111        $date
  • flowsystems-webhook-actions/trunk/src/Services/LogService.php

    r3471792 r3477165  
    66
    77use FlowSystems\WebhookActions\Repositories\LogRepository;
     8use FlowSystems\WebhookActions\Repositories\StatsRepository;
    89
    910class LogService {
    10   private LogRepository $repository;
     11  private LogRepository   $repository;
     12  private StatsRepository $statsRepository;
    1113
    1214  public function __construct() {
    13     $this->repository = new LogRepository();
     15    $this->repository      = new LogRepository();
     16    $this->statsRepository = new StatsRepository();
    1417  }
    1518
     
    6669    int $durationMs
    6770  ) {
    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;
    7794  }
    7895
     
    142159   */
    143160  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;
    145181  }
    146182
     
    161197
    162198    $history = is_array($log['attempt_history']) ? $log['attempt_history'] : [];
     199    $attemptData['attempt'] = empty($history) ? 0 : max(array_column($history, 'attempt')) + 1;
    163200    $history[] = $attemptData;
    164 
    165     $maxAttempts = (int) apply_filters('fswa_max_attempts', 5);
    166     if (count($history) > $maxAttempts) {
    167       $history = array_slice($history, -$maxAttempts);
    168     }
    169201
    170202    return $this->repository->update($logId, ['attempt_history' => $history]);
  • flowsystems-webhook-actions/trunk/src/Services/QueueService.php

    r3471792 r3477165  
    106106
    107107  /**
    108    * Mark a job as failed
    109    *
    110    * @param int $jobId
    111    */
    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   /**
    121108   * Mark a job as permanently failed (non-retryable or max attempts exceeded)
    122109   *
     
    193180      'processing' => 0,
    194181      'completed' => 0,
    195       'failed' => 0,
    196182      'permanently_failed' => 0,
    197183      'total' => 0,
     
    275261    $job = $this->repository->find($jobId);
    276262
    277     if (!$job || !in_array($job['status'], ['failed', 'permanently_failed'], true)) {
     263    if (!$job || !in_array($job['status'], ['pending', 'permanently_failed'], true)) {
    278264      return false;
    279265    }
Note: See TracChangeset for help on using the changeset viewer.