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.
File: remote_log.php (The endpoint)
File: view_logs.php (The Dashboard)
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:
Open view_logs.php in your browser, and you'll see a real-time list of every issue your users are hitting!
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:
- The B4X App: When an error occurs (e.g., in a Try...Catch block), the app sends a POST request to your server.
- The Logger (PHP): A small script (remote_log.php) receives the data and appends it to a text file.
- 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