Laravel exception reporter that routes errors to a TicketMate helpdesk instance — fingerprinted, rate-limited, context-rich. Replaces Sentry for teams that already run TicketMate.
When your Laravel app throws an unhandled exception, this package:
- Fingerprints it (
sha1(class + top-app-frame)) so the same bug doesn't open 500 tickets. - Coalesces bursts — 1000 errors in 60s = 1 HTTP call to TicketMate with
burst_count=1000. - Builds rich context — full stack trace (app + vendor flagged separately), request URL + method, logged-in user, environment, release SHA, PHP/Laravel version.
- Sanitizes — passwords, tokens, secrets, API keys, CSRF, cookies stripped before upload.
- Queues the POST — reporting runs async on your worker; the user's error page is never slowed down.
- Respects silence windows — if the app's domain is put in maintenance mode on the TicketMate side, events still get recorded but no tickets are cut.
TicketMate then fingerprint-dedups, creates a ticket (with a matching GitHub issue via the existing issuetracker flow), runs AI triage for a clean subject + probable-cause note, and auto-assigns the domain.
- PHP 8.2+
- Laravel 10, 11, 12, or 13
- A TicketMate instance with a configured
Repositoryrow for this app
composer require d3vnz/helpdesk-loggerAdd the endpoint + token to .env:
HELPDESK_LOGGER_ENDPOINT=https://helpdesk.d3v.nz
HELPDESK_LOGGER_TOKEN=<the Repository api_token from TicketMate admin>
HELPDESK_LOGGER_ENVIRONMENT="${APP_ENV}"
# HELPDESK_LOGGER_RELEASE is auto-detected from .git/HEAD and platform
# env vars (Heroku, Vercel, Railway, Render, GitHub, …). Only set this
# if you're deploying from an artifact where .git is stripped.The SHA stamped on every event is discovered automatically, in order:
HELPDESK_LOGGER_RELEASEenv var — manual override wins- Platform env vars —
SOURCE_VERSION,VERCEL_GIT_COMMIT_SHA,RAILWAY_GIT_COMMIT_SHA,RENDER_GIT_COMMIT,GITHUB_SHA,CI_COMMIT_SHA,BITBUCKET_COMMIT,COMMIT_SHA,GIT_COMMIT .git/HEADfile — covers Laravel Forge and any git-based deploy (noexec(), no shell)- Empty if none of the above apply
The same token the d3vnz/issuetracker package uses — one token, two uses.
Local dev reports nothing by default. The package only fires on APP_ENV matching production or staging. Override with:
HELPDESK_LOGGER_ENABLED=true # force on (ignores APP_ENV)
HELPDESK_LOGGER_ENABLED=false # force off everywhere
HELPDESK_LOGGER_ENVIRONMENTS="production,staging,preview" # custom allowlistLaravel 11+ (bootstrap/app.php):
use D3vnz\HelpdeskLogger\Facades\Helpdesk;
return Application::configure(basePath: dirname(__DIR__))
// ...
->withExceptions(function (Exceptions $exceptions) {
Helpdesk::captureExceptions($exceptions);
})
->create();Laravel 10 (app/Exceptions/Handler.php):
use D3vnz\HelpdeskLogger\Facades\Helpdesk;
public function register(): void
{
$this->reportable(function (\Throwable $e) {
Helpdesk::report($e);
});
}That's it. Throw anything and it'll show up in TicketMate within a few seconds.
Publish the config to tweak capture / sanitization / sampling:
php artisan vendor:publish --tag=helpdesk-logger-configKey knobs (see config/helpdesk-logger.php for the full list):
| Key | Default | Purpose |
|---|---|---|
enabled |
(unset) | Explicit on/off. Wins over the allowlist. |
environments |
['production','staging'] |
When enabled is unset, only report for these APP_ENV values. |
burst_window_seconds |
60 |
Coalesce same-fingerprint events in this window. |
sample_rate |
1.0 |
Drop-rate for noisy apps (1.0 = report every unique error). |
ignore_exceptions |
[404, 401, CSRF, validation, auth] |
Exceptions silently dropped. |
sanitize_keys |
[password, token, secret, api_key, ...] |
Request-body keys redacted. |
capture.request_body |
true |
Include sanitized request body in reports. |
capture.session |
false |
Session data (off by default — sensitive). |
capture.cookies |
false |
Cookie names (values never captured). |
capture.server_env |
false |
$_SERVER vars (off — CI secrets leak risk). |
stack.max_frames |
40 |
Ship the top N frames. |
max_context_bytes |
65536 |
Hard cap on context JSON size. |
A one-shot artisan command fires a fake exception through the full pipeline so you can verify the round-trip without waiting for a real bug:
php artisan helpdesk:testOutput includes the endpoint, fingerprint, circuit-breaker state, and the TicketMate response with a direct link to the created ticket.
Useful flags:
--dry-run— print the payload that would be sent, don't actually POST.--message="Custom text"— override the generated test message.--fingerprint=abc123— force a specific fingerprint (test dedup/reopen against an existing ticket).
The command bypasses the queue, the SpikeGate, and any open circuit so the test always reflects the current TM reachability.
If the endpoint becomes unreachable, we don't want every failing exception in your app to time out the queue worker and spam the log. After 3 failures in 60 seconds the circuit opens for 5 minutes — every send() during that window returns immediately without touching the network. The first attempt after the window acts as a canary: success closes the circuit, failure reopens it.
Defaults are conservative; tune via env:
HELPDESK_LOGGER_CIRCUIT_THRESHOLD=3 # failures required to open
HELPDESK_LOGGER_CIRCUIT_WINDOW=60 # rolling failure window (sec)
HELPDESK_LOGGER_CIRCUIT_OPEN_FOR=300 # how long the circuit stays open (sec)Combined with the existing layers — queued dispatch (no blocking the user request), tries=1 on the job (no retry storms), try/catch around everything (never re-throws into Laravel), and the SpikeGate's fingerprint coalescing — this means a dead helpdesk never double-errors your app. It quietly does nothing.
Inspect from code:
$status = app(\D3vnz\HelpdeskLogger\Reporter::class)->circuitStatus();
// ['open' => bool, 'open_until' => ISO8601|null, 'failures' => int]- Per-fingerprint burst window: first event fires immediately; subsequent events in the same 60s window silently bump a counter. When the window expires, the next event fires with
burst_count = <silent events + 1>. - Server-side dedup: TicketMate fingerprint-matches by
(repository, fingerprint). Second burst with the same fingerprint appends an occurrence to the existing ticket; reopens it if closed; bumps the count; drops a milestone note at 10 / 50 / 100 / 500 / 1000 / 5000 / 10000. - Per-hour ticket cap: TicketMate rejects NEW-fingerprint tickets past a configurable per-hour limit (default 25). Orphan occurrences still get stored so you see post-incident what you missed.
- Silence mode: staff can mute a domain from the TicketMate Domain edit page (15m / 1h / 4h / 24h / forever). During silence, occurrences are stored but no tickets are created or reopened.
Intentional omissions:
- No raw request body is shipped beyond 8KB — larger bodies replaced with a truncation stub.
- No
Authorization/Cookieheader values. - No session VALUES (keys only, and only with
capture.session=true). - No
$_SERVERunlesscapture.server_env=true(CI secrets live there). - User objects →
id + email + nameonly, never model internals.
php artisan tinker
> \Helpdesk::report(new \RuntimeException('test error from tinker'))With HELPDESK_LOGGER_QUEUE_CONNECTION=sync in .env you'll see the HTTP call happen inline. Check TicketMate's /admin/tickets for the new ticket.
MIT — see LICENSE.