Kemp.rest / Examples / Code / Error Doctor
KEMP.REST / EXAMPLES / CODE
PressCare Error Doctor
Written by Kevin Kemp in PHP for PressCare Error Doctor 1.2.9. WordPress 7, the WordPress AI client, and JSON. The product page is presscare.com/presscare-error-doctor. The readme is on GitHub. Provider keys stay in WordPress Connectors.
View the codeA WordPress or PHP error log is a long list of repeated lines. Error Doctor reads the end of that log, groups the same event, and ranks what is happening now ahead of older history. The scan does not change the site. An explanation from OpenAI, Anthropic, or Google is a separate step, and it runs only after an administrator approves sending the sanitized report.
Type of code
A WordPress plugin. A diagnostic engine, a redactor, and an analyzer that calls the AI client already in WordPress 7. The admin screen is Tools → Error Doctor. The screens are on the design page.
Technology
PHP 8, WordPress 7, JSON, and the WordPress AI client. OpenAI, Anthropic, and Google are connected through the official provider adapters bundled with the plugin. The log read takes a shared lock and keeps only the final 2 MB.
What it does
- Finds the configured log, seeks to the last 2,097,152 bytes, and drops the partial first line so a cut event is not parsed.
- Groups repeated events for the dates the administrator picked, and counts critical events in that window apart from older ones.
- Redacts paths, emails, IP addresses, bearer tokens, API-key shapes, and database names before the report is stored.
- Sends that redacted report to the connected provider, capped at 4,000 tokens, and requires each finding to use a fingerprint from the evidence.
Problem it solved
A fatal error is easier to stop when the repeated lines have already been grouped, the secrets are already gone, and the next step is written down before anyone edits a file.
Code
Back to summary · Error Doctor 1.2.9 · readme
// —— src/Diagnostics/DiagnosticEngine.php — the read-only scan ——
/**
* @param array<string,mixed> $review_range Selected review range.
* @return array<string,mixed>|WP_Error
*/
public function scan( array $review_range = array() ): array|WP_Error {
$located = $this->locator->locate();
if ( is_wp_error( $located ) ) {
return $located;
}
$tail = $this->locator->read_tail( $located['path'] );
if ( is_wp_error( $tail ) ) {
return $tail;
}
$range = $this->normalize_review_range( $review_range );
$parsed = $this->parser->parse( $tail['content'], $range['start'], $range['end'], time() - ( self::RECENT_WINDOW_DAYS * 86400 ) );
$excluded_events = (int) $parsed['recency']['historical'];
$groups = $this->groups_in_review_range( $parsed['groups'] );
$recency = $this->review_recency( $groups );
$counts = $this->severity_counts( $groups );
$events_total = array_sum( array_column( $groups, 'count' ) );
$selected_fatal_events = array_sum(
array_map(
static fn ( array $group ): int => 'critical' === sanitize_key( (string) ( $group['severity'] ?? '' ) ) ? (int) ( $group['recent_count'] ?? 0 ) : 0,
$groups
)
);
$report = array(
'report_id' => wp_generate_uuid4(),
'generated_at' => gmdate( DATE_ATOM ),
'environment' => $this->environment(),
'site_health' => $this->health->inspect(),
'log' => array(
'source' => $located['source'],
'file' => sanitize_file_name( basename( $located['path'] ) ),
'file_size' => $tail['file_size'],
'bytes_read' => $tail['bytes_read'],
'truncated' => $tail['truncated'],
'modified_at' => $tail['modified_at'] > 0 ? gmdate( DATE_ATOM, $tail['modified_at'] ) : null,
),
'summary' => array(
'events_total' => $events_total,
'groups_total' => count( $groups ),
'counts' => $counts,
'recency_window_days' => $range['days'],
'recent_events_total' => $recency['recent'],
'historical_events_total' => 0,
'excluded_events_total' => $excluded_events,
'undated_events_total' => $recency['undated'],
'oldest_seen' => $recency['oldest_seen'],
'newest_seen' => $recency['newest_seen'],
'review_range_key' => $range['key'],
'review_range_start' => $range['start'],
'review_range_end' => $range['end'],
'selected_fatal_total' => $selected_fatal_events,
),
'groups' => $groups,
);
/**
* Filters the sanitized diagnostic report before it is stored or analyzed.
*
* Integrators must not add passwords, tokens, raw log contents, personal data,
* or other secrets to this report.
*
* @param array<string,mixed> $report Diagnostic report.
*/
$report = apply_filters( 'pcaied_diagnostic_report', $report );
return is_array( $report ) ? $this->redactor->redact_value( $report ) : new WP_Error(
'pcaied_invalid_report',
__( 'A third-party integration returned an invalid diagnostic report.', 'presscare-error-doctor' )
);
}
// —— src/Privacy/Redactor.php — secrets and personal data stripped before anything is stored ——
public function redact( string $value ): string {
$value = $this->redact_paths( $value );
$patterns = array(
'/(\btable\s+)[\'\x60][^\'\x60\r\n]+[\'\x60]/i' => '$1[database-table-redacted]',
'/(\bdatabase\s+)[\'\x60][^\'\x60\r\n]+[\'\x60]/i' => '$1[database-name-redacted]',
'/(\bfor user\s+)[\'\x60][^\'\x60\r\n]+[\'\x60](?:@[\'\x60][^\'\x60\r\n]+[\'\x60])?/i' => '$1[database-user-redacted]',
'/\bBearer\s+[A-Za-z0-9._~+\/-]+=*/i' => 'Bearer [redacted]',
'/\bsk-(?:proj-|ant-)?[A-Za-z0-9_-]{16,}\b/' => '[api-key-redacted]',
'/\bAIza[0-9A-Za-z_-]{20,}\b/' => '[api-key-redacted]',
'/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/' => '[jwt-redacted]',
'/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i' => '[email-redacted]',
'/(?<![\d.])(?:\d{1,3}\.){3}\d{1,3}(?![\d.])/' => '[ip-redacted]',
'/((?:api[_ -]?key|access[_ -]?token|secret|password|authorization)\s*[:=]\s*)[^\s&,;]+/i' => '$1[redacted]',
'/([?&](?:key|api_key|token|secret|password|auth)=)[^&#\s]+/i' => '$1[redacted]',
);
$redacted = preg_replace( array_keys( $patterns ), array_values( $patterns ), $value );
return is_string( $redacted ) ? $redacted : $value;
}
// —— src/AI/Analyzer.php — optional explanation, only after a provider is connected ——
$prompt = implode(
"\n\n",
array(
'You are PressCare Error Doctor with AI, a cautious senior WordPress diagnostic analyst.',
'Analyze only the supplied sanitized evidence. Do not invent files, versions, causes, vulnerabilities, or fixes. Every finding must use the exact fingerprint of the evidence group it explains; do not add unrelated findings. Prioritize recent activity over historical log context and return no more than eight of the most actionable findings. Avoid repeating the same explanation for related events. Distinguish observations from likely causes. Recommend verification steps before changes. Never recommend directly editing WordPress core. Keep steps concise and safe for a production site. If evidence is insufficient, say so and use low confidence.',
'Diagnostic evidence:',
(string) wp_json_encode( $payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ),
)
);
try {
$model = $this->providers instanceof ProviderManager ? $this->providers->cost_controlled_model( $provider_id ) : null;
if ( null === $model ) {
return new WP_Error(
'pcaied_ai_model_unavailable',
__( 'Error Doctor could not access its cost-controlled model for this provider. Try again later or choose another provider.', 'presscare-error-doctor' )
);
}
$result = wp_ai_client_prompt( $prompt )
->using_model( $model )
->using_max_tokens( self::MAX_OUTPUT_TOKENS )
->as_json_response( $this->schema( $fingerprints ) )
->generate_text();