Blog · programming

Debugging an integration in production when you can’t reach the logs

2026-08-31

The problem

An integration with an external system “sort of works”: the form submits, the user sees a thank-you, and yet the data never reaches the other end. The code looks fine. The external system replies “200 OK”. And the lead is still nowhere to be found.

On your own machine you’d drop in a few log lines and catch the culprit. But the problem only shows in production – because production is what connects outward, and localhost doesn’t reach the world. That’s where the gymnastics begin: shared hosting, the PHP error log redirected somewhere you can’t find in the panel, and no console access to the server.

Why “200 OK” can lie

The key lesson here: the HTTP status code only tells you the server received the request – not that it accepted it. An external Form Handler can return “200 OK” and, in the response body, politely say “this field is required” while saving nothing. If you only look at the status code, you see success. The truth sits in the body, which you have to pull out and read separately.

The fix: a switch and a preview file

Instead of fighting the hosting logs, I do two things. First: my own log driven by a constant, off by default. It writes to a file next to the module, so I know where to find it.

// Off by default - I turn it on only for testing.
if (!defined('MY_DEBUG')) {
    define('MY_DEBUG', false);
}

private function debug_log($msg) {
    if (!MY_DEBUG) return;                         // silent when off
    $file = __DIR__ . '/_debug.log';
    @file_put_contents($file, '[' . date('Y-m-d H:i:s') . "] $msg\n", FILE_APPEND);
}

I log what I actually want to know: where it went, what went, and – crucially – the response body, not just its code:

$this->debug_log('URL: ' . $url);
$this->debug_log('FIELDS: ' . wp_json_encode($fields, JSON_UNESCAPED_UNICODE));
$this->debug_log('Response: ' . wp_remote_retrieve_response_code($response)
    . ' | body: ' . substr(wp_remote_retrieve_body($response), 0, 300));

Second: a small file that, when opened in a browser, prints the last lines of that log to the screen. So I don’t have to hunt for logs in the hosting panel at all – I open a URL, read, done.

<?php
// _preview.php - I delete this from the server along with the log.

$key   = 'put-something-random-here';
$given = is_string($_GET['k'] ?? null) ? $_GET['k'] : '';
if (!hash_equals($key, $given)) {
    http_response_code(404);   // 404, not 403 - don't confirm the file exists
    exit;
}

$path = __DIR__ . '/_debug.log';
if (!is_readable($path)) {
    exit('No log yet - did the action run?');
}

// Read only the tail, not the whole file - logs grow.
$f = new SplFileObject($path);
$f->seek(PHP_INT_MAX);
$from = max(0, $f->key() - 100);

header('Content-Type: text/plain; charset=utf-8');
foreach (new LimitIterator($f, $from) as $line) {
    echo $line;
}

The key in the URL isn’t hard security – it’s a barrier for those ten minutes, so nobody stumbles onto the file or guesses the address. That’s why 404 rather than 403: with no key, the file pretends it isn’t there. But since the log holds other people’s emails and phone numbers, the key alone isn’t an excuse – the real protection is that the file lives briefly and leaves the server the moment testing ends (more on that below). With text/plain I deliberately skip escaping: the browser renders nothing anyway, and htmlspecialchars() would turn characters in the API response into entities, making the log harder to read.

1. Flip the switch (constant to true), upload the file.
2. Trigger the action you're testing (e.g. submit the form).
3. Open the preview file in a browser - read what went out.
4. When done: switch back to false, delete from the server BOTH
   the preview file AND the log file.

It was exactly this loop – turn on, trigger, preview, turn off – that pinned down a whole series of small causes invisible from the outside: that the system rejected data despite “200”, that detection didn’t work the way I assumed, that field names didn’t match. Without it I’d be guessing in the dark, shipping fix after fix.

A note more important than the whole trick

Such a log records real form data – emails, phone numbers, names. It’s a temporary diagnostic tool, not part of the site. Once testing is done you must flip the switch off and delete both the preview file and the log file from the server. Left in production, it would be a personal-data leak on request.

What I learned

Not every problem is visible from the outside, and “200 OK” can be misleading. Sometimes the fastest route isn’t another fix by feel, but ten minutes spent building yourself a window through which you can finally see what’s really happening. That window is worth closing afterwards.

Back to all posts

Got a project to talk about?

Just tell me what you need - it does not have to be technical, that is my job. I will get back to you and tell you straight whether and how I can help.