Installation
Requirements
- PHP 8.4+, with the
intl,fileinfo,filter,hash,jsonandmbstringextensions - Laravel 12+
- Livewire 4.3+
Composer refuses the install if an extension is missing and names it, so this list is a
heads-up rather than something to verify. intl is the one worth checking first — it is the
only one that is regularly absent from a stock PHP build.
Tested against SQLite, PostgreSQL, and MySQL 8.4, so it runs on Laravel Cloud (serverless Postgres and MySQL 8.4 LTS) out of the box.
Installation
composer require pushery/visual-feedback-for-laravel
The service provider is registered automatically through package discovery.
Publish the configuration, translations, views and the capture bundle:
php artisan vendor:publish --tag=visual-feedback
Then place the widget once in your layout, and the script tag before </body>:
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@include('visual-feedback::style')
@livewireStyles
</head>
<body>
<main>
<h1>Your application</h1>
{{ $slot }}
</main>
@livewire('visual-feedback.report-widget')
@livewireScripts
<x-visual-feedback::scripts />
</body>
</html>
That is the whole integration on the page: a floating button appears in the corner and opens the modal. One more setting decides whether a report goes anywhere.
Give the mail channel a recipient
The mail channel is on by default and ships without an address — nothing sensible could be guessed there. Until you set one, the channel reports itself unavailable and is skipped:
Set it before you send the first test report, because a report with nowhere to go still
shows the reporter a success screen. The only trace is two lines in the log —
an enabled channel reported itself unavailable and was skipped, then
a report was accepted but no channel was enabled and available to deliver it. That is by
design: a reporter is not the right person to tell about your configuration, and a form that
throws in their face teaches them not to report again. It does mean the first quiet install
looks exactly like a working one.
The other two channels are opt-in and have their own required settings — see Delivery channels.
Four things the package cannot do for you
None of these stops the widget from working, which is exactly why they are easy to miss.
1 · Bound Livewire's upload endpoint. Attachments and screenshots ride Livewire's global
upload endpoint, and a file is written to the temporary disk before this package runs — so
this package's caps bound what is accepted, never what is written. Livewire's untouched
defaults allow 12 MB per file at 60 calls per minute per IP, on a page that is usually
public. In config/livewire.php:
'temporary_file_upload' => [
'rules' => ['required', 'file', 'max:8192'],
'middleware' => 'throttle:20,1',
],
Size rules to the largest upload in the whole application — the key is app-global. The
Integration contract explains both numbers.
2 · Schedule the housekeeping commands. The package registers no schedule, on purpose: a
package does not get to write into your scheduler. Nothing is pruned or swept until you add
the entries yourself. In routes/console.php:
use Illuminate\Support\Facades\Schedule;
Schedule::command('visual-feedback:prune')->daily();
Schedule::command('visual-feedback:sweep-orphans')->daily();
Without them retention.reports_days deletes nothing and orphaned files accumulate. See
Privacy and retention.
3 · Keep the attachment disk private. attachments.disk defaults to local. Screenshots
routinely contain whatever the reporter had on screen, so a public disk here is a public URL
to somebody's session — and it looks identical to a correct setup from the inside.
4 · Run a queue worker. Every channel queues its own job, so delivery happens on your
queue and not in the request the reporter submitted. A fresh Laravel application resolves
queue.default to database unless QUEUE_CONNECTION says otherwise, which means the job
is written to the jobs table and waits there. With no worker consuming it, no report is
ever delivered — and, again, the reporter sees the success screen either way.
QUEUE_CONNECTION=sync runs every delivery inline instead, which is a reasonable choice for
a small application and a bad one for a public form: the reporter then waits for your mail
transport, and a slow provider becomes a slow submit.
Publish tags
| Tag | What it writes |
|---|---|
visual-feedback | the umbrella: config, translations, views and the capture bundle |
visual-feedback-config | config/visual-feedback.php only |
visual-feedback-lang | the seven bundled locales, for editing |
visual-feedback-views | the plain Blade view tree, for restyling |
visual-feedback-assets | the compiled capture bundle into public/vendor/visual-feedback |
visual-feedback-wirekit | the WireKit view tree over the plain one (see below) |
visual-feedback-migrations | the optional reports table, for the database channel |
Publish visual-feedback-assets again after every package update — the bundle in
public/ is a copy, and a stale copy is the one bug this setup can produce. You no longer
have to remember it unprompted: php artisan about carries a Published bundle line under
Visual Feedback with one of four states.
| State | Meaning |
|---|---|
current | the published copy matches the installed version |
stale | it is from an earlier release — re-run the publish with --force |
not-published | there is no copy in public/ yet — run the publish once |
served-externally | ui.assets points somewhere else, so there is nothing here to check |
With APP_DEBUG=true a stale copy also writes a warning to the log on any page that
renders the widget. The check reads the two files on the server, so it is right on the very
first request after an upgrade — anything shipped inside the bundle would be running from
the outdated copy itself.
The state is available in code too, if you would rather surface it in your own health
check: app(\Pushery\VisualFeedback\Support\PublishedBundle::class)->status() returns a
PublishedBundleStatus with exactly those four cases.
Upgrading later? Every release is described in the changelog, and anything that needs a change on your side carries an Upgrade note there. Problems and feature requests belong in the issue tracker — a security issue does not, and the security policy says where to send it instead.