Configuration
Every option is documented inline in config/visual-feedback.php. The blocks:
| Key | What it governs |
|---|---|
enabled | master switch |
categories | the category list offered to the reporter. The first one is the default — the picker shows it and an untouched report carries it, so order the list to choose that default |
fields | which fields are shown (subject, phone, …) |
guests | what a guest is required to give. Name and email are always offered to an unauthenticated reporter; these two switches make them mandatory. There is no key here that turns guest reporting off — a widget on a public page accepts a guest report, and refusing one is your page's decision about where the widget goes |
attachments | disk, directory, counts, size caps, accepted types, image bomb guards |
screenshot | the capture cascade, scale, caps, redaction attribute |
metadata | which browser metadata is collected |
abuse | the abuse driver, rate limits, the time trap and the challenge view |
privacy | the privacy notice guests acknowledge |
channels | which delivery channels run, and their queue tuning |
mail, webhook, database | per-channel settings |
retention | how long reports and orphaned files are kept — only once you schedule the two commands yourself, see Privacy and retention |
ui | trigger surface, floating button position, asset base URL |
context_providers | your own classes that attach application context to a report — they implement ReportContextProvider, and authorizing what they expose is your duty, see Delivery channels |
Environment variables
Everything below has a working default, so an install needs none of them. They exist for the settings that differ per environment — where reports go, which disk holds attachments, whether the capture asks for permission.
| Variable | Default | Effect |
|---|---|---|
VISUAL_FEEDBACK_ENABLED | true | master switch — see Turning it off |
VISUAL_FEEDBACK_UI_VARIANT | auto | which view tree renders: auto (WireKit when it is installed, plain otherwise), plain or wirekit |
VISUAL_FEEDBACK_UI_TRIGGER | fab | fab, inline or none (you place the trigger) |
VISUAL_FEEDBACK_UI_POSITION | bottom-right | which corner the floating button sits in |
VISUAL_FEEDBACK_UI_ASSETS | published path | base URL of the capture bundle, if you serve it yourself — see the note below |
VISUAL_FEEDBACK_FIELD_SUBJECT | true | show the subject field |
VISUAL_FEEDBACK_FIELD_PHONE | false | show the phone field |
VISUAL_FEEDBACK_GUEST_REQUIRE_NAME | false | a guest must give a name |
VISUAL_FEEDBACK_GUEST_REQUIRE_EMAIL | false | a guest must give an email |
VISUAL_FEEDBACK_ATTACHMENTS_DISK | local | the disk attachments are stored on — keep it private |
VISUAL_FEEDBACK_ATTACHMENTS_DIR | visual-feedback | directory on that disk |
VISUAL_FEEDBACK_SCREENSHOT_STRATEGY | auto | auto, native, dom or off — see the capture cascade |
VISUAL_FEEDBACK_SCREENSHOT_SCALE | 2 | render scale, or device for the device pixel ratio |
VISUAL_FEEDBACK_SCREENSHOT_DARK_FALLBACK | #111827 | background for a page that declares none and is dark |
VISUAL_FEEDBACK_SCREENSHOT_DEBUG | false | the renderer's own logging — off in production |
VISUAL_FEEDBACK_ABUSE_DRIVER | builtin | which abuse gate runs |
VISUAL_FEEDBACK_PRIVACY_SOURCE | none | url, legal-consent, or a class name |
VISUAL_FEEDBACK_PRIVACY_URL | none | your policy page — required for url and for legal-consent; a class-name source supplies its own |
VISUAL_FEEDBACK_PRIVACY_DOCUMENT_KEY | privacy | which legal-consent document the wording is read from |
VISUAL_FEEDBACK_RETENTION_DAYS | none | how long a stored report is kept before visual-feedback:prune removes it. The package schedules nothing, so this deletes nothing until you schedule that command — see Privacy and retention |
VISUAL_FEEDBACK_CHANNEL_MAIL | true | enable the mail channel |
VISUAL_FEEDBACK_CHANNEL_DATABASE | false | enable the database channel |
VISUAL_FEEDBACK_CHANNEL_WEBHOOK | false | enable the webhook channel |
VISUAL_FEEDBACK_MAIL_TO | none | where reports are mailed — without it the mail channel stays idle |
VISUAL_FEEDBACK_MAIL_FROM_ADDRESS | app default | sender address |
VISUAL_FEEDBACK_MAIL_FROM_NAME | app default | sender name |
VISUAL_FEEDBACK_MAIL_LOCALE | app locale | a fixed locale, or reporter for theirs |
VISUAL_FEEDBACK_MAIL_CONNECTION | default connection | queue connection for the mail job — the worker that carries it, not a database connection |
VISUAL_FEEDBACK_MAIL_QUEUE | default queue | queue for the mail job |
VISUAL_FEEDBACK_DATABASE_CONNECTION | default connection | queue connection for the database job |
VISUAL_FEEDBACK_DATABASE_QUEUE | default queue | queue for the database job |
VISUAL_FEEDBACK_WEBHOOK_URL | none | endpoint for the webhook channel |
VISUAL_FEEDBACK_WEBHOOK_SECRET | none | HMAC key the payload is signed with — required, or the channel is skipped |
VISUAL_FEEDBACK_WEBHOOK_INCLUDE_REPORTER | true | include reporter details in the payload |
VISUAL_FEEDBACK_WEBHOOK_CONNECTION | default connection | queue connection for the webhook job |
VISUAL_FEEDBACK_WEBHOOK_QUEUE | default queue | queue for the webhook job |
Settings that live only in the config file
Most of what you will change has an environment variable. A handful does not, either because it is a value rather than a switch or because it is fine-tuning nobody reaches for twice. They are listed here so that "not in the table above" never has to mean "does not exist".
| Setting | Default | What it does |
|---|---|---|
screenshot.dark_fallback_color | #111827 | the color the DOM stage paints behind a page whose background is transparent while color-scheme is dark. Any background your page actually sets wins through the normal cascade; this is only the last resort |
screenshot.flatten_custom_elements | true | flatten custom elements before capture |
screenshot.iframe_placeholder | true | draw a same-size placeholder naming the host for a cross-origin iframe |
metadata.max_value_length | 2000 | the cap the metadata sanitizer applies to one collected value |
metadata.user_agent_max | 512 | the cap for the user-agent string specifically, which is the one that gets long |
Both screenshot booleans are real switches and do exactly what the table says: the
configuration is handed to the capture bundle, which skips the corresponding pass whenever one
of them is false. Open <dialog> elements are hidden unconditionally and need no key.
An <img src="*.svg"> is not fetched and inlined. It goes through the renderer's ordinary
image path, so a cross-origin SVG served without CORS headers is missing from the capture like
any other cross-origin image, and one served from your own origin is captured like any other
image. That is worth stating because an inlining pass is the thing people assume, and assuming
it hides a CORS problem behind a file format.
The attachments caps and the mail and webhook switches also have no environment variable,
and they are documented where the decision gets made rather than here — see
Integration contract and
Delivery channels.
Serving the bundle yourself
VISUAL_FEEDBACK_UI_ASSETS takes a base URL, without a query string. The rendered <script>
tag appends a cache-busting ?id= token of its own, derived from the installed package version,
so the URL in your page source is not byte-identical to the one you configured. That is expected
and nothing to correct — a Content-Security-Policy script-src source expression matches on the
path and ignores the query, so nothing needs changing there either.
The token solves one of the two ways a bundle goes wrong, and it is worth knowing which. It
handles republished but cached: after vendor:publish --force replaces the bytes on disk, the
URL changes, so a browser or CDN holding the old file under a long max-age fetches the new one.
It does not handle forgot to republish. If the copy under public/vendor/visual-feedback/
is older than the one in vendor/, the token busts the cache and loads the same stale file again.
Republishing after every upgrade of this package is still the only thing that prevents it — see
Installation.