get_environment_id() : 'unknown'; // Check if provider is available in current environment if ( ! $provider->is_available() ) { self::log_event( 'unavailable', $key, 'External storage not available', $environment ); return null; } // Check if provider should handle this option if ( ! $provider->should_handle( $key ) ) { return null; } // Try to get value from the provider try { $value = $provider->get( $key ); // Check if we got a valid value if ( null !== $value && false !== $value && '' !== $value && 0 !== $value ) { return $value; } // Empty value - log it self::log_event( 'empty', $key, '', $environment ); } catch ( \Exception $e ) { // Provider threw an exception self::log_event( 'error', $key, $e->getMessage(), $environment ); } // Provider couldn't provide value, return null for database fallback return null; } /** * Log events if WP_DEBUG is enabled. * Report external storage events through Jetpack Connection Error_Handler. * Includes rate limiting to prevent log spam from noisy events. * * @since 6.18.0 * * @param string $event_type The event type (error, empty, unavailable). * @param string $key The key that triggered the event. * @param string $details Additional details about the event. * @param string $environment The environment identifier (atomic, vip, etc.). */ public static function log_event( $event_type, $key, $details = '', $environment = 'unknown' ) { // Apply rate limiting to prevent log spam if ( ! self::should_log_event( $key ) ) { return; } // Local debug logging (only when WP_DEBUG is enabled) if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log sprintf( 'Jetpack External Storage %s: %s in %s%s', $event_type, $key, $environment, $details ? ' - ' . $details : '' ) ); } // Only report 'error' and 'empty' events to WordPress.com if ( 'error' !== $event_type && 'empty' !== $event_type ) { return; } $should_report_remote = false; if ( 'error' === $event_type ) { // Report external storage errors for supported environments $should_report_remote = self::should_report_for_environment( $key ); } elseif ( 'empty' === $event_type ) { // Use delay mechanism to distinguish disconnection from a delay $should_report_remote = self::should_report_for_environment( $key ) && self::should_report_empty_state( $key ); } if ( ! $should_report_remote || ! class_exists( 'Automattic\Jetpack\Connection\Error_Handler' ) ) { return; } // Create and report error $error_code = 'external_storage_' . $event_type; $error_message = sprintf( 'External storage %s for key "%s"%s', str_replace( '_', ' ', $event_type ), $key, $details ? ': ' . $details : '' ); $error_data = array( 'key' => $key, 'event_type' => $event_type, 'details' => $details, 'environment' => $environment, 'timestamp' => time(), 'site_url' => home_url(), ); $error = new \WP_Error( $error_code, $error_message, $error_data ); Error_Handler::get_instance()->report_error( $error, false, true ); } /** * Determine if the current environment should report external storage errors to WordPress.com. * Allows providers to control remote error reporting per-option via optional should_report_errors_for() method. * * @since 6.18.0 * * @param string $key The option key being accessed. * @return bool True if this environment should report external storage errors to WordPress.com. */ private static function should_report_for_environment( $key = '' ) { $provider = self::$provider; // Check if provider implements per-option reporting (optional method not defined in interface). // Providers can optionally implement: public function should_report_errors_for( $option_name ) if ( null !== $provider && method_exists( $provider, 'should_report_errors_for' ) && ! empty( $key ) ) { // @phan-suppress-next-line PhanUndeclaredMethodInCallable - Optional method, checked via method_exists() return call_user_func( array( $provider, 'should_report_errors_for' ), $key ); } // Deprecated: JETPACK_EXTERNAL_STORAGE_REPORTING_ENABLED constant // @deprecated 6.18.13 Use should_report_errors_for() method in your Storage Provider instead. if ( defined( 'JETPACK_EXTERNAL_STORAGE_REPORTING_ENABLED' ) ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error trigger_error( 'JETPACK_EXTERNAL_STORAGE_REPORTING_ENABLED constant is deprecated. Implement should_report_errors_for() method in your Storage Provider instead.', E_USER_DEPRECATED ); } return (bool) constant( 'JETPACK_EXTERNAL_STORAGE_REPORTING_ENABLED' ); } return false; } /** * Determine if we should report an empty state based on delay mechanism. * We need this due to delays in writing in external storage vs writing into the database. * On first encounter of empty state, sets a transient. On subsequent encounters * after 10 minutes, allows reporting (indicating likely disconnection, not sync delay). * * @since 6.18.0 * * @param string $key The key that was empty. * @return bool True if we should report this empty state, false otherwise. */ private static function should_report_empty_state( $key ) { $delay_key = 'jetpack_external_storage_empty_delay_' . $key; $first_empty_time = get_transient( $delay_key ); if ( false === $first_empty_time ) { // First time encountering empty state - set delay transient and don't report yet set_transient( $delay_key, time(), 15 * MINUTE_IN_SECONDS ); // Keep for 15 minutes return false; } // Check if 10 minutes have passed since first empty encounter $delay_threshold = 10 * MINUTE_IN_SECONDS; if ( ( time() - $first_empty_time ) >= $delay_threshold ) { // 10+ minutes of empty state - likely disconnection, report it delete_transient( $delay_key ); return true; } return false; } /** * Determine if an event should be logged based on rate limiting rules. * * This prevents log spam from noisy events by applying a simple one-hour * rate limit per key, regardless of event type. * * @since 6.18.0 * * @param string $key The key that triggered the event. * @return bool True if the event should be logged, false if rate limited. */ private static function should_log_event( $key ) { $rate_limit_key = 'jetpack_ext_storage_rate_limit_' . $key; // Check if we're still within the rate limit period if ( get_transient( $rate_limit_key ) ) { return false; } set_transient( $rate_limit_key, true, HOUR_IN_SECONDS ); return true; } }