phattarachai/watchtower-laravel
| Install | |
|---|---|
composer require phattarachai/watchtower-laravel |
|
| Latest Version: | v1.3.0 |
| PHP: | ^8.4 |
| License: | MIT |
| Last Updated: | Sep 30, 2026 |
| Links: | GitHub · Packagist |
Watchtower Laravel
Laravel client for Watchtower, a self-hosted Sentry-compatible exception tracker. Installs and configures sentry/sentry-laravel, wires Integration::handles($exceptions) into bootstrap/app.php, and exposes a same-origin browser tunnel (/api/watchtower-relay) that proxies envelopes to your Watchtower instance — dodging ad-blockers that strip ?sentry_key= query strings.
Install
composer require phattarachai/watchtower-laravel
php artisan watchtower:install --dsn=http://your-public-key@your-watchtower-host/42
The install command:
- Validates and writes
WATCHTOWER_DSNandSENTRY_LARAVEL_DSNto.env. - Patches
bootstrap/app.phpto callSentry\Laravel\Integration::handles($exceptions)insidewithExceptions(...). - Publishes
config/watchtower.php. - If
vite.config.{js,ts}is present, writesVITE_SENTRY_DSN,VITE_SENTRY_TUNNEL, andVITE_SENTRY_ENVIRONMENTand prints a Vite entry snippet. - If the
claude(Claude Code) CLI is on PATH, registers the Watchtower MCP server so Claude can query and triage issues directly. Pass--no-mcpto skip.
Re-running is idempotent. Pass --dry-run to preview changes.
Standalone mode
Standalone drops the central Watchtower server entirely: this app owns the tables, the ingest endpoint, the issue UI and an MCP server of its own.
php artisan watchtower:install --standalone
On top of the steps above it:
- Writes
WATCHTOWER_MODE=standaloneand runsphp artisan migrate --forceto create thewatchtower_*tables. - Creates the first project (named after
APP_NAME) and pointsSENTRY_LARAVEL_DSNat its DSN. - Publishes
resources/js/pages/Watchtower.jsx, adds the@watchtowerVite alias, and appends@import 'tailwindcss' prefix(tw);plus the package@sourceline toresources/css/app.css. - Prints the authorization snippet and registers the embedded MCP server at
/watchtower/mcpwith Claude Code.
--mode=dual does all of that and keeps forwarding browser envelopes to a central Watchtower as well, so it still
prompts for the upstream DSN.
The two build-tool edits the package cannot make for you if your project deviates from the default layout:
// vite.config.js
resolve: {
alias: {
'@watchtower': './vendor/phattarachai/watchtower-laravel/resources/js/watchtower',
},
},
/* resources/css/app.css */
@import 'tailwindcss' prefix(tw);
@source '../../vendor/phattarachai/watchtower-laravel/resources/js/watchtower/**/*.jsx';
Only the page stub lives in your tree — the module itself is reached through the alias, so there is no second copy to drift. Authorize the UI from a service provider:
Watchtower::auth(fn ($request): bool => $request->user()?->isAdmin() === true);
Then verify everything with php artisan watchtower:doctor, which checks the tables, the routes, both build-tool
edits, the mailer, the queue, the MCP registration and the self-capture path, and names whatever is missing.
Embedded commands
| Command | Purpose |
|---|---|
watchtower:doctor |
Report every host-app requirement, green or red. |
watchtower:project list |
Projects with masked keys and full DSNs. |
watchtower:project create "Name" |
New project; prints its DSN. --platform= to override laravel. |
watchtower:project rotate-key {id|slug} |
Issue a fresh public key. |
watchtower:project activate/deactivate |
Stop or resume accepting events for one project. |
watchtower:prune |
Drop events past retention (scheduled daily on its own). |
Self-capture
By default this app's own exceptions never leave the process: the Sentry SDK's HTTP transport is swapped for an
in-process one that hands the serialized envelope straight to the ingest pipeline, before_send scrubbing intact.
Set WATCHTOWER_SELF_CAPTURE=loopback to keep the SDK's HTTP transport (events travel over the network back into
this app's ingest route), or false to disable self-capture entirely.
Watchtower's own queue failures are never self-captured: anything a worker reports while running or failing a
ProcessEventJob / ForwardEnvelope is dropped, so a failing job cannot re-queue itself as a new event.
Production
Every ingest path — HTTP, the relay and self-capture — shares a per-project budget (WATCHTOWER_RATE_LIMIT_PER_MIN)
and a per-issue one (WATCHTOWER_RATE_LIMIT_PER_FINGERPRINT_PER_MIN). Events over budget are counted on their issue
but not queued, and nothing is queued past WATCHTOWER_MAX_QUEUE_DEPTH waiting jobs. Each event is scrubbed (SQL row
values included) and trimmed to WATCHTOWER_MAX_EVENT_BYTES (200 KB) before it is queued, by the pipeline in
phattarachai/watchtower-core that the central server shares.
On Redis + Horizon, run events on a dedicated queue. Add a supervisor first, then point Watchtower at it:
// config/horizon.php → 'defaults' (and list it under each environment)
'app-supervisor-watchtower' => [
'connection' => 'redis',
'queue' => ['watchtower'],
'maxProcesses' => 1,
'tries' => 3,
'timeout' => 60,
],
WATCHTOWER_QUEUE_NAME=watchtower
watchtower:doctor fails if no Horizon supervisor consumes that queue, and warns about an uncapped or evicting Redis. Cap Redis too (maxmemory 1gb,
maxmemory-policy noeviction) so a runaway fails writes instead of getting Redis OOM-killed. The bundled skill's
reference.md has the full Horizon block and a triage runbook for a queue that is already flooded.
MCP
Install laravel/mcp and the server mounts at /{prefix}/mcp, authenticated with any active project's public key —
Authorization: Bearer {public_key} or ?api_key=. Every tool is scoped to that project. The tools mirror the
central server: list_issues, get_issue, list_events, get_event, get_stats, resolve_issue, ignore_issue,
unresolve_issue, snooze_issue.
Configuration
| Env key | Default | Purpose |
|---|---|---|
WATCHTOWER_DSN |
falls back to SENTRY_LARAVEL_DSN |
Watchtower DSN: http://{key}@{host}/{numeric-project-id}. |
WATCHTOWER_MODE |
relay |
relay, standalone or dual. |
WATCHTOWER_PATH |
watchtower |
URL prefix for the embedded ingest, UI and MCP endpoints. |
WATCHTOWER_DB_CONNECTION |
(default connection) | Connection the watchtower_* tables live on. |
WATCHTOWER_RETENTION_DAYS |
90 |
Event retention; a project row may override it. |
WATCHTOWER_SELF_CAPTURE |
transport |
transport, loopback or false. |
WATCHTOWER_MCP_ENABLED |
true |
Mount the embedded MCP server (needs laravel/mcp). |
WATCHTOWER_QUEUE_CONNECTION |
(default connection) | Queue connection for ProcessEventJob. |
WATCHTOWER_QUEUE_NAME |
(default queue) | Queue for ProcessEventJob. Recommended: watchtower. |
WATCHTOWER_RATE_LIMIT_PER_MIN |
300 |
Events per minute per project, on every ingest path. |
WATCHTOWER_RATE_LIMIT_PER_FINGERPRINT_PER_MIN |
20 |
Events per minute per issue; the rest are counted, not stored. |
WATCHTOWER_MAX_PAYLOAD_BYTES |
1048576 |
Largest envelope body accepted, as sent. |
WATCHTOWER_MAX_EVENT_BYTES |
200000 |
Events are trimmed to this JSON size before being queued. |
WATCHTOWER_MAX_STRING_BYTES |
8192 |
Cap on any single string in an event. |
WATCHTOWER_MAX_QUEUE_DEPTH |
5000 |
Past this many waiting jobs, events are counted, not queued. |
WATCHTOWER_REDACT_SQL_VALUES |
true |
Strip row values from SQL error messages and query breadcrumbs. |
WATCHTOWER_RELAY_ENABLED |
true |
Register the relay route on boot. |
WATCHTOWER_RELAY_PATH |
/api/watchtower-relay |
Relay endpoint path (must live under /api/). |
WATCHTOWER_RELAY_TIMEOUT |
5 |
Upstream request timeout (seconds). |
WATCHTOWER_RELAY_ASYNC |
false |
Forward envelopes through a queued job instead of sync. |
WATCHTOWER_RELAY_QUEUE |
(default queue) | Queue name when async is enabled. |
WATCHTOWER_VERIFY_SSL |
true |
Verify upstream TLS certificate. |
WATCHTOWER_CONNECT_TIMEOUT |
3 |
Guzzle connect timeout (seconds). |
WATCHTOWER_FORWARD_GZIP |
true |
gzip uncompressed envelopes (≥ 1 KB) before forwarding upstream. |
Browser side
The browser SDK posts envelopes to your own app at /api/watchtower-relay. The relay parses your configured DSN, forwards the request body verbatim to {scheme}://{host_with_port}/api/watchtower-relay on the Watchtower instance, and passes back the upstream status and rate-limit headers.
watchtower:install publishes a small helper to resources/js/vendor/watchtower.js (plus resources/js/vendor/livewire.js, the Livewire beforeSend rules it imports) that wraps Sentry.init(...) with the Watchtower-tuned defaults (same-origin tunnel, no PII, browser-extension denyUrls) and applies <meta name="watchtower-user-*"> to Sentry.setUser(...). Per Vite entry:
import { initWatchtower } from './vendor/watchtower.js';
initWatchtower();
The Sentry config lives inside the published helper, so multiple entries don't duplicate it. Customize options (e.g. ignoreErrors) there once.
In your root Blade layout's <head> add the package directive that emits the user-context meta tags:
@watchtowerUser
@watchtowerUser is registered automatically by the service provider and compiles to three <meta name="watchtower-user-{id,email,name}"> tags. Customize by publishing the view: php artisan vendor:publish --tag=watchtower-views.
For Filament admin panels (which bypass the root Blade layout), register a render hook:
$panel->renderHook(
PanelsRenderHook::HEAD_END,
fn (): string => Blade::render('@watchtowerUser'),
);
Because the request hits your own origin under /api/, ad-blockers don't recognize it as Sentry traffic.
Async forwarding
Set WATCHTOWER_RELAY_ASYNC=true to dispatch each forward through a ForwardEnvelope job. The relay returns 202 {"queued": true} immediately and the worker performs the upstream POST, reusing one HTTP connection across jobs. An unreachable upstream or a 5xx is retried twice with backoff; a 4xx (including 429) is dropped. After the last attempt the failure is logged and the job completes, so an outage never parks envelope bodies in failed_jobs.
Verify
php artisan watchtower:test
Prints the resolved config, runs sentry:test, and POSTs a synthetic envelope through the relay path.
Troubleshooting
The bundled skill at vendor/phattarachai/watchtower-laravel/resources/boost/skills/watchtower-error-tracking/reference.md covers every install + verify + triage path, including the MCP server. To install it into Claude's skill set: php artisan boost:install --skills.
License
MIT.
Related Packages
Lightweight error monitoring for Laravel — captures PHP exceptions with full sta...
Turn failed Laravel requests into encrypted, locally replayable timecodes and Pe...
Tasty intergration of Laravel & Sentry for sweet reporting of your logs
Sentry (Raven) error monitoring for Laravel and Lumen with send in background vi...