boneybone / laravel-bug-report
Floating "report a problem" button for Laravel apps that attaches console logs, XHR/fetch calls, a screenshot and server exceptions to each ticket.
Package info
gitlab.com/nazarsyafiq89/bb-laravel-bug-report
pkg:composer/boneybone/laravel-bug-report
Requires
- php: ^8.0.2
- guzzlehttp/guzzle: ^7.2|^8.0
- illuminate/console: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/http: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/log: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/queue: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/routing: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/view: ^9.0|^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- laravel/pint: ^1.0
- orchestra/testbench: ^7.0|^8.0|^9.0|^10.0|^11.0
- pestphp/pest: ^1.22|^2.0|^3.0|^4.0
Suggests
- boneybone/auth-server-sdk: Signs deliveries to your ticketing API with this app's auth server client token (auth => auth-server).
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-10-27 19:58:26 UTC
README
A floating "Report a problem" button for Laravel apps. When a logged-in user clicks it, a side panel slides in. They write what went wrong and send it as a ticket. The ticket automatically includes:
- the browser console output and JS errors (including failed scripts/images and unhandled promise rejections)
- the recent XHR / fetch calls: method, URL, status, duration, and request/response bodies (truncated, with secrets masked)
- an automatic screenshot of what the user was looking at
- the Laravel exceptions that user hit in the last hour, each with class, message, file:line and a short trace
- page URL, browser, viewport, timezone, the user's id/name/email, and app name/env/Laravel version
Tickets are saved under storage/app/bug-report and sent to the ticket system, which files them as tickets. It can also send them to a webhook or any other ticketing API. After sending, the reporter sees the ticket system's ticket number (e.g. BBV/2026/0185) and an Open ticket button that opens the ticket in a new tab.
- Works on Laravel 9–13 with Blade, Livewire 2–4, Inertia (Vue/React) and Turbo. Laravel 9–11 no longer get security fixes from Laravel, and recent Composer versions refuse to install them, so upgrade apps still on them when you can.
- Built for apps that sign in with
boneybone/auth-server-sdk: it reads the signed-in user the SDK gives you, and signs its requests to the ticket system with the app's own client token. - The widget is plain JS with standalone CSS inside a Shadow DOM. There's no build step, so your Tailwind/Bootstrap can't break it and it can't affect your styles.
Install
composer require boneybone/laravel-bug-report:^1.0
Releases are tagged (v1.0.0, …) and listed in CHANGELOG.md; composer update then only brings releases, never work in progress on main.
Apps that installed it from the private GitLab repository can remove the laravel-bug-report entry from repositories in their composer.json; the next composer update gets it from Packagist.
The button appears for logged-in users on every HTML page served through the web middleware group. There are no migrations and no assets to publish.
Sending reports to the ticket system
- The app has
boneybone/auth-server-sdkinstalled and configured (AUTH_SERVER_CLIENT_ID,AUTH_SERVER_CLIENT_SECRET,AUTH_SERVER_ENDPOINT), pointing at the same auth server as the ticket system. Add one line to the app's
.env:BUG_REPORT_API_URL=https://<ticket-system-domain>/api/bug-reportsRun a queue worker (
php artisan queue:work) so a report the ticket system couldn't take right away is retried.
There is no token to create. Each report is signed with the app's own client token, and the ticket system checks it with the auth server.
The ticket system then:
- files the report as a ticket, with the reporter's words, the app it came from and the page they were on;
- matches the reporter to their user by auth-server id, falling back to email;
- shows the console, JS errors, network calls and server exceptions under Technical details on the ticket, for staff only;
- attaches the screenshot to the ticket.
If the ticket system receives the same report twice (a retry after a slow answer), it returns the ticket it already made.
Optional:
php artisan vendor:publish --tag=bug-report-config
BUG_REPORT_POSITION=bottom-right # bottom-right | bottom-left | top-right | top-left
BUG_REPORT_WEBHOOK_URL=https://hooks.example.com/bug
BUG_REPORT_WEBHOOK_SECRET=change-me # signs the body (X-Bug-Report-Signature: sha256=...)
BUG_REPORT_API_URL=https://tickets.example.com/api/bug-reports
BUG_REPORT_API_AUTH=auth-server # auth-server (default) | token | none
BUG_REPORT_API_TOKEN=... # only for BUG_REPORT_API_AUTH=token
BUG_REPORT_QUEUE_CONNECTION=redis # optional; defaults to your default queue
Schedule the cleanup (it keeps retention_days, 30 by default):
// Laravel 11+: routes/console.php
Schedule::command('bug-report:prune')->daily();
// Laravel 9/10: app/Console/Kernel.php
$schedule->command('bug-report:prune')->daily();
How it works
| Piece | What it does |
|---|---|
InjectBugReport middleware | Appended to the web group. Adds two <script> tags right after <head> for authenticated users on HTML responses. Skips JSON, redirects, downloads, Inertia/Livewire XHR requests and guests. |
bug-report.js | Starts capturing straight away, before your app's scripts run. Mounts the button on DOMContentLoaded and stays mounted across wire:navigate, Turbo and Inertia page changes. |
ServerErrorRecorder | Listens to Laravel's MessageLogged event. Every reported exception passes through it on every Laravel version, and it stores them per user. Error responses get an X-Bug-Report-Error header, so a failed XHR in the ticket points to its server exception. |
GET /bug-report/tickets | Same middleware, throttle:30,1. The signed-in user's own reports for My reports, from a small per-user index under storage/app/bug-report/tickets/_users. |
POST /bug-report/tickets | web + auth + throttle:5,1. Validates, redacts again on the server, stores the ticket and fires TicketSubmitted. Destinations with send_now (the ticketing API, by default) are sent right away so the reporter gets their ticket number; the rest, and any that fail, are queued as one SendTicket job per destination. With the sync queue those jobs run after the response, so users never wait on your webhook. |
Delivery status (sent / retrying / failed, attempts, remote id, ticket number and link, error) is written back into each ticket's ticket.json.
Queues: use a real queue (
database,redis…) with a worker running, so failed deliveries are retried with backoff. Without a worker, a report the ticket system couldn't take right away (and every webhook delivery) waits in thejobstable. On thesyncqueue a delivery is tried once after the response. Under php-fpm the user doesn't wait for it, but underphp artisan servethey do. Don't point a destination back at the same single-threadedartisan serveinstance, because it will block until the timeout.
Destinations
Webhook
The webhook receives this JSON:
{
"event": "ticket.created",
"text": "[BR-261020-1A2B3C] Save button does nothing — Aisyah (https://app.test/leave)",
"ticket": { "id": "...", "reference": "BR-261020-1A2B3C", "title": "...", "description": "...",
"user": {...}, "page": {...}, "client": {...}, "app": {...},
"console": [...], "errors": [...], "network": [...], "server_errors": [...],
"context": {...}, "screenshot": "https://app.test/bug-report/tickets/<id>/screenshot?expires=...&signature=..." }
}
textmakes Slack, Google Chat and Mattermost incoming webhooks show something readable without extra work.- The screenshot is a signed link that stays valid for
screenshot_link_days. Set'screenshot' => 'base64'to embed the image instead, or'none'to leave it out.
Verifying the signature on the receiving side:
$expected = 'sha256='.hash_hmac('sha256', $request->getContent(), env('BUG_REPORT_WEBHOOK_SECRET'));
abort_unless(hash_equals($expected, $request->header('X-Bug-Report-Signature', '')), 401);
Your ticketing API
'ticketing' => [
'driver' => 'http',
'url' => env('BUG_REPORT_API_URL'),
'method' => 'post', // post | put | patch
'auth' => 'auth-server', // auth-server | token | none
'token' => env('BUG_REPORT_API_TOKEN'),
'headers' => ['X-Tenant' => 'hq'],
'format' => 'json', // or 'multipart': screenshot uploaded as a file
'screenshot' => 'base64', // json format: url | base64 | none
'screenshot_field' => 'screenshot',
'mapper' => App\BugReport\TicketMapper::class,
'response_id_key' => 'data.id', // stored as the remote ticket id
'response_reference_key' => 'serial_number', // ticket number shown to the reporter
'response_url_key' => 'url', // shown as an "Open ticket" button (new tab)
'send_now' => true, // send while the reporter waits (falls back to the queue)
'send_now_timeout' => 8,
],
With send_now on (the default), the ticket is sent while the reporter waits, so the panel shows your system's ticket number (e.g. BBV/2026/0185) and an Open ticket button that opens it in a new tab. If the API fails or is too slow, the panel shows the local BR-… reference and the queued delivery retries it.
With auth set to auth-server (the default), each delivery signs in with this app's own client token from boneybone/auth-server-sdk (AuthServer::getClientAccessToken()), so there is no token to set up: the receiving API checks it with the SDK's AuthServerClientAuthenticationMiddleware. The ticket system's POST /api/bug-reports takes the default payload as it is. The token is kept in your app's cache until a minute before it expires (50 minutes at most), so a report doesn't wait on the auth server each time; if the API refuses it, the report is sent once more with a new one. Use token for any other API.
A mapper reshapes the ticket into the fields your API expects:
namespace App\BugReport;
use Boneybone\BugReport\Delivery\Contracts\PayloadMapper;
use Boneybone\BugReport\Delivery\DefaultPayloadMapper;
use Boneybone\BugReport\Support\Ticket;
class TicketMapper implements PayloadMapper
{
public function map(Ticket $ticket, array $config): array
{
return [
'subject' => $ticket->get('title'),
'description' => $ticket->get('description'),
'reporter_email' => $ticket->get('user.email'),
'category' => 'bug',
'source_url' => $ticket->get('page.url'),
'screenshot' => DefaultPayloadMapper::screenshot($ticket, $config['screenshot'] ?? 'base64'),
'technical_details' => json_encode([
'console' => $ticket->get('console'),
'network' => $ticket->get('network'),
'errors' => $ticket->get('errors'),
'server_errors' => $ticket->get('server_errors'),
'client' => $ticket->get('client'),
]),
];
}
}
Anything else
You can add a destination with any class that implements Boneybone\BugReport\Delivery\Contracts\Destination:
'jira' => ['driver' => App\BugReport\JiraDestination::class, 'url' => '...'],
The class receives its config array as a $config constructor argument. You can also listen for Boneybone\BugReport\Events\TicketSubmitted to react inside your app.
Customising the widget
All of these are in config/bug-report.php:
button:positionandoffset_x/offset_ydraggable: users can drag the button, the position is remembered per browser, and the panel has a "Reset button position" linklabel,show_label,color,z_index
texts: every string in the panel, e.g. to translate it.gate: a Gate ability name that limits who sees the button, e.g.'report-bugs'.except: path patterns where the button never appears.capture: buffer sizes, body capture on/off, body length limitignore_urls(full-URL patterns with*; Laravel Boost's*/_boost/*is ignored by default) andignore_console(whole-message patterns with*; Boost's "Browser logger active" line is ignored by default).my_reports: the My reports link in the panel lists the user's own reports (up tolimit, kept forretention_days) with the ticket system's number, whether it was sent, and an Open ticket link. Setenabledtofalseto hide it.user_fields: the user's fields sent with each report (id,name,email). They're read from the SDK'sBoneyBoneUsertoo, and a user with onlyfirst_name/last_namegets a combined name.redact_keys: keys that get masked. They're checked in the browser and again on the server.
Manual placement
Set auto_inject to false and put the directive in your layout's <head>:
<head>
@bugReport
...
</head>
JavaScript API
BugReport.open(); // open the panel from your own "Help" menu
BugReport.addContext({ tenant: 'hq', cartId: 123 }); // extra data attached to the next report
BugReport.log('Clicked approve', { requestId: 55 }); // breadcrumb that only appears in reports
Privacy notes
- Only authenticated users get the widget, and tickets are only accepted from authenticated users.
- Request/response bodies are truncated to
body_limitcharacters. Values whose keys look sensitive (password, token, authorization, cookie, card numbers…) are masked in the browser before they're stored. They're masked again on the server. - Bodies are only read for text/JSON responses. Binary, HTML and streaming responses are recorded as metadata only.
- Server errors are kept per user for
server_errors.ttl_minutes(60 by default). - Users see a summary of exactly what's attached under "Technical details" before they send.
Troubleshooting
Each report's delivery status is in storage/app/bug-report/tickets/<id>/ticket.json, under deliveries.
| What you see | Why |
|---|---|
The panel shows a BR-… reference and no Open ticket button | The ticket system didn't answer within send_now_timeout, or refused the report. The delivery's error says why, and the queue retries it. |
401 from the ticket system | The app and the ticket system use different auth servers, or the app's client ID/secret are wrong. |
Deliveries stay empty and the jobs table fills up | No queue worker is running. |
| The ticket has no reporter | The user isn't in the ticket system yet (its users are synced from the auth server), or the app's auth server isn't the ticket system's. |
413 or the screenshot is missing | The web server's upload limit (nginx client_max_body_size, PHP post_max_size) is smaller than the report. Screenshots can be a few MB. |
Developing the package
To try changes in an app before pushing them, point the app at your local checkout:
composer config repositories.laravel-bug-report '{"type": "path", "url": "../laravel-bug-report", "options": {"symlink": true}}'
composer require "boneybone/laravel-bug-report:*@dev"
Switch back to the vcs repository and ^1.0 before committing the app.
Testing
composer test
GitLab CI (.gitlab-ci.yml) runs the suite on every supported Laravel version: 9 on PHP 8.0, 10 on 8.1, 11 and 12 on 8.2, and 13 on 8.3 and 8.5.
Releasing
- Merge into
mainand add the release toCHANGELOG.md. - Tag it and push the tag:
git tag -a v1.1.0 -m "v1.1.0" && git push origin v1.1.0. - Apps get it with
composer update boneybone/laravel-bug-report.