Collecting user feedback in Laravel with Wirebug

php dev.to

Add a feedback widget to an existing Laravel app and end up with a queryable inbox, with automatic technical context and no design work.

The goal for this post is small. Adding a feedback widget to your Laravel app, having a queryable inbox of reports sitting in your own database. No design work, no third-party service, no schema you have to write yourself.

The package doing the work is Wirebug. It drops a widget into your app that lets users report a bug, a suggestion or anything else, attaches an optional screenshot or screen recording, and collects the technical context automatically. Every report is a plain Eloquent row you can read however you like.

How to install

Wirebug is a Composer package, and it registers its own migration, so there is nothing to publish before you can run it.

composer require edulazaro/wirebug
php artisan migrate
Enter fullscreen mode Exit fullscreen mode

It needs PHP 8.2+, Laravel 11+ and Alpine.js on the page. You also import the CSS in your bundle, wiremodal first and wirebug second, since wirebug builds on wiremodal's styles.

@import '../../vendor/edulazaro/wiremodal/resources/css/wiremodal.css';
@import '../../vendor/edulazaro/wirebug/resources/css/wirebug.css';
Enter fullscreen mode Exit fullscreen mode

If your widget only lives behind a login, publish the config and lock the endpoint down. The submit route ships with web and a throttle by default, and you add auth yourself.

php artisan vendor:publish --tag=wirebug-config
Enter fullscreen mode Exit fullscreen mode
// config/wirebug.php
'route' => [
    'enabled'    => true,
    'path'       => 'wirebug',
    'middleware' => ['web', 'auth', 'throttle:10,1'],
],
Enter fullscreen mode Exit fullscreen mode

How to use it

The integration is one Blade component. Put it in a layout and pick the shared theme once on the root element.

<html data-wire-theme="studio">
    ...
    <x-wirebug />
Enter fullscreen mode Exit fullscreen mode

Because attachments matter for this angle, the first thing worth configuring is where they go. Screenshots and recordings are written through Laravel's Storage, and the report row keeps only the path, so pointing them at S3 or an S3-compatible R2 disk instead of the local filesystem is a config change and nothing else.

// config/wirebug.php
'uploads' => [
    'disk'   => 's3',       // any disk from config/filesystems.php
    'path'   => 'wirebug',
    'max_kb' => 5120,       // 5 MB screenshot cap
],
Enter fullscreen mode Exit fullscreen mode

The screenshot accepts jpg, png, gif and webp. SVG is rejected on purpose, since an uploaded SVG is an XSS vector and there is no reason to accept one for a bug report.

Reading the reports

This is the part the whole post is building toward. A report is an EduLazaro\WireBug\Models\BugReport, and the model ships the scopes you need to build a triage screen with no extra code. A fresh report has a status of new, so an inbox of things you have not looked at yet is one line.

use EduLazaro\WireBug\Models\BugReport;

$inbox = BugReport::new()->latest()->get();
Enter fullscreen mode Exit fullscreen mode

Filtering by type

The selector offers a few report types, and you filter by them with ofType. There is one trap to know about. The type keys are bug, idea and other, and the middle one is labelled "Suggestion" in the UI, so the query for suggestions is ofType('idea'), not ofType('suggestion').

$bugs        = BugReport::ofType('bug')->get();
$suggestions = BugReport::ofType('idea')->get();   // not 'suggestion'
Enter fullscreen mode Exit fullscreen mode

Who sent it

Each report has a polymorphic reporter relation. It resolves through your app's morph map, so it hands you back whatever model actually submitted the report, a User, a Client, or whatever you have mapped. For a guest who was not logged in, it is simply null.

foreach ($inbox as $report) {
    $who = $report->reporter?->name ?? 'Guest';
}
Enter fullscreen mode Exit fullscreen mode

Reaching a guest who left no account

Because a guest has no reporter to attach, Wirebug shows non-authenticated visitors an optional email field so you can still reply. Authenticated users never see it, since you already know who they are. That field is on by the ask_guest_email config flag, and the value lands on the report row for you to read next to the null reporter.

What gets captured automatically

Every report carries technical context that the user never typed. Wirebug records the current URL, the user agent, the viewport, the app locale and the referer, which together are usually enough to reproduce a problem without a single follow-up question. The whole block is behind the capture_context config flag, so if you would rather not store any of it you flip one boolean.

The recording and wire:navigate

Wirebug can capture a screen recording using the browser's native getDisplayMedia and MediaRecorder, capped by config at ninety seconds, fifty megabytes and a two megabit bitrate. Nothing records until the user clicks, and the record button only shows up on desktop browsers that support the API.

The one operational detail to know is that the recording lives in the page's JavaScript context. It survives wire:navigate navigation, so a user can keep recording as Livewire swaps pages, but a hard reload throws the context away and ends the recording. That is a browser limit, not something to work around.

When a submit succeeds, Wirebug dispatches a wirebug-sent event on window, which is a convenient place to fire a toast or a tracking call. Add the component, point the disk where you want it, and you have a feedback inbox you can query with Eloquent.

👉 Package on Packagist: https://packagist.org/packages/edulazaro/wirebug
👉 Source on GitHub: https://github.com/edulazaro/wirebug

Source: dev.to

arrow_back Back to Tutorials