Error tracking
Know the moment your server starts returning errors or your frontend starts throwing — without digging through logs.
How it works
Both tracking modes are off by default and configured independently under Settings → Errors. Every occurrence is grouped into a de-duped issue— repeat hits of the same underlying error increment a counter instead of flooding you with one row per request. You get an email the first time a new issue shows up, or if one you've already marked resolved starts happening again.
- Server-side — your own backend calls an authenticated API whenever it hits an error.
- Frontend — a checkbox; the tracking snippet you've already installed starts capturing uncaught JavaScript errors automatically, no code change needed.
Server-side: POST /v1/errors
Call this from your own backend whenever it hits an error worth knowing about — a Laravel exception handler, an Express error middleware, or any language that can make an HTTP request. Authenticated with your site's API key, the same as the Transactions API.
curl -X POST https://api.shonylabs.com/v1/errors \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": "Call to undefined method App\\Models\\User::foo()",
"status_code": 500,
"path": "/checkout",
"stack": "Error: Call to undefined method\n#0 /app/Http/Controllers/CheckoutController.php(42)"
}'| Field | Required | Notes |
|---|---|---|
message | Yes | Up to 500 characters. Two reports with the same message, status code, and path (numbers in the message are collapsed first) are grouped into the same issue. |
status_code | No | The HTTP status this error produced, e.g. 500. Only recorded if it falls within the range configured in Settings → Errors (default 500-599). Omit it entirely for an error with no HTTP response behind it (e.g. a background job) — those are always tracked once enabled. |
path | No | Up to 500 characters, e.g. /checkout. |
stack | No | Up to 4,000 characters. Shown when you expand the issue on the Errors page. |
context | No | A flat JSON object of your own key/value pairs — order ID, environment, provider, anything worth attaching. Up to 10 keys, 255 characters per value. |
timestamp | No | ISO 8601 event time. Defaults to the time the request is received. |
shonylabs_visitor_id | No | Links the error back to the visitor it happened to — see "Linking an error to a visitor" below. |
A response of { "accepted": false, "reason": "..." }(still HTTP 200) means the report was received but intentionally not recorded — either error tracking isn't enabled for this site, or the status code you sent isn't in the tracked range. Your integration doesn't need to treat that as a failed request.
Laravel example
Add this to your exception handler's report() method so every unhandled exception gets reported:
// app/Exceptions/Handler.php
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
public function report(Throwable $e)
{
parent::report($e);
Http::withToken(env('SHONYLABS_API_KEY'))->post('https://api.shonylabs.com/v1/errors', [
'message' => $e->getMessage(),
'status_code' => $e instanceof HttpExceptionInterface ? $e->getStatusCode() : 500,
'stack' => (string) $e,
'path' => request()->path(),
'shonylabs_visitor_id' => request()->cookie('shonylabs_visitor_id'),
]);
}Express example
Register this as your last error-handling middleware:
// Express error-handling middleware — register it last, after all other app.use()/routes.
// req.cookies requires the cookie-parser middleware; omit shonylabs_visitor_id if you don't use it.
app.use((err, req, res, next) => {
fetch('https://api.shonylabs.com/v1/errors', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.SHONYLABS_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: err.message,
status_code: err.statusCode || 500,
path: req.path,
stack: err.stack,
shonylabs_visitor_id: req.cookies?.shonylabs_visitor_id,
}),
}).catch(() => {});
next(err);
});Attaching context
Attach whatever will help you diagnose the error later — an order ID, environment, or the provider involved:
curl -X POST https://api.shonylabs.com/v1/errors \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": "Payment provider timeout",
"status_code": 502,
"context": {
"order_id": "order_9F2K",
"provider": "stripe",
"environment": "production"
}
}'Linking an error to a visitor
The tracking snippet already mirrors the visitor's ID into a first-party shonylabs_visitor_id cookie — the same one the Transactions API uses to attribute a purchase. Read it server-side and pass it along (see the shonylabs_visitor_idline in the Laravel and Express examples above) and the Errors page will show who was affected, with a link straight to their visitor journey. It's optional — omit it and the issue still records everything else, just without a name attached.
Frontend: automatic JavaScript error capture
Turn on Enable frontend error trackingunder Settings → Errors and the tracking snippet you've already installed starts listening for uncaught errors and unhandled promise rejections in visitors' browsers — no code change, no redeploy. This works with both the cookie-based and cookieless tracking snippets. Unlike a server-reported error, a browser error always carries the visitor who hit it — no extra field to pass, it's automatic.
Viewing and resolving issues
Your site's Errors pagelists every issue — source, status code, message, occurrence count, first/last seen, and (when available) the most recently affected visitor, linked to their journey. Click a row to expand the stack trace and any context you attached. Once you've shipped a fix, click Resolve — if the same fingerprint occurs again later, the issue reopens and you get a fresh email, the same as a brand new issue.