diff --git a/assets/js/components/email-reporting/notices/EmailReportingErrorNotices.js b/assets/js/components/email-reporting/notices/EmailReportingErrorNotices.js
index 85a5e456bdc..8fcff7fb758 100644
--- a/assets/js/components/email-reporting/notices/EmailReportingErrorNotices.js
+++ b/assets/js/components/email-reporting/notices/EmailReportingErrorNotices.js
@@ -25,6 +25,7 @@ import useViewOnly from '@/js/hooks/useViewOnly';
import PermissionsErrorNotice from '@/js/components/email-reporting/notices/errors/PermissionsErrorNotice';
import ReportErrorNotice from '@/js/components/email-reporting/notices/errors/ReportErrorNotice';
import SendingErrorNotice from '@/js/components/email-reporting/notices/errors/SendingErrorNotice';
+import CronSchedulerErrorNotice from '@/js/components/email-reporting/notices/errors/CronSchedulerErrorNotice';
import ServerErrorNotice from '@/js/components/email-reporting/notices/errors/ServerErrorNotice';
export default function EmailReportingErrorNotices() {
@@ -66,6 +67,8 @@ export default function EmailReportingErrorNotices() {
);
case 'sending_error':
return ;
+ case 'cron_scheduler_error':
+ return ;
default:
return ;
}
diff --git a/assets/js/components/email-reporting/notices/EmailReportingErrorNotices.test.js b/assets/js/components/email-reporting/notices/EmailReportingErrorNotices.test.js
index 68f69d1171f..324930e14c0 100644
--- a/assets/js/components/email-reporting/notices/EmailReportingErrorNotices.test.js
+++ b/assets/js/components/email-reporting/notices/EmailReportingErrorNotices.test.js
@@ -172,6 +172,39 @@ describe( 'EmailReportingErrorNotices', () => {
).toBeInTheDocument();
} );
+ it( 'should render the cron scheduler error notice when there is a cron_scheduler_error category ID', () => {
+ registry.dispatch( CORE_SITE ).receiveGetEmailReportingSettings( {
+ enabled: true,
+ } );
+ registry.dispatch( CORE_SITE ).receiveGetEmailReportingErrors( {
+ errors: {
+ cron_scheduler_error: [ 'Cron issue.' ],
+ },
+ error_data: {
+ cron_scheduler_error: {
+ category_id: 'cron_scheduler_error',
+ },
+ },
+ } );
+
+ const { container, getByText } = render(
+ ,
+ {
+ registry,
+ }
+ );
+
+ expect( container ).not.toBeEmptyDOMElement();
+ expect(
+ getByText( 'Email reports are failing to send' )
+ ).toBeInTheDocument();
+ expect(
+ getByText(
+ 'We were unable to deliver your report, likely due to a WP-Cron configuration error in your WordPress site’s system settings. To fix this, contact your administrator or get help. Report delivery will automatically resume once the issue is resolved.'
+ )
+ ).toBeInTheDocument();
+ } );
+
it( 'should not render when email reporting is not enabled', () => {
registry.dispatch( CORE_SITE ).receiveGetEmailReportingSettings( {
enabled: false,
diff --git a/assets/js/components/email-reporting/notices/errors/CronSchedulerErrorNotice.js b/assets/js/components/email-reporting/notices/errors/CronSchedulerErrorNotice.js
new file mode 100644
index 00000000000..d9377291810
--- /dev/null
+++ b/assets/js/components/email-reporting/notices/errors/CronSchedulerErrorNotice.js
@@ -0,0 +1,57 @@
+/**
+ * CronSchedulerErrorNotice component.
+ *
+ * Site Kit by Google, Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import Notice from '@/js/components/Notice';
+import { TYPES } from '@/js/components/Notice/constants';
+import useNotificationEvents from '@/js/googlesitekit/notifications/hooks/useNotificationEvents';
+import withIntersectionObserver from '@/js/util/withIntersectionObserver';
+
+export const EMAIL_REPORTING_CRON_SCHEDULER_ERROR_NOTICE =
+ 'email_reporting_cron_scheduler_error_notice';
+
+const NoticeWithIntersectionObserver = withIntersectionObserver( Notice );
+
+export default function CronSchedulerErrorNotice() {
+ const trackEvents = useNotificationEvents(
+ EMAIL_REPORTING_CRON_SCHEDULER_ERROR_NOTICE
+ );
+
+ return (
+
+ );
+}
diff --git a/assets/js/components/email-reporting/notices/errors/CronSchedulerErrorNotice.test.js b/assets/js/components/email-reporting/notices/errors/CronSchedulerErrorNotice.test.js
new file mode 100644
index 00000000000..2a3c8bd0c92
--- /dev/null
+++ b/assets/js/components/email-reporting/notices/errors/CronSchedulerErrorNotice.test.js
@@ -0,0 +1,38 @@
+/**
+ * CronSchedulerErrorNotice component tests.
+ *
+ * Site Kit by Google, Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Internal dependencies
+ */
+import CronSchedulerErrorNotice from './CronSchedulerErrorNotice';
+import { render } from '../../../../../../tests/js/test-utils';
+
+describe( 'CronSchedulerErrorNotice', () => {
+ it( 'renders expected title and description', () => {
+ const { getByText } = render( );
+
+ expect(
+ getByText( 'Email reports are failing to send' )
+ ).toBeInTheDocument();
+ expect(
+ getByText(
+ 'We were unable to deliver your report, likely due to a WP-Cron configuration error in your WordPress site’s system settings. To fix this, contact your administrator or get help. Report delivery will automatically resume once the issue is resolved.'
+ )
+ ).toBeInTheDocument();
+ } );
+} );
diff --git a/assets/sass/components/email-reporting/_googlesitekit-user-settings-selection-panel.scss b/assets/sass/components/email-reporting/_googlesitekit-user-settings-selection-panel.scss
index cdd7edd5a1b..8abc4fcf798 100644
--- a/assets/sass/components/email-reporting/_googlesitekit-user-settings-selection-panel.scss
+++ b/assets/sass/components/email-reporting/_googlesitekit-user-settings-selection-panel.scss
@@ -41,7 +41,6 @@
// Ensure every notice after the first notice has a margin
// so the notices don't render with no gap.
- //
// See: https://github.com/google/site-kit-wp/issues/12279
.googlesitekit-notice-container:nth-of-type(n + 2) {
margin-top: $grid-gap-phone;
diff --git a/includes/Core/Email_Reporting/Content_Map.php b/includes/Core/Email_Reporting/Content_Map.php
index a0b0381547a..4d968eb6f73 100644
--- a/includes/Core/Email_Reporting/Content_Map.php
+++ b/includes/Core/Email_Reporting/Content_Map.php
@@ -107,9 +107,10 @@ function ( $paragraph ) use ( $args ) {
protected static function get_all_titles() {
return array(
/* translators: 1: opening anchor tag with mailto link, 2: inviter email address, 3: closing anchor tag */
- 'invitation-email' => __( '%1$s%2$s%3$s invited you to receive periodic performance reports', 'google-site-kit' ),
- 'subscription-confirmation' => __( 'Success! You’re subscribed to Site Kit reports', 'google-site-kit' ),
- 'error-email' => __( 'Action needed: your Site Kit report couldn’t be generated', 'google-site-kit' ),
+ 'invitation-email' => __( '%1$s%2$s%3$s invited you to receive periodic performance reports', 'google-site-kit' ),
+ 'subscription-confirmation' => __( 'Success! You’re subscribed to Site Kit reports', 'google-site-kit' ),
+ 'error-email' => __( 'Action needed: your Site Kit report couldn’t be generated', 'google-site-kit' ),
+ 'error-email-cron-scheduler' => __( 'Email reports are failing to send', 'google-site-kit' ),
);
}
@@ -135,6 +136,9 @@ protected static function get_all_bodies() {
'error-email' => array(
__( 'We were unable to generate your report due to a server error. To fix this, contact your host. Report delivery will automatically resume once the issue is resolved.', 'google-site-kit' ),
),
+ 'error-email-cron-scheduler' => array(
+ __( 'We were unable to deliver your report, likely due to a WP-Cron configuration error in your WordPress site’s system settings. To fix this, contact your administrator or get help. Report delivery will automatically resume once the issue is resolved.', 'google-site-kit' ),
+ ),
// Opening/closing tag placeholders keep inline styles and HTML
// out of translation strings. Inline color styles are required
// because many email clients strip or ignore CSS classes.
diff --git a/includes/Core/Email_Reporting/Cron_Health_Check.php b/includes/Core/Email_Reporting/Cron_Health_Check.php
new file mode 100644
index 00000000000..3bf6c1da85a
--- /dev/null
+++ b/includes/Core/Email_Reporting/Cron_Health_Check.php
@@ -0,0 +1,275 @@
+batch_query = $batch_query;
+ $this->scheduler = $scheduler;
+ }
+
+ /**
+ * Checks for stale cron-related work and marks logs with cron errors when found.
+ *
+ * @since n.e.x.t
+ */
+ public function check_stale_tasks() {
+ $now = time();
+ $has_overdue_cron_tasks = false;
+ $frequencies = array(
+ User_Email_Reporting_Settings::FREQUENCY_WEEKLY,
+ User_Email_Reporting_Settings::FREQUENCY_MONTHLY,
+ User_Email_Reporting_Settings::FREQUENCY_QUARTERLY,
+ );
+
+ foreach ( $frequencies as $frequency ) {
+ $scheduled = $this->scheduler->get_initiator_timestamp( $frequency );
+
+ if ( false === $scheduled ) {
+ continue;
+ }
+
+ if ( ( (int) $scheduled + DAY_IN_SECONDS ) < $now ) {
+ $has_overdue_cron_tasks = true;
+ break;
+ }
+ }
+
+ if ( $has_overdue_cron_tasks ) {
+ $this->set_cron_scheduler_error();
+ }
+
+ if ( $this->batch_query->has_stale_pending_logs() ) {
+ $this->mark_stale_logs_cron_error();
+ }
+ }
+
+ /**
+ * Tracks worker progress by frequency and marks cron errors when retries stall.
+ *
+ * @since n.e.x.t
+ *
+ * @param string $frequency Frequency slug.
+ * @param int $emails_processed Number of emails sent in the worker run.
+ * @param string $batch_id Batch ID for the current run.
+ */
+ public function track_worker_progress( string $frequency, int $emails_processed, string $batch_id ) {
+ if ( '' === $frequency || '' === $batch_id ) {
+ return;
+ }
+
+ $transient_key = $this->get_zero_send_transient_key( $frequency );
+
+ if ( $emails_processed > 0 ) {
+ delete_transient( $transient_key );
+ return;
+ }
+
+ $count = ( (int) get_transient( $transient_key ) ) + 1;
+
+ set_transient( $transient_key, $count, HOUR_IN_SECONDS );
+
+ if ( $count >= self::ZERO_SEND_THRESHOLD ) {
+ $this->mark_batch_cron_error( $batch_id );
+ }
+ }
+
+ /**
+ * Marks all pending logs in the given batch as failed due to cron scheduler issues.
+ *
+ * @since n.e.x.t
+ *
+ * @param string $batch_id Batch identifier.
+ */
+ public function mark_batch_cron_error( $batch_id ) {
+ $batch_id = (string) $batch_id;
+ if ( '' === $batch_id ) {
+ return;
+ }
+
+ $pending_ids = $this->batch_query->get_pending_ids( $batch_id );
+
+ if ( empty( $pending_ids ) ) {
+ return;
+ }
+
+ $error_details = $this->get_cron_scheduler_error_json();
+
+ foreach ( $pending_ids as $post_id ) {
+ $this->mark_post_with_cron_error( $post_id, $error_details );
+ }
+ }
+
+ /**
+ * Marks stale scheduled logs as failed with cron scheduler errors.
+ *
+ * @since n.e.x.t
+ */
+ public function mark_stale_logs_cron_error() {
+ $stale_ids = $this->get_stale_pending_log_ids();
+ if ( empty( $stale_ids ) ) {
+ return;
+ }
+
+ $error_details = $this->get_cron_scheduler_error_json();
+
+ foreach ( $stale_ids as $post_id ) {
+ $this->mark_post_with_cron_error( $post_id, $error_details );
+ }
+ }
+
+ /**
+ * Marks latest batch pending logs with cron scheduler errors.
+ *
+ * @since n.e.x.t
+ */
+ private function set_cron_scheduler_error() {
+ $latest_batch_post_ids = $this->batch_query->get_latest_batch_post_ids();
+ if ( empty( $latest_batch_post_ids ) ) {
+ return;
+ }
+
+ $first_post_id = (int) reset( $latest_batch_post_ids );
+ $batch_id = get_post_meta( $first_post_id, Email_Log::META_BATCH_ID, true );
+
+ if ( empty( $batch_id ) ) {
+ return;
+ }
+
+ $this->mark_batch_cron_error( (string) $batch_id );
+ }
+
+ /**
+ * Builds the transient key for zero-send tracking.
+ *
+ * @since n.e.x.t
+ *
+ * @param string $frequency Frequency slug.
+ * @return string
+ */
+ private function get_zero_send_transient_key( $frequency ) {
+ return sprintf( 'googlesitekit_email_cron_zero_sends_%s', sanitize_key( $frequency ) );
+ }
+
+ /**
+ * Gets stale pending log IDs older than one day.
+ *
+ * @since n.e.x.t
+ *
+ * @return array
+ */
+ private function get_stale_pending_log_ids() {
+ $query = new WP_Query(
+ array(
+ 'post_type' => Email_Log::POST_TYPE,
+ 'post_status' => Email_Log::STATUS_SCHEDULED,
+ // phpcs:ignore WordPress.WP.PostsPerPage.posts_per_page_posts_per_page
+ 'posts_per_page' => 10000,
+ 'fields' => 'ids',
+ 'no_found_rows' => true,
+ 'update_post_meta_cache' => false,
+ 'update_post_term_cache' => false,
+ 'date_query' => array(
+ array(
+ 'column' => 'post_date',
+ 'before' => gmdate( 'Y-m-d H:i:s', time() - DAY_IN_SECONDS ),
+ ),
+ ),
+ )
+ );
+
+ return array_map( 'intval', $query->posts );
+ }
+
+ /**
+ * Gets error details payload for cron scheduler errors.
+ *
+ * @since n.e.x.t
+ *
+ * @return string
+ */
+ private function get_cron_scheduler_error_json() {
+ $error = new \WP_Error(
+ 'cron_scheduler_error',
+ __( 'Email report generation could not be completed due to a cron scheduling issue.', 'google-site-kit' ),
+ array(
+ 'category_id' => 'cron_scheduler_error',
+ )
+ );
+
+ $payload = array(
+ 'errors' => $error->errors,
+ 'error_data' => $error->error_data,
+ );
+
+ $encoded = wp_json_encode( $payload, JSON_UNESCAPED_UNICODE );
+
+ return is_string( $encoded ) ? $encoded : '';
+ }
+
+ /**
+ * Marks a single log post as failed due to cron scheduler error.
+ *
+ * @since n.e.x.t
+ *
+ * @param int $post_id Email log post ID.
+ * @param string $error_details Encoded cron scheduler error payload.
+ */
+ private function mark_post_with_cron_error( $post_id, $error_details ) {
+ $this->batch_query->update_status( $post_id, Email_Log::STATUS_FAILED );
+ update_post_meta( $post_id, Email_Log::META_SEND_ATTEMPTS, Email_Log_Batch_Query::MAX_ATTEMPTS );
+ update_post_meta( $post_id, Email_Log::META_ERROR_DETAILS, $error_details );
+ }
+}
diff --git a/includes/Core/Email_Reporting/Email_Log_Batch_Query.php b/includes/Core/Email_Reporting/Email_Log_Batch_Query.php
index dfd324a6b41..555a0970718 100644
--- a/includes/Core/Email_Reporting/Email_Log_Batch_Query.php
+++ b/includes/Core/Email_Reporting/Email_Log_Batch_Query.php
@@ -320,6 +320,35 @@ public function get_latest_batch_error() {
return get_post_meta( $first_post_id, Email_Log::META_ERROR_DETAILS, true );
}
+ /**
+ * Checks if any scheduled email logs are stale (older than one day).
+ *
+ * @since n.e.x.t
+ *
+ * @return bool True when at least one stale scheduled log exists.
+ */
+ public function has_stale_pending_logs() {
+ $query = new WP_Query(
+ array(
+ 'post_type' => Email_Log::POST_TYPE,
+ 'post_status' => Email_Log::STATUS_SCHEDULED,
+ 'posts_per_page' => 1,
+ 'fields' => 'ids',
+ 'no_found_rows' => true,
+ 'update_post_meta_cache' => false,
+ 'update_post_term_cache' => false,
+ 'date_query' => array(
+ array(
+ 'column' => 'post_date',
+ 'before' => gmdate( 'Y-m-d H:i:s', time() - DAY_IN_SECONDS ),
+ ),
+ ),
+ )
+ );
+
+ return ! empty( $query->posts );
+ }
+
/**
* Checks if all logs in a batch have failed after maximum attempts.
*
diff --git a/includes/Core/Email_Reporting/Email_Reporting.php b/includes/Core/Email_Reporting/Email_Reporting.php
index 3fa515b5708..042f5640c02 100644
--- a/includes/Core/Email_Reporting/Email_Reporting.php
+++ b/includes/Core/Email_Reporting/Email_Reporting.php
@@ -232,6 +232,8 @@ public function __construct(
$this->user_settings = new User_Email_Reporting_Settings( $this->user_options );
$conversion_tracking_settings = new Conversion_Tracking_Settings( $this->options );
$conversion_tracking = new Conversion_Tracking( $this->context, $this->options );
+ $frequency_planner = new Frequency_Planner();
+ $this->scheduler = new Email_Reporting_Scheduler( $frequency_planner );
$this->email_notices = new Email_Notices(
$this->context,
$this->golinks,
@@ -241,11 +243,11 @@ public function __construct(
)
);
- $frequency_planner = new Frequency_Planner();
$this->subscribed_users_query = new Subscribed_Users_Query( $this->user_settings, $this->modules );
$eligible_subscribers_query = new Eligible_Subscribers_Query( $this->modules, $this->user_options );
$max_execution_limiter = new Max_Execution_Limiter( (int) ini_get( 'max_execution_time' ) );
$this->email_log_batch_query = new Email_Log_Batch_Query();
+ $health_check = new Cron_Health_Check( $this->email_log_batch_query, $this->scheduler );
$email_sender = new Email();
$section_builder = new Email_Report_Section_Builder( $this->context );
$template_formatter = new Email_Template_Formatter( $this->context, $section_builder, $this->golinks, $this->email_notices );
@@ -259,10 +261,10 @@ public function __construct(
$this->user_settings,
$eligible_subscribers_query,
$email_sender,
- $this->golinks
+ $this->golinks,
+ $health_check
);
$this->email_log = new Email_Log();
- $this->scheduler = new Email_Reporting_Scheduler( $frequency_planner );
$this->initiator_task = new Initiator_Task( $this->scheduler, $this->subscribed_users_query );
$notifier = new Batch_Error_Notifier( $this->email_log_batch_query, $email_sender, $this->context, $this->golinks );
$this->worker_task = new Worker_Task(
@@ -271,7 +273,8 @@ public function __construct(
$this->scheduler,
$log_processor,
$this->data_requests,
- $notifier
+ $notifier,
+ $health_check
);
$this->fallback_task = new Fallback_Task( $this->email_log_batch_query, $this->scheduler, $this->worker_task, $notifier );
$this->monitor_task = new Monitor_Task( $this->scheduler, $this->settings );
diff --git a/includes/Core/Email_Reporting/REST_Email_Reporting_Controller.php b/includes/Core/Email_Reporting/REST_Email_Reporting_Controller.php
index 845dba29022..17c9b77f30c 100644
--- a/includes/Core/Email_Reporting/REST_Email_Reporting_Controller.php
+++ b/includes/Core/Email_Reporting/REST_Email_Reporting_Controller.php
@@ -88,6 +88,14 @@ class REST_Email_Reporting_Controller {
*/
private $email_log_batch_query;
+ /**
+ * Cron health check instance.
+ *
+ * @since n.e.x.t
+ * @var Cron_Health_Check
+ */
+ private $health_check;
+
/**
* Email sender instance.
*
@@ -111,6 +119,7 @@ class REST_Email_Reporting_Controller {
* @since 1.170.0 Added modules and user email reporting settings dependencies.
* @since 1.173.0 Added eligible subscribers query and email sender dependencies and removed unused user options dependency.
* @since 1.174.0 Added golinks dependency.
+ * @since n.e.x.t Added cron health check dependency.
*
* @param Email_Reporting_Settings $settings Email_Reporting_Settings instance.
* @param Modules $modules Modules instance.
@@ -118,6 +127,7 @@ class REST_Email_Reporting_Controller {
* @param Eligible_Subscribers_Query $eligible_subscribers_query Eligible subscribers query instance.
* @param Email $email_sender Email sender instance.
* @param Golinks $golinks Golinks instance.
+ * @param Cron_Health_Check $health_check Cron health check instance.
*/
public function __construct(
Email_Reporting_Settings $settings,
@@ -125,7 +135,8 @@ public function __construct(
User_Email_Reporting_Settings $user_email_reporting_settings,
Eligible_Subscribers_Query $eligible_subscribers_query,
Email $email_sender,
- Golinks $golinks
+ Golinks $golinks,
+ Cron_Health_Check $health_check
) {
$this->settings = $settings;
$this->modules = $modules;
@@ -134,6 +145,7 @@ public function __construct(
$this->email_log_batch_query = new Email_Log_Batch_Query();
$this->email_sender = $email_sender;
$this->golinks = $golinks;
+ $this->health_check = $health_check;
}
/**
@@ -157,6 +169,7 @@ function ( $paths ) {
array(
'/' . REST_Routes::REST_ROOT . '/core/site/data/email-reporting',
'/' . REST_Routes::REST_ROOT . '/core/site/data/email-reporting-eligible-subscribers',
+ '/' . REST_Routes::REST_ROOT . '/core/site/data/email-reporting-errors',
)
);
}
@@ -285,6 +298,7 @@ function ( WP_User $user ) use ( $meta_key ) {
array(
'methods' => WP_REST_Server::READABLE,
'callback' => function () {
+ $this->health_check->check_stale_tasks();
$errors = $this->email_log_batch_query->get_latest_batch_error();
return new WP_REST_Response( is_string( $errors ) ? json_decode( $errors, true ) : array() );
diff --git a/includes/Core/Email_Reporting/Worker_Task.php b/includes/Core/Email_Reporting/Worker_Task.php
index 098599988c7..ed22599ce88 100644
--- a/includes/Core/Email_Reporting/Worker_Task.php
+++ b/includes/Core/Email_Reporting/Worker_Task.php
@@ -76,6 +76,15 @@ class Worker_Task {
*/
private $notifier;
+ /**
+ * Cron health check service.
+ *
+ * @since n.e.x.t
+ *
+ * @var Cron_Health_Check
+ */
+ private $health_check;
+
/**
* Constructor.
*
@@ -88,6 +97,7 @@ class Worker_Task {
* @param Email_Log_Processor $log_processor Log processor instance.
* @param Email_Reporting_Data_Requests $data_requests Data requests helper.
* @param Batch_Error_Notifier $notifier Batch error notifier.
+ * @param Cron_Health_Check $health_check Cron health check service.
*/
public function __construct(
Max_Execution_Limiter $max_execution_limiter,
@@ -95,7 +105,8 @@ public function __construct(
Email_Reporting_Scheduler $scheduler,
Email_Log_Processor $log_processor,
Email_Reporting_Data_Requests $data_requests,
- Batch_Error_Notifier $notifier
+ Batch_Error_Notifier $notifier,
+ Cron_Health_Check $health_check
) {
$this->max_execution_limiter = $max_execution_limiter;
$this->batch_query = $batch_query;
@@ -103,6 +114,7 @@ public function __construct(
$this->log_processor = $log_processor;
$this->data_requests = $data_requests;
$this->notifier = $notifier;
+ $this->health_check = $health_check;
}
/**
@@ -150,7 +162,8 @@ public function handle_callback_action( $batch_id, $frequency, $initiator_timest
return;
}
- $this->process_pending_logs( $pending_ids, $frequency, $initiator_timestamp );
+ $emails_processed = $this->process_pending_logs( $pending_ids, $frequency, $initiator_timestamp );
+ $this->health_check->track_worker_progress( $frequency, $emails_processed, $batch_id );
$this->notifier->maybe_notify( $batch_id );
} finally {
@@ -169,15 +182,19 @@ public function handle_callback_action( $batch_id, $frequency, $initiator_timest
* @param array $pending_ids Pending post IDs.
* @param string $frequency Frequency slug.
* @param int $initiator_timestamp Initiator timestamp.
+ * @return int Number of emails processed and marked sent.
*/
private function process_pending_logs( array $pending_ids, $frequency, $initiator_timestamp ) {
- $shared_payloads = $this->get_shared_payloads_for_pending_ids( $pending_ids );
+ $shared_payloads = $this->get_shared_payloads_for_pending_ids( $pending_ids );
+ $processed_sent_ids = array();
foreach ( $pending_ids as $post_id ) {
if ( $this->should_abort( $initiator_timestamp ) ) {
- return;
+ return count( $processed_sent_ids );
}
+ $previous_status = get_post_status( $post_id );
+
$email_log = get_post( $post_id );
$user = null;
@@ -203,7 +220,13 @@ private function process_pending_logs( array $pending_ids, $frequency, $initiato
} else {
$this->log_processor->process( $post_id, $frequency, $shared_payloads_for_user );
}
+
+ if ( Email_Log::STATUS_SENT === get_post_status( $post_id ) && Email_Log::STATUS_SENT !== $previous_status ) {
+ $processed_sent_ids[] = $post_id;
+ }
}
+
+ return count( $processed_sent_ids );
}
/**
diff --git a/tests/phpunit/integration/Core/Email_Reporting/Cron_Health_CheckTest.php b/tests/phpunit/integration/Core/Email_Reporting/Cron_Health_CheckTest.php
new file mode 100644
index 00000000000..462f69fbe4f
--- /dev/null
+++ b/tests/phpunit/integration/Core/Email_Reporting/Cron_Health_CheckTest.php
@@ -0,0 +1,232 @@
+register_email_log_dependencies();
+
+ $this->query = new Email_Log_Batch_Query();
+ $scheduler = new Email_Reporting_Scheduler( new Frequency_Planner() );
+ $this->health_check = new Cron_Health_Check( $this->query, $scheduler );
+ $this->created_post_ids = array();
+
+ $this->clear_state();
+ }
+
+ public function tear_down() {
+ $this->clear_state();
+
+ foreach ( $this->created_post_ids as $post_id ) {
+ wp_delete_post( $post_id, true );
+ }
+
+ if ( post_type_exists( Email_Log::POST_TYPE ) && function_exists( 'unregister_post_type' ) ) {
+ unregister_post_type( Email_Log::POST_TYPE );
+ }
+
+ foreach ( array( Email_Log::STATUS_SENT, Email_Log::STATUS_FAILED, Email_Log::STATUS_SCHEDULED ) as $status ) {
+ if ( isset( $GLOBALS['wp_post_statuses'][ $status ] ) ) {
+ unset( $GLOBALS['wp_post_statuses'][ $status ] );
+ }
+ }
+
+ parent::tear_down();
+ }
+
+ public function test_check_stale_tasks_marks_latest_pending_batch_when_initiator_is_overdue() {
+ $batch_id = 'batch-overdue';
+ $post_id = $this->create_log_post( $batch_id, Email_Log::STATUS_SCHEDULED, 0 );
+
+ wp_schedule_single_event(
+ time() - DAY_IN_SECONDS - HOUR_IN_SECONDS,
+ Email_Reporting_Scheduler::ACTION_INITIATOR,
+ array( Email_Reporting_Settings::FREQUENCY_WEEKLY, time() - DAY_IN_SECONDS - HOUR_IN_SECONDS )
+ );
+ wp_schedule_single_event(
+ time() + DAY_IN_SECONDS,
+ Email_Reporting_Scheduler::ACTION_INITIATOR,
+ array( Email_Reporting_Settings::FREQUENCY_MONTHLY, time() + DAY_IN_SECONDS )
+ );
+ wp_schedule_single_event(
+ time() + DAY_IN_SECONDS,
+ Email_Reporting_Scheduler::ACTION_INITIATOR,
+ array( Email_Reporting_Settings::FREQUENCY_QUARTERLY, time() + DAY_IN_SECONDS )
+ );
+
+ $this->health_check->check_stale_tasks();
+
+ $this->assertSame( Email_Log::STATUS_FAILED, get_post_status( $post_id ), 'Overdue initiator should mark latest pending logs as failed.' );
+ $this->assertSame( Email_Log_Batch_Query::MAX_ATTEMPTS, (int) get_post_meta( $post_id, Email_Log::META_SEND_ATTEMPTS, true ), 'Overdue initiator should set max attempts on failed logs.' );
+ $this->assertStringContainsString( 'cron_scheduler_error', (string) get_post_meta( $post_id, Email_Log::META_ERROR_DETAILS, true ), 'Overdue initiator should store cron scheduler error details.' );
+ }
+
+ public function test_check_stale_tasks_does_not_mark_logs_when_all_frequencies_are_healthy() {
+ $batch_id = 'batch-healthy';
+ $post_id = $this->create_log_post( $batch_id, Email_Log::STATUS_SCHEDULED, 0 );
+
+ wp_schedule_single_event(
+ time() + DAY_IN_SECONDS,
+ Email_Reporting_Scheduler::ACTION_INITIATOR,
+ array( Email_Reporting_Settings::FREQUENCY_WEEKLY, time() + DAY_IN_SECONDS )
+ );
+ wp_schedule_single_event(
+ time() + DAY_IN_SECONDS,
+ Email_Reporting_Scheduler::ACTION_INITIATOR,
+ array( Email_Reporting_Settings::FREQUENCY_MONTHLY, time() + DAY_IN_SECONDS )
+ );
+ wp_schedule_single_event(
+ time() + DAY_IN_SECONDS,
+ Email_Reporting_Scheduler::ACTION_INITIATOR,
+ array( Email_Reporting_Settings::FREQUENCY_QUARTERLY, time() + DAY_IN_SECONDS )
+ );
+
+ $this->health_check->check_stale_tasks();
+
+ $this->assertSame( Email_Log::STATUS_SCHEDULED, get_post_status( $post_id ), 'Healthy schedules should not mark logs as failed.' );
+ }
+
+ public function test_check_stale_tasks_marks_stale_pending_logs() {
+ $batch_id = 'batch-stale';
+ $post_id = $this->create_log_post(
+ $batch_id,
+ Email_Log::STATUS_SCHEDULED,
+ 0,
+ array(
+ 'post_date' => gmdate( 'Y-m-d H:i:s', time() - ( 2 * DAY_IN_SECONDS ) ),
+ 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', time() - ( 2 * DAY_IN_SECONDS ) ),
+ )
+ );
+
+ $this->health_check->check_stale_tasks();
+
+ $this->assertSame( Email_Log::STATUS_FAILED, get_post_status( $post_id ), 'Stale scheduled logs should be marked as failed.' );
+ $this->assertSame( Email_Log_Batch_Query::MAX_ATTEMPTS, (int) get_post_meta( $post_id, Email_Log::META_SEND_ATTEMPTS, true ), 'Stale scheduled logs should set attempts to max.' );
+ $this->assertStringContainsString( 'cron_scheduler_error', (string) get_post_meta( $post_id, Email_Log::META_ERROR_DETAILS, true ), 'Stale scheduled logs should store cron scheduler error details.' );
+ }
+
+ public function test_track_worker_progress_marks_batch_after_three_consecutive_zero_sends() {
+ $batch_id = 'batch-zero-send';
+ $post_id = $this->create_log_post( $batch_id, Email_Log::STATUS_SCHEDULED, 0 );
+
+ $this->health_check->track_worker_progress( Email_Reporting_Settings::FREQUENCY_WEEKLY, 0, $batch_id );
+ $this->health_check->track_worker_progress( Email_Reporting_Settings::FREQUENCY_WEEKLY, 0, $batch_id );
+ $this->health_check->track_worker_progress( Email_Reporting_Settings::FREQUENCY_WEEKLY, 0, $batch_id );
+
+ $this->assertSame( Email_Log::STATUS_FAILED, get_post_status( $post_id ), 'Three zero-send runs should mark the batch as failed.' );
+ $this->assertSame( Email_Log_Batch_Query::MAX_ATTEMPTS, (int) get_post_meta( $post_id, Email_Log::META_SEND_ATTEMPTS, true ), 'Three zero-send runs should set attempts to max.' );
+ }
+
+ public function test_track_worker_progress_keeps_frequency_counters_isolated() {
+ $weekly_key = 'googlesitekit_email_cron_zero_sends_' . Email_Reporting_Settings::FREQUENCY_WEEKLY;
+ $monthly_key = 'googlesitekit_email_cron_zero_sends_' . Email_Reporting_Settings::FREQUENCY_MONTHLY;
+
+ $this->health_check->track_worker_progress( Email_Reporting_Settings::FREQUENCY_WEEKLY, 0, 'batch-weekly' );
+ $this->health_check->track_worker_progress( Email_Reporting_Settings::FREQUENCY_WEEKLY, 0, 'batch-weekly' );
+ $this->health_check->track_worker_progress( Email_Reporting_Settings::FREQUENCY_MONTHLY, 0, 'batch-monthly' );
+
+ $this->assertSame( 2, (int) get_transient( $weekly_key ), 'Weekly counter should track only weekly runs.' );
+ $this->assertSame( 1, (int) get_transient( $monthly_key ), 'Monthly counter should track only monthly runs.' );
+ }
+
+ public function test_track_worker_progress_resets_counter_on_successful_send() {
+ $batch_id = 'batch-reset';
+ $post_id = $this->create_log_post( $batch_id, Email_Log::STATUS_SCHEDULED, 0 );
+
+ $this->health_check->track_worker_progress( Email_Reporting_Settings::FREQUENCY_WEEKLY, 0, $batch_id );
+ $this->health_check->track_worker_progress( Email_Reporting_Settings::FREQUENCY_WEEKLY, 0, $batch_id );
+ $this->health_check->track_worker_progress( Email_Reporting_Settings::FREQUENCY_WEEKLY, 1, $batch_id );
+ $this->health_check->track_worker_progress( Email_Reporting_Settings::FREQUENCY_WEEKLY, 0, $batch_id );
+
+ $this->assertSame( Email_Log::STATUS_SCHEDULED, get_post_status( $post_id ), 'Counter reset should prevent premature batch failure.' );
+ $this->assertSame(
+ 1,
+ (int) get_transient( 'googlesitekit_email_cron_zero_sends_' . Email_Reporting_Settings::FREQUENCY_WEEKLY ),
+ 'Counter should restart from 1 after a successful send.'
+ );
+ }
+
+ public function test_mark_batch_cron_error_updates_logs_and_latest_batch_error_pipeline() {
+ $batch_id = 'batch-mark';
+ $post_id = $this->create_log_post( $batch_id, Email_Log::STATUS_SCHEDULED, 0 );
+
+ $this->health_check->mark_batch_cron_error( $batch_id );
+
+ $this->assertSame( Email_Log::STATUS_FAILED, get_post_status( $post_id ), 'mark_batch_cron_error should set failed status.' );
+ $this->assertSame( Email_Log_Batch_Query::MAX_ATTEMPTS, (int) get_post_meta( $post_id, Email_Log::META_SEND_ATTEMPTS, true ), 'mark_batch_cron_error should set max attempts.' );
+
+ $latest_error = $this->query->get_latest_batch_error();
+ $this->assertIsString( $latest_error, 'Latest batch error should be persisted as JSON string.' );
+ $this->assertStringContainsString( 'cron_scheduler_error', $latest_error, 'Latest batch error should include cron scheduler category.' );
+ }
+
+ private function create_log_post( $batch_id, $status, $attempts, array $overrides = array() ) {
+ $post_id = $this->factory()->post->create(
+ array_merge(
+ array(
+ 'post_type' => Email_Log::POST_TYPE,
+ 'post_status' => $status,
+ 'post_title' => 'Cron log ' . uniqid(),
+ 'post_date' => current_time( 'mysql' ),
+ 'post_date_gmt' => current_time( 'mysql', 1 ),
+ ),
+ $overrides
+ )
+ );
+
+ update_post_meta( $post_id, Email_Log::META_BATCH_ID, $batch_id );
+ update_post_meta( $post_id, Email_Log::META_REPORT_FREQUENCY, Email_Reporting_Settings::FREQUENCY_WEEKLY );
+ update_post_meta( $post_id, Email_Log::META_SEND_ATTEMPTS, $attempts );
+ update_post_meta(
+ $post_id,
+ Email_Log::META_REPORT_REFERENCE_DATES,
+ array(
+ 'startDate' => time() - DAY_IN_SECONDS,
+ 'sendDate' => time(),
+ )
+ );
+
+ $this->created_post_ids[] = $post_id;
+
+ return $post_id;
+ }
+
+ private function register_email_log_dependencies() {
+ $email_log = new Email_Log( new Context( GOOGLESITEKIT_PLUGIN_MAIN_FILE ) );
+ $register_method = new \ReflectionMethod( Email_Log::class, 'register_email_log' );
+ $register_method->setAccessible( true );
+ $register_method->invoke( $email_log );
+ }
+
+ private function clear_state() {
+ wp_unschedule_hook( Email_Reporting_Scheduler::ACTION_INITIATOR );
+
+ delete_transient( 'googlesitekit_email_cron_zero_sends_' . Email_Reporting_Settings::FREQUENCY_WEEKLY );
+ delete_transient( 'googlesitekit_email_cron_zero_sends_' . Email_Reporting_Settings::FREQUENCY_MONTHLY );
+ delete_transient( 'googlesitekit_email_cron_zero_sends_' . Email_Reporting_Settings::FREQUENCY_QUARTERLY );
+ }
+}
diff --git a/tests/phpunit/integration/Core/Email_Reporting/Email_Log_Batch_QueryTest.php b/tests/phpunit/integration/Core/Email_Reporting/Email_Log_Batch_QueryTest.php
index 52b5d9763f1..6a5f0348333 100644
--- a/tests/phpunit/integration/Core/Email_Reporting/Email_Log_Batch_QueryTest.php
+++ b/tests/phpunit/integration/Core/Email_Reporting/Email_Log_Batch_QueryTest.php
@@ -137,6 +137,26 @@ public function test_get_latest_batch_error__number_of_attempts_not_exceeded() {
$this->assertNull( $latest_error, 'Latest batch error should return null if not all logs have exceeded max attempts.' );
}
+ public function test_has_stale_pending_logs_returns_true_when_scheduled_log_is_older_than_a_day() {
+ $post_id = $this->create_log_post( 'batch-stale', Email_Log::STATUS_SCHEDULED, 0 );
+
+ wp_update_post(
+ array(
+ 'ID' => $post_id,
+ 'post_date' => gmdate( 'Y-m-d H:i:s', time() - ( 2 * DAY_IN_SECONDS ) ),
+ 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', time() - ( 2 * DAY_IN_SECONDS ) ),
+ )
+ );
+
+ $this->assertTrue( $this->query->has_stale_pending_logs(), 'A scheduled log older than one day should be considered stale.' );
+ }
+
+ public function test_has_stale_pending_logs_returns_false_when_only_recent_scheduled_logs_exist() {
+ $this->create_log_post( 'batch-recent', Email_Log::STATUS_SCHEDULED, 0 );
+
+ $this->assertFalse( $this->query->has_stale_pending_logs(), 'Recent scheduled logs should not be considered stale.' );
+ }
+
public function test_get_total_count_by_status__returns_zero_when_no_logs() {
$this->assertSame( 0, $this->query->get_total_count_by_status( Email_Log::STATUS_SENT ), 'Expected zero sent logs when none exist.' );
}
diff --git a/tests/phpunit/integration/Core/Email_Reporting/REST_Email_Reporting_ControllerTest.php b/tests/phpunit/integration/Core/Email_Reporting/REST_Email_Reporting_ControllerTest.php
index 919bb18e337..5eecbc92066 100644
--- a/tests/phpunit/integration/Core/Email_Reporting/REST_Email_Reporting_ControllerTest.php
+++ b/tests/phpunit/integration/Core/Email_Reporting/REST_Email_Reporting_ControllerTest.php
@@ -13,6 +13,8 @@
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Authentication\Authentication;
use Google\Site_Kit\Core\Email\Email;
+use Google\Site_Kit\Core\Email_Reporting\Cron_Health_Check;
+use Google\Site_Kit\Core\Email_Reporting\Email_Log_Batch_Query;
use Google\Site_Kit\Core\Email_Reporting\Email_Reporting_Golink_Handler;
use Google\Site_Kit\Core\Email_Reporting\Email_Reporting_Settings;
use Google\Site_Kit\Core\Email_Reporting\Eligible_Subscribers_Query;
@@ -121,6 +123,7 @@ public function set_up() {
$golinks = new Golinks( $this->context );
$golinks->register_handler( 'manage-subscription-email-reporting', new Email_Reporting_Golink_Handler() );
+ $health_check = $this->createMock( Cron_Health_Check::class );
$this->controller = new REST_Email_Reporting_Controller(
$this->settings,
@@ -128,7 +131,8 @@ public function set_up() {
$this->user_settings,
new Eligible_Subscribers_Query( $this->modules, $this->user_options ),
new Email(),
- $golinks
+ $golinks,
+ $health_check
);
$this->original_sharing_option = get_option( Module_Sharing_Settings::OPTION );
}
@@ -169,6 +173,7 @@ public function test_get_routes() {
$routes = array(
'/' . REST_Routes::REST_ROOT . '/core/site/data/email-reporting',
'/' . REST_Routes::REST_ROOT . '/core/site/data/email-reporting-eligible-subscribers',
+ '/' . REST_Routes::REST_ROOT . '/core/site/data/email-reporting-errors',
'/' . REST_Routes::REST_ROOT . '/core/site/data/email-reporting-invite-user',
);
$get_routes = array_intersect( $routes, array_keys( $server->get_routes() ) );
@@ -374,6 +379,58 @@ public function test_get_eligible_subscribers_non_matching_search_returns_empty_
$this->assertSame( 0, $data['totalPages'], 'Non-matching search should return zero total pages.' );
}
+ public function test_get_email_reporting_errors_runs_health_check_before_reading_latest_batch_error() {
+ remove_all_filters( 'googlesitekit_rest_routes' );
+
+ $call_order = array();
+
+ $health_check = $this->createMock( Cron_Health_Check::class );
+ $health_check->expects( $this->once() )
+ ->method( 'check_stale_tasks' )
+ ->willReturnCallback(
+ function () use ( &$call_order ) {
+ $call_order[] = 'health_check';
+ }
+ );
+
+ $controller = new REST_Email_Reporting_Controller(
+ $this->settings,
+ $this->modules,
+ $this->user_settings,
+ new Eligible_Subscribers_Query( $this->modules, $this->user_options ),
+ new Email(),
+ new Golinks( $this->context ),
+ $health_check
+ );
+
+ $batch_query = $this->createMock( Email_Log_Batch_Query::class );
+ $batch_query->expects( $this->once() )
+ ->method( 'get_latest_batch_error' )
+ ->willReturnCallback(
+ function () use ( &$call_order ) {
+ $call_order[] = 'latest_batch_error';
+ return '{"errors":{"cron_scheduler_error":["Cron issue"]},"error_data":{"cron_scheduler_error":{"category_id":"cron_scheduler_error"}}}';
+ }
+ );
+
+ $reflection = new \ReflectionProperty( REST_Email_Reporting_Controller::class, 'email_log_batch_query' );
+ $reflection->setAccessible( true );
+ $reflection->setValue( $controller, $batch_query );
+
+ $controller->register();
+ $this->register_rest_routes();
+
+ $request = new \WP_REST_Request( 'GET', '/' . REST_Routes::REST_ROOT . '/core/site/data/email-reporting-errors' );
+ $response = rest_get_server()->dispatch( $request );
+
+ $this->assertEquals( 200, $response->get_status(), 'Email reporting errors endpoint should return 200.' );
+ $this->assertSame(
+ array( 'health_check', 'latest_batch_error' ),
+ $call_order,
+ 'Cron health check should run before reading latest batch error.'
+ );
+ }
+
public function test_get_eligible_subscribers_includes_invited_field() {
$current_admin = $this->create_admin_with_token( 'admin-current' );
$other_admin = $this->create_admin_with_token( 'admin-other' );
diff --git a/tests/phpunit/integration/Core/Email_Reporting/Worker_TaskTest.php b/tests/phpunit/integration/Core/Email_Reporting/Worker_TaskTest.php
index d8226561436..b4813837e32 100644
--- a/tests/phpunit/integration/Core/Email_Reporting/Worker_TaskTest.php
+++ b/tests/phpunit/integration/Core/Email_Reporting/Worker_TaskTest.php
@@ -10,6 +10,7 @@
use Google\Site_Kit\Context;
use Google\Site_Kit\Core\Email\Email;
use Google\Site_Kit\Core\Email_Reporting\Batch_Error_Notifier;
+use Google\Site_Kit\Core\Email_Reporting\Cron_Health_Check;
use Google\Site_Kit\Core\Email_Reporting\Email_Log;
use Google\Site_Kit\Core\Email_Reporting\Email_Log_Batch_Query;
use Google\Site_Kit\Core\Email_Reporting\Email_Log_Processor;
@@ -94,6 +95,11 @@ class Worker_TaskTest extends TestCase {
*/
private $real_batch_query;
+ /**
+ * @var Cron_Health_Check|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $health_check;
+
public function set_up() {
parent::set_up();
@@ -108,6 +114,7 @@ public function set_up() {
$this->template_renderer_factory = $this->createMock( Email_Template_Renderer_Factory::class );
$this->template_renderer_factory->method( 'create' )->willReturn( $this->template_renderer );
$this->notifier = $this->createMock( Batch_Error_Notifier::class );
+ $this->health_check = $this->createMock( Cron_Health_Check::class );
$this->email_log = new Email_Log( $this->context );
$this->created_post_ids = array();
$this->real_batch_query = new Email_Log_Batch_Query();
@@ -247,6 +254,51 @@ function ( $delay ) use ( $initiator_stamp, &$captured_delay, $expected_delay )
$this->assertNotNull( $captured_delay, 'Follow-up delay should be captured for assertion.' );
}
+ public function test_tracks_worker_progress_after_processing_pending_logs() {
+ $pending_ids = array( 123 );
+ $initiator_stamp = time();
+ $log_processor_mock = $this->createMock( Email_Log_Processor::class );
+
+ $this->limiter->method( 'should_abort' )->willReturn( false );
+
+ $this->batch_query->expects( $this->once() )
+ ->method( 'is_complete' )
+ ->with( 'batch-progress' )
+ ->willReturn( false );
+
+ $this->batch_query->expects( $this->once() )
+ ->method( 'get_pending_ids' )
+ ->with( 'batch-progress' )
+ ->willReturn( $pending_ids );
+
+ $this->scheduler->expects( $this->once() )
+ ->method( 'schedule_worker' );
+
+ $log_processor_mock->expects( $this->once() )
+ ->method( 'process' )
+ ->with( $pending_ids[0], Email_Reporting_Settings::FREQUENCY_WEEKLY );
+
+ $this->health_check->expects( $this->once() )
+ ->method( 'track_worker_progress' )
+ ->with( Email_Reporting_Settings::FREQUENCY_WEEKLY, 0, 'batch-progress' );
+
+ $this->notifier->expects( $this->once() )
+ ->method( 'maybe_notify' )
+ ->with( 'batch-progress' );
+
+ $task = new Worker_Task(
+ $this->limiter,
+ $this->batch_query,
+ $this->scheduler,
+ $log_processor_mock,
+ $this->data_requests,
+ $this->notifier,
+ $this->health_check
+ );
+
+ $task->handle_callback_action( 'batch-progress', Email_Reporting_Settings::FREQUENCY_WEEKLY, $initiator_stamp );
+ }
+
public function test_switches_to_log_site_id_on_multisite() {
if ( ! is_multisite() ) {
$this->markTestSkipped( 'This test only runs on multisite.' );
@@ -297,7 +349,8 @@ function () use ( &$observed_blog_id ) {
$this->scheduler,
$log_processor_mock,
$this->data_requests,
- $this->notifier
+ $this->notifier,
+ $this->health_check
);
$task->handle_callback_action( $batch_id, Email_Reporting_Settings::FREQUENCY_WEEKLY, time() );
@@ -327,7 +380,8 @@ public function test_increments_attempts_for_pending_posts() {
new Email_Report_Sender( $this->template_renderer_factory, $this->email_sender )
),
$this->data_requests,
- $this->notifier
+ $this->notifier,
+ $this->health_check
);
$batch_id = 'batch-real';
@@ -776,7 +830,8 @@ private function create_worker_task( $batch_query = null, $template_renderer_fac
$this->scheduler,
$log_processor,
$this->data_requests,
- $this->notifier
+ $this->notifier,
+ $this->health_check
);
}