GAP School Module 10 — Production Hardening Lesson 10.4

A deploy that doesn’t break the homepage can still break the contact form. Standard uptime monitoring — which checks that a URL returns HTTP 200 — doesn’t catch functional failures. Only a check that verifies the form actually processes a submission catches the class of bug that shows up most often: a PHP change that breaks a specific AJAX endpoint while leaving the rest of the site intact.


The situation

The Anchor build went live without a monitoring layer. Two weeks in, the AJAX endpoint for the contact form silently returned an error for 3 hours — a PHP change had an edge case nobody caught in testing. The owner found out when a customer called to say the form “didn’t work.” The fix was 4 minutes. The exposure window was 3 hours. After that incident: external uptime monitoring, synthetic form submission checks, and PHP error log alerting.


What I did

Uptime monitoring (external)

An external uptime service (UptimeRobot, Better Uptime, or equivalent) sends HTTP requests to key URLs every 5 minutes and alerts on non-200 responses or response time thresholds:

Monitor targets
Monitor targets: - https://[site]/ → 200 within 3s - https://[site]/inventory/ → 200 within 4s, contains "unit-card" - https://[site]/wp-admin/admin-ajax.php → 400 (expected for unauthenticated) - https://[site]/wp-json/ → 200 (REST API up) Alert channels: - SMS to owner's phone (P1 — site down) - Email to owner (P2 — slow response) - Slack/webhook for dev team (all alerts)

Synthetic form check

A cron job runs every 15 minutes and submits a test lead through the contact form AJAX endpoint. If the response isn’t a success JSON, it sends an alert:

PHP — synthetic check + alert
add_action( '[client]_synthetic_form_check', '[client]_run_synthetic_check' ); function [client]_run_synthetic_check(): void { $response = wp_remote_post( home_url( '/wp-admin/admin-ajax.php' ), [ 'timeout' => 10, 'body' => [ 'action' => '[client]_contact_lead', 'first_name' => 'Synthetic', 'last_name' => 'Check', 'email' => 'synthetic@internal.[site]', 'phone' => '5555550000', 'message' => 'Automated synthetic check — not a real lead', 'is_synthetic' => '1', [client]_get_nonce_field_name() => wp_create_nonce( '[client]_contact' ), ], ] ); if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) { [client]_send_monitoring_alert( 'Contact form AJAX endpoint failed', [ 'status' => wp_remote_retrieve_response_code( $response ), 'body' => wp_remote_retrieve_body( $response ), ] ); return; } $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( empty( $body['success'] ) ) { [client]_send_monitoring_alert( 'Contact form returned success:false', $body ); } } function [client]_send_monitoring_alert( string $message, array $context = [] ): void { wp_mail( [client]_get_setting( 'alert_email', get_option( 'admin_email' ) ), '[ALERT] ' . get_bloginfo( 'name' ) . ': ' . $message, print_r( $context, true ) ); }

PHP error log alerting

The error log is the first place real bugs appear. A cron job scans the PHP error log every 30 minutes for new Fatal error or PHP Warning entries using incremental file position tracking to avoid re-reading the entire log on every pass:

PHP — incremental error log scan
function [client]_scan_error_log(): void { $log_path = ini_get( 'error_log' ); if ( ! $log_path || ! file_exists( $log_path ) ) { return; } $last_checked = get_option( '[client]_error_log_last_pos', 0 ); $current_size = filesize( $log_path ); if ( $current_size <= $last_checked ) { update_option( '[client]_error_log_last_pos', $current_size ); return; } $handle = fopen( $log_path, 'r' ); fseek( $handle, $last_checked ); $new_lines = ''; while ( ( $line = fgets( $handle ) ) !== false ) { if ( str_contains( $line, 'Fatal error' ) || str_contains( $line, 'PHP Warning' ) ) { $new_lines .= $line; } } fclose( $handle ); update_option( '[client]_error_log_last_pos', $current_size ); if ( $new_lines ) { [client]_send_monitoring_alert( 'New PHP errors detected', [ 'log_excerpt' => $new_lines ] ); } }

Why it matters

The contact form check catches the most common production failure mode: a code change that breaks a specific AJAX endpoint while leaving the rest of the site intact. Standard uptime monitoring doesn’t catch this — the site returns 200. Only a check that verifies functional behavior catches it.

The error log scan catches fatal PHP errors before they cascade. A single fatal error in a function called on every page load can silently degrade performance for hundreds of requests before uptime monitoring notices the increased response time. The incremental file position pattern means the scan doesn’t re-read the full log every 30 minutes — it picks up from the last byte read, so the scan stays fast even on high-volume error logs.


The Anchor build

External uptime monitoring on 4 endpoints. Synthetic form check running every 15 minutes. PHP error log scan every 30 minutes. P1 MTTD (mean time to detect) after monitoring went live: 4 minutes. Prior: 3 hours (owner found out from a customer call). Zero customer-reported P1 incidents after monitoring was in place. The synthetic check caught 2 regression bugs in the first 6 months — both were PHP changes that had no visible effect on the homepage but broke the contact form AJAX handler.


Do this, not that

  • Monitor function, not just availability. An HTTP 200 check tells you the server responded. A synthetic form check tells you the form actually works. Both are necessary — neither substitutes for the other.
  • Alert on PHP errors immediately. Error logs that nobody reads are logs that tell nobody about problems. A 30-minute scan cycle is acceptable; a “check it when you remember” cycle is not.
  • Set the alert threshold lower than you think you need to. A 3-second response time alert feels aggressive until the day a bad database query starts taking 2.8 seconds and the alert fires 6 hours before real users notice the slowdown.
  • Route alerts to the owner’s phone for P1, email for P2. An alert that only goes to email gets seen during business hours. A form that’s been down all night gets seen in the morning. Phone for down, email for slow.
When you’re ready to build

The lessons are yours. When you want it built, we’re here.

Every lesson stays free — no account, no paywall, no email gate, ever. But if you’d rather have this system standing on your business than wire all 48 lessons yourself, leave your email. We’ll send you a direct line to a build — and you’ll be first to hear when we add new tools to the curriculum.

None of this gates a single lesson. The curriculum was free before you got here and it stays that way.

We’ll use your email to send you a fast-track to a GAP build and occasional notes on how GAP builds digital sales departments. Lessons stay 100% free — no email required to read any of them. We never share or sell your information. Unsubscribe any time. Privacy policy at gapindustriesllc.com/privacy.html.

Done learning how it’s built? We’ll build it.

You came here to understand the system, and now you do. If you’d rather have it standing on your business than spend the next three months wiring it yourself, GAP Concierge is the same architecture from these lessons — a white-label AI agent that knows your catalog and captures your leads — set up for you, from $97/mo.

See GAP Concierge →