PHP
BugWatch for PHP: exceptions, messages and structured logs from any PHP 8.2+ application, with deep Laravel integration and adapters for Monolog, PSR-3 and native error handling.
Requirementslink
- PHP 8.2+; hard deps are
psr/log ^3,psr/http-client ^1,psr/http-factory ^1,psr/http-message ^1|^2 - Default transport is cURL with a PHP-streams fallback; a PSR-18 client is used only if you inject one via
httpClient - Laravel 11 to 13 via the bundled auto-discovered service provider; Monolog 2/3 optional
1 - Installlink
composer require newinstance/bugwatch-php2 - Initialiselink
Plain PHP:
use NewInstance\BugWatch\BugWatch;
BugWatch::init([
'projectKey' => getenv('BUGWATCH_KEY'),
'release' => getenv('APP_VERSION'),
]);Laravel: set the env keys and you are done; publish the config to tune more:
BUGWATCH_KEY=sk_live_KEYID:secret
BUGWATCH_RELEASE=2.4.1
BUGWATCH_ENABLED=true
BUGWATCH_CAPTURE_EXCEPTIONS=true
php artisan vendor:publish --tag=bugwatch-configMulti-tenant or multi-project setups construct isolated clients with createClient([...]) instead of the global singleton.
3 - Configurationlink
| Option | Default | What it does |
|---|---|---|
projectKey | - | Server credential (KEYID:secret). Environment is bound to the key server-side: one key per environment, no environment option in this SDK |
release | - | Version label on every event |
enabled | true | Master switch. A disabled client returns '' from capture calls |
endpoint / sessionUrl | prod API / - | Must be valid http(s) URLs or ConfigException is thrown |
sampleRate | 1.0 | Range-validated 0.0 to 1.0; sampled-out events still return a stable event id |
sensitiveFields | [] | Your keys merge with the built-in redaction list |
batchSize | 50 | Validated 1 to 5000; the queue flushes automatically when it fills |
maxQueueSize | 1000 | Queue cap |
requestTimeout | 15000 ms | Per-request timeout |
retry | 3 attempts, 200 ms to 5 s, x2 | Exponential backoff |
beforeSend / httpClient / debug | - / - / false | Event filter, custom PSR-18 transport, diagnostics |
Flushing happens when the queue reaches batchSize, on explicit flush(), and at shutdown. Under PHP-FPM the shutdown flush runs after fastcgi_finish_request(), so delivery adds no response latency.
Laravel maps only key, endpoint, release, enabled, sample rate and sensitive fields from config/bugwatch.php; the remaining options require createClient or plain init().
4 - Capture APIlink
$id = BugWatch::captureException($e, ['tags' => ['route' => $routeName]]);
BugWatch::captureMessage('Sync finished', 'info');
BugWatch::captureLog(['level' => 'warn', 'message' => 'Slow query', 'tags' => ['db' => 'orders']]);
BugWatch::setUser(['id' => 'u_123', 'email' => 'ada@example.com']);
BugWatch::setTags(['tenant' => 't_42']);
BugWatch::setContext('payment', ['provider' => 'paystack']);
BugWatch::setRelease('checkout@2.4.1');
BugWatch::setFingerprint(['checkout', 'GATEWAY_TIMEOUT']);
BugWatch::withScope(function ($scope) use ($e) { $scope->setTag('job', 'sync'); BugWatch::captureException($e); });
BugWatch::resetScope();
BugWatch::flush();
BugWatch::close();- Levels accept BugWatch numerics, PSR-3 names and Monolog ints (notice maps to 30; critical, alert and emergency map to 60).
- Only
id,email,username,ipsurvive onuser; other keys are dropped before send. - Chained
getPrevious()exceptions serialise into acausesarray. - Capturing the same Throwable instance twice returns the first event id (in-process WeakMap dedupe).
client()exposes the underlying client;diagnostics()returns delivery counters.
5 - Laravel integrationlink
- Exceptions: reported automatically; disable with
BUGWATCH_CAPTURE_EXCEPTIONS=false. Log::*to BugWatch: add a channel with'driver' => 'bugwatch'inconfig/logging.php(standalone or inside a stack); without it, Laravel logs never reach BugWatch.- Per-request users: register
BugWatchContextMiddleware; it scopes the authenticated user (id only by default), setsmethod,urlandroutetags, and resets on termination. Customise identity withBugWatchContextMiddleware::resolveUserUsing(fn ($request) => [...])from a providerboot()(closures registered elsewhere breakconfig:cache); a throwing resolver never affects the request. - Long-lived runtimes: Octane's
RequestTerminatedplus queueJobProcessed/JobFailedflush and reset the scope automatically. ArtisanCommandFinishedonly flushes, so looping commands callresetScope()themselves. RoadRunner has no automatic hook: use the middleware or manualflush()+resetScope()per request. Withext-swooleloaded, scope is stored per coroutine automatically. - Browser session mint:
BrowserSessionControlleris a ready-made route for the browser token flow; non-Laravel backends callmintBrowserSession(['projectKey' => ...])which returns['token', 'expiresAt'](see Browser Ingest).
6 - Logging adapterslink
$log->pushHandler(new \NewInstance\BugWatch\Integration\Monolog\Handler(BugWatch::client(), 'warning'));- One Monolog handler class serves Monolog 2 arrays and Monolog 3
LogRecord; level defaults todebug, the third argument is$bubble; channel plus scalar context and extra become tags; it never throws into Monolog. getLogger()returns a PSR-3 logger with{placeholder}interpolation; a Throwable in context becomes a captured exception.ErrorHandler::install($client, ['exceptions' => true, 'errors' => true, 'shutdown' => true])chains existing handlers, respectserror_reporting()and@, is recursion-guarded, and returns a handle withuninstall().
Testinglink
$client = new Client($config, new InMemoryTransport());Assert on $transport->events; set $transport->result = false to simulate delivery failure.
Troubleshootinglink
- Silent with a missing key: Laravel self-disables the client; plain
init()swallows theConfigExceptionand writes[BugWatch] disabled: ...toerror_logunlessdebug => true. - Nothing arrives from a worker loop: call
flush()andresetScope()at each unit-of-work boundary, otherwise the previous job's identity leaks into the next and events sit queued. - Laravel reports twice: remove manual
captureExceptioncalls from your own handler once automatic capture is on.
Wire calls this SDK makes: POST /api/v1/bugwatch/ingest with Content-Type: application/x-ndjson and x-api-key, plus POST /api/v1/bugwatch/browser-session for the browser flow.