Skip to main content

Installation

Requirements

  • PHP 8.4+, with the intl, fileinfo, filter, hash, json and mbstring extensions
  • 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

TagWhat it writes
visual-feedbackthe umbrella: config, translations, views and the capture bundle
visual-feedback-configconfig/visual-feedback.php only
visual-feedback-langthe seven bundled locales, for editing
visual-feedback-viewsthe plain Blade view tree, for restyling
visual-feedback-assetsthe compiled capture bundle into public/vendor/visual-feedback
visual-feedback-wirekitthe WireKit view tree over the plain one (see below)
visual-feedback-migrationsthe 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.

StateMeaning
currentthe published copy matches the installed version
staleit is from an earlier release — re-run the publish with --force
not-publishedthere is no copy in public/ yet — run the publish once
served-externallyui.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.