PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.2
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.2
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
← All changes | includes/class-metasync-heartbeat-manager.php +47 -5 2.7.0 → 2.7.2 View file →
@@ -17,8 +17,11 @@
17 17 {
18 18 /** @var self|null */
19 19 private static $instance = null;
20 20
21 + /** @var array Failure class of the last heartbeat test, for analytics. */
22 + private $last_failure = array();
23 +
21 24 /**
22 25 * Get singleton instance.
23 26 *
24 27 * @return self
@@ -552,9 +555,9 @@
552 555 }
553 556 $log_message .= ' | ' . implode(', ', $context_parts);
554 557 }
555 558
556 - error_log($log_message);
559 + error_log($log_message); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
557 560 }
558 561
559 562 public function clear_public_hash_cache($otto_pixel_uuid = '')
560 563 {
@@ -626,9 +629,9 @@
626 629 }
627 630 $message .= ' | ' . implode(', ', $details_formatted);
628 631 }
629 632
630 - error_log($message);
633 + error_log($message); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
631 634
632 635 if ($level === 'error' || $level === 'critical') {
633 636 $this->store_heartbeat_error_log($event, $details);
634 637 }
@@ -646,9 +649,9 @@
646 649 if ($last_space !== false && $last_space > $length * 0.75) {
647 650 $truncated = substr($truncated, 0, $last_space);
648 651 }
649 652
650 - $truncated = strip_tags($truncated);
653 + $truncated = wp_strip_all_tags($truncated);
651 654 return $truncated . '... [truncated]';
652 655 }
653 656
654 657 private function should_throttle_log($level, $event, $details = array())
@@ -695,9 +698,9 @@
695 698 'created_at' => current_time('mysql')
696 699 ));
697 700 }
698 701 } catch (Exception $e) {
699 - error_log('Failed to store heartbeat error log: ' . $e->getMessage());
702 + error_log('Failed to store heartbeat error log: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
700 703 }
701 704 }
702 705
703 706 // ------------------------------------------------------------------
@@ -718,10 +721,12 @@
718 721 $sync_request = new Metasync_Sync_Requests();
719 722 $response = $sync_request->SyncCustomerParams($apikey);
720 723
721 724 $request_duration = round((microtime(true) - $start_time) * 1000, 2);
725 + $this->last_failure = array();
722 726
723 727 if (is_wp_error($response)) {
728 + $this->last_failure = array('error_class' => 'transport', 'status_code' => 0);
724 729 $this->log_heartbeat('error', 'Heartbeat test via SyncCustomerParams failed', array(
725 730 'error_code' => $response->get_error_code(),
726 731 'error_message' => $response->get_error_message(),
727 732 'request_duration_ms' => $request_duration,
@@ -733,8 +738,13 @@
733 738 $status_code = wp_remote_retrieve_response_code($response);
734 739 $body = wp_remote_retrieve_body($response);
735 740
736 741 if ($status_code !== 200) {
742 + // SyncCustomerParams() returns null for transport errors and non-200
743 + // replies alike, so only a real response carries a status to report.
744 + $this->last_failure = is_array($response)
745 + ? array('error_class' => 'http', 'status_code' => (int) $status_code)
746 + : array('error_class' => 'no_response', 'status_code' => 0);
737 747 $this->log_heartbeat('error', 'Heartbeat test returned non-200 status', array(
738 748 'status_code' => $status_code,
739 749 'response_body' => $this->smart_truncate($body, 300),
740 750 'request_duration_ms' => $request_duration,
@@ -975,14 +985,36 @@
975 985 );
976 986
977 987 set_transient('metasync_heartbeat_status_cache', $cache_data, 300);
978 988
989 + $was_connected = $this->get_last_known_connection_state();
979 990 $this->set_last_known_connection_state($is_connected);
991 + $this->track_connection_change($was_connected, $is_connected);
980 992
981 993 return $is_connected;
982 994 }
983 995
984 996 /**
997 + * Consent-gated analytics: only the connected -> disconnected edge, so a
998 + * site that stays down does not report on every cron tick.
999 + *
1000 + * @param mixed $was_connected Stored state before this check. The stored
1001 + * bool comes back from the DB as '1' / '',
1002 + * hence the loose check.
1003 + * @param bool $is_connected Result of this check.
1004 + */
1005 + private function track_connection_change($was_connected, $is_connected)
1006 + {
1007 + if (empty($was_connected) || $is_connected) {
1008 + return;
1009 + }
1010 + Metasync_PostHog::feature_once('connection_lost', array(
1011 + 'error_class' => $this->last_failure['error_class'] ?? 'unknown',
1012 + 'status_code' => $this->last_failure['status_code'] ?? 0,
1013 + ), 'heartbeat', DAY_IN_SECONDS);
1014 + }
1015 +
1016 + /**
985 1017 * Add custom cron schedule for 2-hour intervals and daily cleanup.
986 1018 */
987 1019 public function add_heartbeat_cron_schedule($schedules)
988 1020 {
@@ -987,32 +1019,38 @@
987 1019 public function add_heartbeat_cron_schedule($schedules)
988 1020 {
989 1021 $schedules['metasync_every_2_hours'] = array(
990 1022 'interval' => 2 * HOUR_IN_SECONDS,
1023 + /* translators: %s: effective plugin name (whitelabel-aware). */
991 1024 'display' => esc_html(sprintf(__('Every 2 Hours (%s)', 'metasync'), Metasync::get_effective_plugin_name()))
992 1025 );
993 1026
994 1027 $schedules['metasync_every_2_minutes'] = array(
995 1028 'interval' => 2 * MINUTE_IN_SECONDS,
1029 + /* translators: %s: effective plugin name (whitelabel-aware). */
996 1030 'display' => esc_html(sprintf(__('Every 2 Minutes (%s Burst)', 'metasync'), Metasync::get_effective_plugin_name()))
997 1031 );
998 1032 $schedules['metasync_every_5_minutes'] = array(
999 1033 'interval' => 5 * MINUTE_IN_SECONDS,
1034 + /* translators: %s: effective plugin name (whitelabel-aware). */
1000 1035 'display' => esc_html(sprintf(__('Every 5 Minutes (%s)', 'metasync'), Metasync::get_effective_plugin_name()))
1001 1036 );
1002 1037 // 10-minute heartbeat cadence used for UNREGISTERED + KEY_PENDING short-interval behavior
1003 1038 $schedules['metasync_every_10_minutes'] = array(
1004 1039 'interval' => 10 * MINUTE_IN_SECONDS,
1040 + /* translators: %s: effective plugin name (whitelabel-aware). */
1005 1041 'display' => esc_html(sprintf(__('Every 10 Minutes (%s)', 'metasync'), Metasync::get_effective_plugin_name()))
1006 1042 );
1007 1043
1008 1044 $schedules['metasync_daily_cleanup'] = array(
1009 1045 'interval' => DAY_IN_SECONDS,
1046 + /* translators: %s: effective plugin name (whitelabel-aware). */
1010 1047 'display' => esc_html(sprintf(__('Daily (%s Cleanup)', 'metasync'), Metasync::get_effective_plugin_name()))
1011 1048 );
1012 1049
1013 1050 $schedules['metasync_weekly'] = array(
1014 1051 'interval' => 7 * DAY_IN_SECONDS,
1052 + /* translators: %s: effective plugin name (whitelabel-aware). */
1015 1053 'display' => esc_html(sprintf(__('Weekly (%s)', 'metasync'), Metasync::get_effective_plugin_name()))
1016 1054 );
1017 1055
1018 1056 return $schedules;
@@ -1069,9 +1107,13 @@
1069 1107 $general = $options['general'] ?? [];
1070 1108 $general['heartbeat_state'] = 'CONNECTED';
1071 1109 $general['heartbeat_state_changed_at'] = time();
1072 1110
1073 - if (!empty($ping['otto_pixel_uuid']) && (empty($general['otto_pixel_uuid']) || $general['otto_pixel_uuid'] !== $ping['otto_pixel_uuid'])) {
1111 + // The UUID comes from the Search Atlas connection ping, but it is
1112 + // still gated by the shared UUID check: a non-UUID (username or
1113 + // other arbitrary text) must never be persisted here either.
1114 + if (!empty($ping['otto_pixel_uuid']) && Metasync::is_valid_uuid($ping['otto_pixel_uuid'])
1115 + && (empty($general['otto_pixel_uuid']) || $general['otto_pixel_uuid'] !== $ping['otto_pixel_uuid'])) {
1074 1116 $general['otto_pixel_uuid'] = sanitize_text_field($ping['otto_pixel_uuid']);
1075 1117 }
1076 1118
1077 1119 $options['general'] = $general;