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-php

2 - 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-config

Multi-tenant or multi-project setups construct isolated clients with createClient([...]) instead of the global singleton.

3 - Configurationlink

OptionDefaultWhat 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
enabledtrueMaster switch. A disabled client returns '' from capture calls
endpoint / sessionUrlprod API / -Must be valid http(s) URLs or ConfigException is thrown
sampleRate1.0Range-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
batchSize50Validated 1 to 5000; the queue flushes automatically when it fills
maxQueueSize1000Queue cap
requestTimeout15000 msPer-request timeout
retry3 attempts, 200 ms to 5 s, x2Exponential backoff
beforeSend / httpClient / debug- / - / falseEvent 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, ip survive on user; other keys are dropped before send.
  • Chained getPrevious() exceptions serialise into a causes array.
  • 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' in config/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), sets method, url and route tags, and resets on termination. Customise identity with BugWatchContextMiddleware::resolveUserUsing(fn ($request) => [...]) from a provider boot() (closures registered elsewhere break config:cache); a throwing resolver never affects the request.
  • Long-lived runtimes: Octane's RequestTerminated plus queue JobProcessed/JobFailed flush and reset the scope automatically. Artisan CommandFinished only flushes, so looping commands call resetScope() themselves. RoadRunner has no automatic hook: use the middleware or manual flush() + resetScope() per request. With ext-swoole loaded, scope is stored per coroutine automatically.
  • Browser session mint: BrowserSessionController is a ready-made route for the browser token flow; non-Laravel backends call mintBrowserSession(['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 to debug, 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, respects error_reporting() and @, is recursion-guarded, and returns a handle with uninstall().

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 the ConfigException and writes [BugWatch] disabled: ... to error_log unless debug => true.
  • Nothing arrives from a worker loop: call flush() and resetScope() 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 captureException calls 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.