4.9 KiB
Crash Report Endpoint
Clients (POS apps running in a browser or Node.js) can submit crash / uncaught-error details to the sync server for later inspection. The server writes each report to a plain-text file under logs/crash/.
POST /v1/crashreport
Paths may also be called as /api/v1/crashreport — both resolve to the same handler.
Request
- Method:
POST - Content-Type:
application/json - Body: any JSON object you want to record. Keep it free-form; common fields are shown below.
{
"app": "my-pos-app",
"version": "1.2.3",
"platform": "win32",
"userAgent": "Mozilla/5.0 ...",
"message": "Cannot read properties of undefined (reading 'price')",
"stack": "TypeError: Cannot read properties of undefined (reading 'price')\\n at ...",
"context": {
"currentView": "checkout",
"orderId": "abc-123"
},
"device": {
"id": "clerk-01",
"display": 1920
}
}
Nothing is validated server-side: the body is logged as-is (JSON.stringify(body, null, 2)).
Responses
| Status | Body | Meaning |
|---|---|---|
200 |
{ "Message": "OK", "file": "<timestamp>-<ip>.txt" } |
Report saved |
400 |
{ "Message": "Invalid JSON body." } |
Body is not valid JSON |
Non-JSON or empty bodies are treated as follows:
- Empty body → saved as an empty object
{}. - Invalid JSON (e.g. a raw text dump) → rejected with
400.
Log file format
Each accepted report is written to:
logs/crash/<timestamp>-<ip>.txt
<timestamp>— local server time,YYYYMMDD-HHMMSS.<ip>— the client's remote address (::ffff:IPv4 prefix is stripped;:is replaced with_).
Two reports from the same client within the same second overwrite each other. Include a unique id inside the body if you need to guarantee every report is preserved.
Example file:
timestamp: 2026-08-17T09:41:22.731Z
ip: ::ffff:127.0.0.1
user-agent: node-fetch/1.0 (+https://github.com/bitinn/node-fetch)
{
"app": "my-pos-app",
"message": "Cannot read properties of undefined",
"stack": "TypeError: ..."
}
The log directory logs/crash/ is created automatically on first use.
Usage from Node.js
// plain fetch (Node 18+)
const baseUrl = 'https://192.168.1.10:4443';
const AUTH_TOKEN = '<your pairing token>'; // optional if your server does not enforce it
fetch(`${baseUrl}/v1/crashreport`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
app: 'my-pos-app',
version: '1.2.3',
message: error.message,
stack: error.stack,
}),
})
.then(async (res) => console.log(res.status, await res.json()))
.catch((err) => console.error('could not send crash report', err));
Because the server uses a self-signed certificate, set rejectUnauthorized: false when using node-fetch or Node's https module:
import https from 'node:https';
import fetch from 'node-fetch';
const agent = new https.Agent({ rejectUnauthorized: false });
fetch('https://<host>:4443/v1/crashreport', {
method: 'POST',
agent,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ app: 'my-pos-app', message: error.message, stack: error.stack }),
})
.then(async (res) => console.log(res.status, await res.json()));
Hook it into your global error handlers so nothing is lost:
process.on('uncaughtException', (err) => {
void sendCrashReport(err);
// ...your own logging / shutdown
});
process.on('unhandledRejection', (reason) => {
void sendCrashReport(reason instanceof Error ? reason : new Error(String(reason)));
});
Usage from the browser
Use fetch with mode: 'no-cors' or a normal cors request — the server sends Access-Control-Allow-Origin: *, so a plain fetch works from any origin.
function sendCrashReport(payload) {
return fetch('https://<host>:4443/v1/crashreport', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
.then(async (res) => {
const json = await res.json();
console.log(`crash report ${res.ok ? 'saved' : 'failed'}`, json);
return json;
})
.catch((err) => console.error('could not send crash report', err));
}
Wire it into window.onerror and unhandledrejection:
window.addEventListener('error', (event) => {
sendCrashReport({
app: 'my-pos-app',
version: appVersion,
message: event.message,
stack: event.error?.stack,
context: { url: location.href },
});
});
window.addEventListener('unhandledrejection', (event) => {
sendCrashReport({
app: 'my-pos-app',
version: appVersion,
message: event.reason?.message || String(event.reason),
stack: event.reason?.stack,
});
});
Note: if you use
mode: 'no-cors'the browser will send the request but you will not be able to read the JSON response body — the report is still saved server-side.