$event */ public static function append( array $event ): bool { global $wpdb; try { CheckoutMonitoringSchema::ensure_installed(); if ( ! self::table_exists() ) { self::append_legacy_option( $event ); return true; } self::safe_maybe_import_legacy_events(); $flow_run_id = CheckoutFlowRunRepository::upsert_from_event( $event ); $inserted = $wpdb->insert( self::table_name(), [ 'event_uuid' => sanitize_key( (string) ( $event['id'] ?? '' ) ), 'flow_run_id' => $flow_run_id, 'trace_id' => sanitize_key( (string) ( $event['traceId'] ?? '' ) ), 'origin' => sanitize_key( (string) ( $event['origin'] ?? '' ) ), 'event' => sanitize_key( (string) ( $event['event'] ?? '' ) ), 'step' => sanitize_key( (string) ( $event['step'] ?? '' ) ), 'status' => sanitize_key( (string) ( $event['status'] ?? '' ) ), 'gateway_id' => sanitize_key( (string) ( $event['gatewayId'] ?? '' ) ), 'provider_key' => sanitize_key( (string) ( $event['providerKey'] ?? '' ) ), 'message' => sanitize_text_field( (string) ( $event['message'] ?? '' ) ), 'context_json' => wp_json_encode( is_array( $event['context'] ?? null ) ? $event['context'] : [] ), 'created_at' => self::normalize_mysql_datetime( (string) ( $event['createdAt'] ?? '' ) ), ], [ '%s', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' ] ); if ( false === $inserted ) { return false; } self::safe_prune_after_append(); return true; } catch ( \Throwable $error ) { return false; } } /** * @return array> */ public static function recent( int $limit = 50 ): array { global $wpdb; $limit = max( 1, $limit ); CheckoutMonitoringSchema::ensure_installed(); if ( ! self::table_exists() ) { return array_values( array_slice( self::legacy_all(), 0, $limit ) ); } self::maybe_import_legacy_events(); $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i ORDER BY created_at DESC, id DESC LIMIT %d', self::table_name(), $limit ), ARRAY_A ); if ( ! is_array( $rows ) ) { return []; } return array_map( [ __CLASS__, 'map_row_to_event' ], $rows ); } public static function clear(): void { global $wpdb; CheckoutMonitoringSchema::ensure_installed(); if ( self::table_exists() ) { $wpdb->query( $wpdb->prepare( 'DELETE FROM %i', self::table_name() ) ); } CheckoutFlowRunRepository::clear(); update_option( self::FALLBACK_OPTION_KEY, [], false ); } /** * @return array */ public static function summary( int $limit = 50 ): array { $events = self::recent( $limit ); $summary = [ 'total' => count( $events ), 'errors' => 0, 'warnings' => 0, 'frontend' => 0, 'backend' => 0, 'latestEventAt' => '', 'gatewayCounts' => [], ]; foreach ( $events as $event ) { $status = (string) ( $event['status'] ?? '' ); $origin = (string) ( $event['origin'] ?? '' ); $gateway_id = (string) ( $event['gatewayId'] ?? '' ); if ( in_array( $status, [ 'error', 'failed', 'critical' ], true ) ) { $summary['errors']++; } elseif ( 'warning' === $status ) { $summary['warnings']++; } if ( 'frontend' === $origin ) { $summary['frontend']++; } else { $summary['backend']++; } if ( '' === $summary['latestEventAt'] && ! empty( $event['createdAt'] ) ) { $summary['latestEventAt'] = (string) $event['createdAt']; } if ( '' !== $gateway_id ) { if ( ! isset( $summary['gatewayCounts'][ $gateway_id ] ) ) { $summary['gatewayCounts'][ $gateway_id ] = 0; } $summary['gatewayCounts'][ $gateway_id ]++; } } return $summary; } /** * @return array> */ private static function legacy_all(): array { $events = \WooOrderCart\Config\CartConfig::read_owned_option( self::FALLBACK_OPTION_KEY, [] ); return is_array( $events ) ? array_values( $events ) : []; } public static function table_name(): string { return ( new Checkout_TraceEvents() )->get_name(); } public static function table_exists(): bool { global $wpdb; $table = self::table_name(); return $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) === $table; } /** * @param array $event */ private static function append_legacy_option( array $event ): void { $events = self::legacy_all(); array_unshift( $events, $event ); $events = array_slice( $events, 0, CheckoutTraceConfig::max_events() ); update_option( self::FALLBACK_OPTION_KEY, $events, false ); } private static function maybe_import_legacy_events(): void { if ( self::$legacy_import_checked ) { return; } self::$legacy_import_checked = true; $legacy_events = self::legacy_all(); if ( [] === $legacy_events ) { return; } global $wpdb; $has_rows = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(1) FROM %i', self::table_name() ) ); if ( $has_rows > 0 ) { return; } foreach ( array_reverse( $legacy_events ) as $legacy_event ) { if ( ! is_array( $legacy_event ) ) { continue; } $flow_run_id = CheckoutFlowRunRepository::upsert_from_event( $legacy_event ); $wpdb->insert( self::table_name(), [ 'event_uuid' => sanitize_key( (string) ( $legacy_event['id'] ?? '' ) ), 'flow_run_id' => $flow_run_id, 'trace_id' => sanitize_key( (string) ( $legacy_event['traceId'] ?? '' ) ), 'origin' => sanitize_key( (string) ( $legacy_event['origin'] ?? '' ) ), 'event' => sanitize_key( (string) ( $legacy_event['event'] ?? '' ) ), 'step' => sanitize_key( (string) ( $legacy_event['step'] ?? '' ) ), 'status' => sanitize_key( (string) ( $legacy_event['status'] ?? '' ) ), 'gateway_id' => sanitize_key( (string) ( $legacy_event['gatewayId'] ?? '' ) ), 'provider_key' => sanitize_key( (string) ( $legacy_event['providerKey'] ?? '' ) ), 'message' => sanitize_text_field( (string) ( $legacy_event['message'] ?? '' ) ), 'context_json' => wp_json_encode( is_array( $legacy_event['context'] ?? null ) ? $legacy_event['context'] : [] ), 'created_at' => self::normalize_mysql_datetime( (string) ( $legacy_event['createdAt'] ?? '' ) ), ], [ '%s', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' ] ); } self::prune_to_max_events(); CheckoutFlowRunRepository::prune_to_max_runs( CheckoutTraceConfig::max_events() ); } private static function safe_maybe_import_legacy_events(): void { try { self::maybe_import_legacy_events(); } catch ( \Throwable $error ) { self::$legacy_import_checked = true; } } private static function safe_prune_after_append(): void { try { self::prune_to_max_events(); CheckoutFlowRunRepository::prune_to_max_runs( CheckoutTraceConfig::max_events() ); } catch ( \Throwable $error ) { return; } } private static function prune_to_max_events(): void { global $wpdb; if ( ! self::table_exists() ) { return; } $max_events = CheckoutTraceConfig::max_events(); $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE id NOT IN (SELECT id FROM (SELECT id FROM %i ORDER BY created_at DESC, id DESC LIMIT %d) AS ips_trace_keep)', self::table_name(), self::table_name(), $max_events ) ); } /** * @param array $row * @return array */ private static function map_row_to_event( array $row ): array { $context = json_decode( (string) ( $row['context_json'] ?? '' ), true ); return [ 'id' => (string) ( $row['event_uuid'] ?? '' ), 'traceId' => (string) ( $row['trace_id'] ?? '' ), 'event' => (string) ( $row['event'] ?? '' ), 'step' => (string) ( $row['step'] ?? '' ), 'status' => (string) ( $row['status'] ?? '' ), 'origin' => (string) ( $row['origin'] ?? '' ), 'gatewayId' => (string) ( $row['gateway_id'] ?? '' ), 'providerKey' => (string) ( $row['provider_key'] ?? '' ), 'message' => (string) ( $row['message'] ?? '' ), 'createdAt' => self::mysql_to_iso( (string) ( $row['created_at'] ?? '' ) ), 'context' => is_array( $context ) ? $context : [], ]; } private static function normalize_mysql_datetime( string $value ): string { $timestamp = strtotime( $value ); if ( false === $timestamp ) { return current_time( 'mysql', true ); } return gmdate( 'Y-m-d H:i:s', $timestamp ); } private static function mysql_to_iso( string $value ): string { $timestamp = strtotime( $value . ' UTC' ); if ( false === $timestamp ) { return gmdate( 'c' ); } return gmdate( 'c', $timestamp ); } }