Reporting exceptions

QuietGuard\Monitor\Reporter is the platform-neutral client. Adapters wire it to a host's exception and logging hooks; you can also drive it by hand.

Wiring a Reporter

The Reporter constructor takes a Config, an Http\HttpClient, a Support\Scrubber, a Payload\ExceptionPayloadBuilder, and an optional PSR LoggerInterface:

php
use QuietGuard\Monitor\Config;
use QuietGuard\Monitor\Reporter;
use QuietGuard\Monitor\Http\CurlHttpClient;
use QuietGuard\Monitor\Support\Scrubber;
use QuietGuard\Monitor\Payload\ExceptionPayloadBuilder;

$config = new Config(
    url: 'https://monitor.example.com',
    key: 'project-token',
    release: 'a1b2c3d',
);

$reporter = new Reporter(
    config: $config,
    http: new CurlHttpClient(),
    scrubber: new Scrubber(['password', 'authorization', 'api_key']),
    builder: new ExceptionPayloadBuilder(
        traceLimit: $config->traceLimit,
        release: $config->release,
    ),
    logger: null, // optional Psr\Log\LoggerInterface
);

Sending data

php
try {
    // your code
} catch (\Throwable $e) {
    $reporter->reportException($e, ['user_id' => 42]);
}

The Reporter exposes three send methods, each returning bool and never throwing, monitoring must not break the host application:

MethodEndpointNotes
reportException(Throwable $e, array $context = [])POST /api/v1/ingestBuilds the payload, scrubs the context, sends it.
sendLogs(array $logs)POST /api/v1/logsWraps logs as {"logs": [...]} and scrubs them. Returns true immediately when $logs is empty.
sendDependencies(array $packages)POST /api/v1/dependenciesWraps as {"packages": [...]}. Returns false immediately when $packages is empty; dependencies are not scrubbed.

A send returns true only on an HTTP 2xx. If the client is not configured (Config::isConfigured() is false) the methods return false without touching the network. Any transport exception is caught, logged through the optional logger as a warning, and turned into false.

The exception payload

Payload\ExceptionPayloadBuilder::build() turns a Throwable into the wire shape:

php
[
    'exception' => [
        'class'   => $e::class,
        'message' => $e->getMessage(),
        'file'    => $e->getFile(),
        'line'    => $e->getLine(),
        'trace'   => [ // up to traceLimit frames
            ['class' => ..., 'type' => ..., 'function' => ..., 'file' => ..., 'line' => ...],
        ],
    ],
    'context' => [ /* release (if set) merged with your context */ ],
]

The trace is sliced to traceLimit frames, each reduced to class, type, function, file, line. When release is non-null it is merged into context (your explicit context keys win on collision). The exact wire contract is described by the ingestion API (/docs/tool/1.0/ingestion-api).

The transport abstraction

Http\HttpClient is a deliberately minimal contract:

php
interface HttpClient
{
    // returns the HTTP status code, or 0 on transport failure; must not throw
    public function postJson(string $url, string $token, array $payload, int $timeout): int;
}

The bundled Http\CurlHttpClient implements it with ext-curl and no other dependency: it JSON-encodes the body and sends Content-Type: application/json, Accept: application/json and Authorization: Bearer <token>. It returns 0 (rather than throwing) when JSON encoding or curl_init() fails.

Bringing your own client

Because HttpClient is a plain interface, not PSR-18 itself, you can wrap any client (a PSR-18 client, Guzzle, the framework HTTP client) behind it. Implementations must not throw and must return the status code (or 0):

php
use QuietGuard\Monitor\Http\HttpClient;
use Psr\Http\Client\ClientInterface;      // PSR-18
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;

final class Psr18HttpClient implements HttpClient
{
    public function __construct(
        private ClientInterface $client,
        private RequestFactoryInterface $requests,
        private StreamFactoryInterface $streams,
    ) {}

    public function postJson(string $url, string $token, array $payload, int $timeout): int
    {
        try {
            $request = $this->requests->createRequest('POST', $url)
                ->withHeader('Content-Type', 'application/json')
                ->withHeader('Accept', 'application/json')
                ->withHeader('Authorization', 'Bearer '.$token)
                ->withBody($this->streams->createStream((string) json_encode($payload)));

            return $this->client->sendRequest($request)->getStatusCode();
        } catch (\Throwable) {
            return 0;
        }
    }
}

Pass your implementation as the http argument to the Reporter. To capture errors automatically in a plain-PHP app, see the global error handler; to keep secrets out of payloads, see scrubbing.

You are reading the PHP Core v1.0 documentation.