Looking to hire Laravel developers? Try LaraJobs
This package is not available.

laravel-bug-report maintained by boneybone

Description
Floating "report a problem" button for Laravel apps that attaches console logs, XHR/fetch calls, a screenshot and server exceptions to each ticket.
Last update
2026/10/21 07:45 (dev-main)
License
Links
Downloads
1

Comments
comments powered by Disqus

boneybone/laravel-bug-report

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

  1. The app has boneybone/auth-server-sdk installed and configured (AUTH_SERVER_CLIENT_ID, AUTH_SERVER_CLIENT_SECRET, AUTH_SERVER_ENDPOINT), pointing at the same auth server as the ticket system.

  2. Add one line to the app's .env:

    BUG_REPORT_API_URL=https://<ticket-system-domain>/api/bug-reports
    
  3. Run 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 the jobs table. On the sync queue a delivery is tried once after the response. Under php-fpm the user doesn't wait for it, but under php artisan serve they do. Don't point a destination back at the same single-threaded artisan serve instance, 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=..." }
}
  • text makes 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:
    • position and offset_x/offset_y
    • draggable: users can drag the button, the position is remembered per browser, and the panel has a "Reset button position" link
    • label, 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 limit ignore_urls (full-URL patterns with *; Laravel Boost's */_boost/* is ignored by default) and ignore_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 to limit, kept for retention_days) with the ticket system's number, whether it was sent, and an Open ticket link. Set enabled to false to hide it.
  • user_fields: the user's fields sent with each report (id, name, email). They're read from the SDK's BoneyBoneUser too, and a user with only first_name/last_name gets 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_limit characters. 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

  1. Merge into main and add the release to CHANGELOG.md.
  2. Tag it and push the tag: git tag -a v1.1.0 -m "v1.1.0" && git push origin v1.1.0.
  3. Apps get it with composer update boneybone/laravel-bug-report.