Share My Creation Remote "Blackbox" Recorder

Hi everyone,

I always get help from the generous people here and this is my small contributions back that I have found handy. I hope I have captured it all correctly from my app so here goes :)

I've put together a "Blackbox" style remote logging routine that I use to catch errors in the wild. If a user encounters an issue, this routine sends the error details, app version, and raw data (like a JSON response or stack trace) to a private web dashboard for review.

The Workflow:

  1. The B4X App: When an error occurs (e.g., in a Try...Catch block), the app sends a POST request to your server.
  2. The Logger (PHP): A small script (remote_log.php) receives the data and appends it to a text file.
  3. The Viewer (PHP): A Bootstrap-based dashboard (view_logs.php) lets you view, filter for failures, and export logs to CSV.

1. The Server Side (PHP)

Create a folder on your server (e.g., /diagnostics/) and a sub-folder named /logs/. Make sure the /logs/ folder is writable (CHMOD 755 or 777).

File: remote_log.php (The endpoint)
remote_log.php:
<?php
// remote_log.php
$user = $_POST['user'] ?? 'unknown';
$error = $_POST['error'] ?? 'no_error';
$data = $_POST['data'] ?? 'no_data';
$ver = $_POST['ver'] ?? '0';

$logEntry = "[" . date("Y-m-d H:i:s") . "] User: $user | Ver: $ver | Msg: $error | Raw: $data" . PHP_EOL;

// Save to the logs folder
file_put_contents("logs/app_diagnostic.txt", $logEntry, FILE_APPEND);
echo "OK";
?>

File: view_logs.php (The Dashboard)
PHP:
<?php
/**
 * App Diagnostic Log Viewer & Exporter
 */

$logFile = 'logs/app_diagnostic.txt';

// --- CSV EXPORT LOGIC ---
if (isset($_GET['export']) && $_GET['export'] === 'csv') {
    if (!file_exists($logFile)) die("No log file found.");
    
    header('Content-Type: text/csv');
    header('Content-Disposition: attachment; filename="App_Logs_'.date('Ymd_His').'.csv"');
    
    $output = fopen('php://output', 'w');
    fputcsv($output, ['Timestamp', 'User ID', 'Version', 'Message/Label', 'Raw Data']);
    
    $entries = file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    foreach ($entries as $line) {
        preg_match('/\[(.*?)\] User: (.*?) \| Ver: (.*?) \| Msg: (.*?) \| Raw: (.*)/', $line, $matches);
        if (count($matches) >= 6) {
            fputcsv($output, [$matches[1], $matches[2], $matches[3], $matches[4], $matches[5]]);
        }
    }
    fclose($output);
    exit;
}

// --- VIEW LOGIC ---
if (!file_exists($logFile)) {
    die("No diagnostic logs found yet.");
}

$logEntries = array_reverse(file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES));
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>App Diagnostic Center</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    <style>
        body { background-color: #f8f9fa; font-size: 0.85rem; }
        .raw-data { max-height: 80px; overflow-y: auto; background: #f1f1f1; padding: 5px; font-family: monospace; font-size: 0.75rem; word-break: break-all; }
        .sticky-top-header { position: sticky; top: 0; background: #212529; color: white; z-index: 1000; }
    </style>
</head>
<body>
<div class="container-fluid py-4">
    <div class="d-flex justify-content-between align-items-center mb-3">
        <h2 class="h4">Remote Diagnostic Logs</h2>
        <div class="btn-group">
            <button onclick="window.location.reload();" class="btn btn-outline-primary btn-sm">Refresh</button>
            <a href="?export=csv" class="btn btn-outline-success btn-sm">Download CSV</a>
        </div>
    </div>

    <div class="table-responsive shadow-sm border rounded bg-white">
        <table class="table table-sm table-hover mb-0">
            <thead class="sticky-top-header">
                <tr>
                    <th width="15%">Time (AEST)</th>
                    <th width="10%">User</th>
                    <th width="5%">Ver</th>
                    <th width="20%">Label</th>
                    <th width="50%">Response Content</th>
                </tr>
            </thead>
            <tbody>
                <?php foreach ($logEntries as $line):
                    preg_match('/\[(.*?)\] User: (.*?) \| Ver: (.*?) \| Msg: (.*?) \| Raw: (.*)/', $line, $matches);
                    if (count($matches) < 6) continue;
                    
                    $isError = (stripos($matches[4], 'Crash') !== false || stripos($matches[4], 'Failure') !== false);
                ?>
                <tr class="<?php echo $isError ? 'table-danger' : ''; ?>">
                    <td class="text-nowrap"><?php echo $matches[1]; ?></td>
                    <td><span class="badge bg-light text-dark border"><?php echo htmlspecialchars($matches[2]); ?></span></td>
                    <td><?php echo htmlspecialchars($matches[3]); ?></td>
                    <td><span class="badge <?php echo $isError ? 'bg-danger' : 'bg-primary'; ?>"><?php echo htmlspecialchars($matches[4]); ?></span></td>
                    <td><div class="raw-data"><?php echo htmlspecialchars($matches[5]); ?></div></td>
                </tr>
                <?php endforeach; ?>
            </tbody>
        </table>
    </div>
</div>
</body>
</html>

2. The B4X Side

Place this routine in a code module. It uses jOkHttpUtils2 to send the data.
Sub RemoteLog:
Public Sub RemoteLog(ErrorMessage As String, RawData As String)
    ' Recommended: Only proceed if a diagnostic toggle is enabled in your app settings
    ' If Main.DiagnosticMode = False Then Return

    Dim j As HttpJob
    j.Initialize("RemoteLog", Me)
    
    ' Truncate very long data to keep the server happy
    Dim safeData As String = RawData
    If safeData.Length > 4000 Then safeData = safeData.SubString2(0, 4000)
    
    ' The URL to your remote_log.php script
    Dim logURL As String = "https://your-server.com/remote_log.php"
    
    ' Prepare the post data using your app's variables
    Dim postData As String
    postData = "user=" & Main.userID & _
               "&ver=" & Main.VersionCode & _
               "&error=" & StringToUrl(ErrorMessage) & _
               "&data=" & StringToUrl(safeData)
    
    j.PostString(logURL, postData)
    
    ' Note: For logging, "Fire and Forget" is usually fine.
End Sub

' Helper to encode the URL parameters
Private Sub StringToUrl(Text As String) As String
    Dim su As StringUtils
    Return su.EncodeUrl(Text, "UTF8")
End Sub

The "Story" / How to use it:

Once you have the PHP files on your server, you can call the routine whenever something goes wrong in your app.
I have a togle to turn debug mode on and off. It uses an Easteregg tap the label 5 time type thing to trigger it, store debug mode in a KVS, warn user each time app loads that debugging is turned on and asks wether to turn it off.

Example:
Example usage:
' Example: Logging a network failure
RemoteLog("Network_Failure_" & job.JobName, job.ErrorMessage)

' Example: Catching a crash
Try
    ' ... some database code ...
Catch
    RemoteLog("Database_Crash", LastException.Message)
End Try
Open view_logs.php in your browser, and you'll see a real-time list of every issue your users are hitting!
 
Top