$tests * @return array */ public function add_tests( array $tests ): array { $tests['direct']['aftercare_api_key'] = array( 'label' => __( 'Aftercare can collect Core Web Vitals', 'aftercare' ), 'test' => array( $this, 'test_api_key' ), ); $tests['direct']['aftercare_cron'] = array( 'label' => __( 'Aftercare daily checks are running', 'aftercare' ), 'test' => array( $this, 'test_cron' ), ); // Only when lab checks are on: a disabled feature is not a problem. // The test reads the outcome of the last real check rather than making // a Lighthouse call, which takes up to 30 seconds per run. if ( LabClient::enabled() ) { $tests['direct']['aftercare_lab'] = array( 'label' => __( 'Aftercare lab checks reach PageSpeed Insights', 'aftercare' ), 'test' => array( $this, 'test_lab' ), ); } if ( Options::get( 'error_watch' ) ) { $tests['direct']['aftercare_errors'] = array( 'label' => __( 'No unresolved PHP fatal errors', 'aftercare' ), 'test' => array( $this, 'test_errors' ), ); } if ( AvailabilityCheck::enabled() ) { $tests['direct']['aftercare_availability'] = array( 'label' => __( 'Tracked pages load', 'aftercare' ), 'test' => array( $this, 'test_availability' ), ); } $tests['direct']['aftercare_incidents'] = array( 'label' => __( 'Site performance is within budget', 'aftercare' ), 'test' => array( $this, 'test_incidents' ), ); return $tests; } /** * @return array */ public function test_api_key(): array { $configured = '' !== (string) Options::get( 'api_key' ); return $this->result( 'aftercare_api_key', $configured ? 'good' : 'recommended', $configured ? __( 'Aftercare has a Google API key and can pull Core Web Vitals', 'aftercare' ) : __( 'Aftercare has no Google API key configured', 'aftercare' ), $configured ? __( 'Daily p75 vitals are collected from the Chrome UX Report.', 'aftercare' ) : __( 'Without a Chrome UX Report API key, Aftercare cannot collect Core Web Vitals or detect regressions. The key is free.', 'aftercare' ), $configured ? '' : $this->settings_action() ); } /** * @return array */ public function test_cron(): array { $last = (int) get_option( Cron::LAST_RUN_OPT, 0 ); $stale = $last > 0 && ( time() - $last ) > 2 * DAY_IN_SECONDS; $never = 0 === $last; if ( ! $stale && ! $never ) { return $this->result( 'aftercare_cron', 'good', __( 'Aftercare daily checks are running', 'aftercare' ), sprintf( /* translators: %s: human time diff */ __( 'The daily vitals pull and breach detection last ran %s ago.', 'aftercare' ), human_time_diff( $last ) ) ); } return $this->result( 'aftercare_cron', 'recommended', $never ? __( 'Aftercare daily checks have not run yet', 'aftercare' ) : __( 'Aftercare daily checks look stalled', 'aftercare' ), __( 'Vitals collection and breach detection run via WP-Cron (or Action Scheduler when available). If this persists, configure a real server cron job that calls wp-cron.php, or run the checks manually from the Aftercare dashboard.', 'aftercare' ), $this->dashboard_action() ); } /** * @return array */ public function test_lab(): array { $error = LabClient::last_error(); $success = (int) get_option( LabClient::SUCCESS_OPT, 0 ); if ( $error && 403 === $error['status'] ) { return $this->result( 'aftercare_lab', 'recommended', __( 'The PageSpeed Insights API is not enabled for Aftercare\'s Google API key', 'aftercare' ), sprintf( /* translators: %s: error message from Google */ __( 'Lab checks use the same key as the Chrome UX Report, but the PageSpeed Insights API has to be enabled for it separately, and an API restriction on the key must allow it. Google said: %s', 'aftercare' ), $error['message'] ), '' . esc_html__( 'Enable the PageSpeed Insights API', 'aftercare' ) . '' ); } if ( $error ) { return $this->result( 'aftercare_lab', 'recommended', __( 'Aftercare lab checks are failing', 'aftercare' ), sprintf( /* translators: 1: URL, 2: error message */ __( 'The last lab check of %1$s failed: %2$s Google loads the page from its own servers, so it must be publicly reachable (not behind a login, a maintenance page or a firewall).', 'aftercare' ), $error['url'], $error['message'] ), $this->dashboard_action() ); } if ( 0 === $success ) { return $this->result( 'aftercare_lab', 'good', __( 'Aftercare lab checks are set up', 'aftercare' ), __( 'No lab check has completed yet. The first one runs with the next daily checks, or right away with the wp aftercare lab command.', 'aftercare' ) ); } if ( time() - $success > 2 * DAY_IN_SECONDS ) { return $this->result( 'aftercare_lab', 'recommended', __( 'Aftercare lab checks have stopped completing', 'aftercare' ), sprintf( /* translators: %s: human time diff */ __( 'The last lab check completed %s ago. Lab checks run as scheduled tasks, so check that WP-Cron or Action Scheduler is running.', 'aftercare' ), human_time_diff( $success ) ), $this->dashboard_action() ); } return $this->result( 'aftercare_lab', 'good', __( 'Aftercare lab checks reach PageSpeed Insights', 'aftercare' ), sprintf( /* translators: %s: human time diff */ __( 'The last lab check completed %s ago.', 'aftercare' ), human_time_diff( $success ) ) ); } /** * @return array */ public function test_errors(): array { $open = ( new IncidentRepository() )->count_open( array( IncidentRepository::SOURCE_ERROR ) ); if ( 0 === $open ) { return $this->result( 'aftercare_errors', 'good', __( 'No unresolved PHP fatal errors', 'aftercare' ), __( 'No PHP fatal error incident is open. Error incidents resolve once their error has not happened for 24 hours.', 'aftercare' ) ); } return $this->result( 'aftercare_errors', 'critical', sprintf( /* translators: %d: number of errors */ _n( '%d PHP fatal error is breaking requests', '%d PHP fatal errors are breaking requests', $open, 'aftercare' ), $open ), __( 'Requests on this site are dying with a fatal error. The error list shows where each one happens and the plugin or theme it most likely came from, and the incident lists the changes made in the 72 hours before it started.', 'aftercare' ), '' . esc_html__( 'View PHP errors', 'aftercare' ) . '' ); } /** * @return array */ public function test_availability(): array { $loopback = get_option( AvailabilityCheck::LOOPBACK_OPT ); if ( is_array( $loopback ) ) { return $this->result( 'aftercare_availability', 'recommended', __( 'Aftercare cannot request this site\'s own pages', 'aftercare' ), sprintf( /* translators: %s: connection error */ __( 'The availability check could not connect to any tracked page from the server itself, which usually means the host blocks loopback requests. No page is reported as broken because of this, but broken pages also cannot be detected. Last error: %s', 'aftercare' ), (string) ( $loopback['message'] ?? '' ) ) ); } $open = ( new IncidentRepository() )->count_open( array( IncidentRepository::SOURCE_AVAILABILITY ) ); if ( 0 === $open ) { return $this->result( 'aftercare_availability', 'good', __( 'Tracked pages load', 'aftercare' ), __( 'The homepage and tracked URLs answered the last availability check without a server error, a missing page or a cut-off page.', 'aftercare' ) ); } return $this->result( 'aftercare_availability', 'critical', sprintf( /* translators: %d: number of pages */ _n( '%d tracked page is failing to load', '%d tracked pages are failing to load', $open, 'aftercare' ), $open ), __( 'A tracked page failed two availability checks in a row. The incident shows the response and every site change from the 72 hours before.', 'aftercare' ), '' . esc_html__( 'View incidents', 'aftercare' ) . '' ); } /** * @return array */ public function test_incidents(): array { $open = ( new IncidentRepository() )->count_open( IncidentRepository::VITALS_SOURCES ); if ( 0 === $open ) { return $this->result( 'aftercare_incidents', 'good', __( 'No open performance incidents', 'aftercare' ), __( 'All monitored Core Web Vitals are within their performance budgets.', 'aftercare' ) ); } return $this->result( 'aftercare_incidents', 'recommended', sprintf( /* translators: %d: number of open incidents */ _n( '%d performance budget is currently breached', '%d performance budgets are currently breached', $open, 'aftercare' ), $open ), __( 'One or more Core Web Vitals metrics regressed past their budget or baseline. Open the incident to see every site change from the 72 hours before the regression.', 'aftercare' ), '' . esc_html__( 'View incidents', 'aftercare' ) . '' ); } /** * @return array */ private function result( string $test, string $status, string $label, string $description, string $actions = '' ): array { return array( 'label' => $label, 'status' => $status, 'badge' => array( 'label' => __( 'Performance', 'aftercare' ), 'color' => 'blue', ), 'description' => '

' . esc_html( $description ) . '

', 'actions' => $actions, 'test' => $test, ); } private function settings_action(): string { return '' . esc_html__( 'Open Aftercare Settings', 'aftercare' ) . ''; } private function dashboard_action(): string { return '' . esc_html__( 'Open the Aftercare dashboard', 'aftercare' ) . ''; } }