Official BugBoard SDK for PHP — report errors as cards on your project board, with first-class Laravel and Symfony support.

v1.0.0 2026-07-15 06:52 UTC

This package is auto-updated.

Last update: 2026-07-16 18:06:56 UTC


README

CI Packagist Version License: MIT

The official BugBoard SDK for PHP. Report errors as cards on your project board — from plain PHP, Laravel, Symfony, or any framework with a PSR-18 HTTP client.

🚧 WORK IN PROGRESS 🚧

BugBoard.dev and its SDKs are currently under active development. 🚨 Please do not use this package for anything right now. 🚨 Once an official release is published, it will be available for everyone.

use BugBoard\Laravel\Facades\BugBoard;

try {
    $payments->charge($order);
} catch (\Throwable $e) {
    BugBoard::criticalHigh('Payment failed', $e, ['payment', 'backend']);
}

Reporting is fire-and-forget: the call buffers the report and returns immediately, delivery happens after your response is sent (with retries and backoff), and the SDK never throws into your app.

Requirements

  • PHP 8.2+
  • Any PSR-18 HTTP client plus PSR-17 factories. Guzzle is the recommendation (composer require guzzlehttp/guzzle); if you don't pick one, php-http/discovery finds whatever PSR-18 client your project already has.
  • ext-sodium only if you enable payload encryption (bundled with PHP by default)

Installation

composer require bugboard/sdk

Set your credentials in .env (or the environment). Servers use a secret key — a key id (bbk_…) plus a signing secret (bb_sec_…). Every request is HMAC-signed; the secret never travels on the wire:

BUGBOARD_KEY_ID=bbk_xxxxxxxx
BUGBOARD_SIGNING_SECRET=bb_sec_xxxxxxxx

Get keys from your BugBoard project under Settings → API Keys (create a Secret key).

Installing php-sodium (for payload encryption)

ext-sodium ships with PHP by default on most systems, but if you enable payload encryption (or php -m doesn't list sodium), install and configure it:

1. Install the extension

Match the package to the PHP version you actually run (php -v):

Debian / Ubuntu

sudo apt update
sudo apt install php8.3-sodium

RHEL / CentOS / Rocky / AlmaLinux / Fedora

sudo dnf install php-sodium

Alpine

apk add php83-sodium

macOS (Homebrew)

Homebrew's PHP already includes sodium, so there is usually nothing to do:

brew install php

Docker

On the official php images, libsodium's headers aren't in the base image, so install them and compile the extension:

RUN apt-get update \
    && apt-get install -y libsodium-dev \
    && docker-php-ext-install sodium

Windows

php_sodium.dll ships with the official builds. Nothing to install; go straight to step 3.

Building PHP from source

Configure with --with-sodium.

2. Enable it

The Debian/Ubuntu and RHEL packages normally enable the extension for you. If php -m still doesn't list it, load it explicitly.

On Debian / Ubuntu:

sudo phpenmod sodium

Anywhere else (and on Windows), add this line to php.ini — run php --ini to find which file is in use:

extension=sodium

3. Restart the process that serves your app

The extension is loaded at startup, so a running worker won't pick it up:

sudo systemctl restart php8.3-fpm   # PHP-FPM
sudo systemctl restart apache2      # mod_php

The gotcha: The CLI and FPM usually read different php.ini files. php -m tells you about the CLI only, so it can happily print sodium while your web requests still crash. Check the runtime your app actually uses:

php-fpm -m | grep sodium

— or hit a phpinfo() page and search for "sodium".

4. Verify a sealed box actually works

The real proof is a round trip, not just a loaded extension:

php -r '
$keypair = sodium_crypto_box_keypair();
$sealed = sodium_crypto_box_seal("hello", sodium_crypto_box_publickey($keypair));
echo sodium_crypto_box_seal_open($sealed, $keypair), PHP_EOL;
'

If it prints hello, sealed boxes work and the SDK can encrypt. If it fatals on an undefined function, the extension still isn't loaded in that runtime — go back to step 3.

Laravel

The package is auto-discovered — no manual registration. Configure via .env (above) and report from anywhere using the facade:

use BugBoard\Laravel\Facades\BugBoard;

BugBoard::major('Checkout is slow'); // a title is all you need
BugBoard::critical('Payment failed', $e); // attach the caught Throwable
BugBoard::critical('Payment failed', $e, ['payments', 'checkout']);

Or inject the shared client instead of using the facade:

use BugBoard\Client as BugBoardClient;

public function store(Request $request, BugBoardClient $bugboard)
{
    $bugboard->moderate('Slow image upload', null, 'uploads');
}

Buffered reports are delivered when the app terminates — after the response has gone out — so reporting never adds latency to a request. To customize defaults, publish the config:

php artisan vendor:publish --tag=bugboard-config

A good default for config/bugboard.php is already provided: every option it exposes is env-driven (BUGBOARD_ENABLED, BUGBOARD_SAMPLE_RATE, BUGBOARD_DEBUG, …) and cards are tagged with your APP_ENV out of the box.

beforeSend is the one option env can't express — it's a closure, so add it to the published config file directly:

// config/bugboard.php
'before_send' => function (array $payload): ?array {
    $payload['description'] = preg_replace('/\S+@\S+/', '[email]', $payload['description'] ?? '') ?: null;

    return $payload; // or return null to drop the report
},

Want every unhandled exception on your board? Add one line to your exception handler:

// bootstrap/app.php (Laravel 11+)
->withExceptions(function (Exceptions $exceptions) {
    $exceptions->report(function (\Throwable $e) {
        \BugBoard\Laravel\Facades\BugBoard::critical($e->getMessage() ?: $e::class, $e);
    });
})

Symfony

Enable the bundle and configure it:

// config/bundles.php
return [
    // …
    BugBoard\Symfony\BugBoardBundle::class => ['all' => true],
];
# config/packages/bugboard.yaml
bugboard:
  key_id: '%env(BUGBOARD_KEY_ID)%'
  signing_secret: '%env(BUGBOARD_SIGNING_SECRET)%'
  environment: '%kernel.environment%'

Then autowire the client anywhere:

use BugBoard\Client as BugBoardClient;

public function __construct(private readonly BugBoardClient $bugboard) {}

$this->bugboard->major('Checkout is slow', $exception, ['checkout']);

Plain PHP (any framework)

Build a client once and hold onto it — reports buffer during the request and are delivered by a shutdown hook (or call flush() yourself):

use BugBoard\ClientBuilder;
use BugBoard\Config;

$bugboard = ClientBuilder::create(new Config(
    keyId: getenv('BUGBOARD_KEY_ID') ?: null,
    signingSecret: getenv('BUGBOARD_SIGNING_SECRET') ?: null,
    environment: 'production',
));

$bugboard->minor('Tooltip misaligned', null, 'ui,polish');
$bugboard->flush(); // optional — the shutdown hook flushes automatically

ClientBuilder::create() accepts an explicit PSR-18 client + PSR-17 factories if you want to control the HTTP stack; otherwise it uses Guzzle when installed and PSR discovery as the fallback.

If your config already lives in an array (from a config file, a container, .env parsing), skip the Config constructor — ClientBuilder::createFromArray() takes snake_case or camelCase keys and is exactly what the Laravel and Symfony integrations use internally:

$bugboard = ClientBuilder::createFromArray([
    'key_id' => getenv('BUGBOARD_KEY_ID') ?: null,
    'signing_secret' => getenv('BUGBOARD_SIGNING_SECRET') ?: null,
    'environment' => 'production',
    'sample_rate' => 0.5,
]);

The 16 reporting methods

Every method takes (string $title, string|\Throwable|null $description = null, array|string $tags = []). The method name sets the card's severity and priority — there is no generic report():

low medium (default) high
critical criticalLow critical / criticalMedium criticalHigh
major majorLow major / majorMedium majorHigh
moderate moderateLow moderate / moderateMedium moderateHigh
minor minorLow minor / minorMedium minorHigh

Most apps only need the four medium-priority methods: critical, major, moderate, minor. Tags accept an array (['ui', 'checkout']) or a CSV string ('ui,checkout').

$client->droppedCount() reports how many reports the buffer discarded (see maxQueueSize below) — useful as a health metric if you sample heavily or report in tight loops.

Configuration

Every option, its type, and its default:

Option Type Default Purpose
keyId string Public key id (bbk_…) for HMAC auth. Recommended for servers.
signingSecret string Signing secret (bb_sec_…). Never transmitted.
apiKey string Publishable key (bb_pub_…), bearer auth — a client-side key; rarely right in PHP.
encryptionPublicKey string Base64 X25519 public key. When set, every payload is encrypted in transit.
encryptionKeyId string bbek_… id echoed in the envelope (enables key rotation).
enabled bool true Master switch (e.g. disable in tests).
environment string Added to every card as tag env:<value>.
release string Added to every card as tag release:<value>.
defaultTags string[] [] Merged into every card's tags.
sampleRate float 1.0 Probability (0–1) a report is sent.
maxQueueSize int 100 Buffer cap; overflow drops the newest report.
timeoutMs int 5000 Per-request timeout.
maxRetries int 3 Retries for 429/5xx/network errors (backoff + jitter, honors Retry-After).
beforeSend Closure Scrub PII or veto a report — return the payload array, or null to drop.
debug bool false Verbose internal logging via error_log (keys always redacted).
logLocally bool false Log each report locally instead of sending it (dry run).
captureLocation bool true Auto-capture the caller's file/line as file_name/line_number.
hideApiResponse bool true Ask the server to omit the card from its response (not echoed back).

concurrency and flushIntervalMs are accepted but have no effect: PHP's execution model delivers reports sequentially at flush time.

Scrubbing PII

new Config(
    keyId: …,
    signingSecret: …,
    beforeSend: function (array $payload): ?array {
        $payload['description'] = preg_replace('/\S+@\S+/', '[email]', $payload['description'] ?? '') ?: null;

        return $payload; // or return null to drop the report
    },
);

Encrypting sensitive reports

Set an encryption key and every payload is sealed with a libsodium sealed box (X25519) before it leaves the server — opaque at proxies and in access logs; BugBoard decrypts on receipt:

BUGBOARD_ENCRYPTION_PUBLIC_KEY=base64-x25519-public-key
BUGBOARD_ENCRYPTION_KEY_ID=bbek_xxxxxxxx

Generate the keypair under Settings → API Keys → Payload encryption. No extra dependency is needed — sodium ships with PHP.

Delivery semantics

  • Never blocks, never throws. Reporting methods buffer and return; delivery happens after the response (Laravel terminating, or register_shutdown_function). Failures surface via error_log when debug is on — never as exceptions in your app.
  • Retries on 429/5xx/network errors with exponential backoff + jitter, honoring Retry-After. Other 4xx (bad key, invalid payload) are never retried.
  • Deduplication is server-side: a report whose title or description exactly matches an existing card increments its occurrence count instead of creating a duplicate — use stable, deterministic titles (no timestamps or ids in the title).
  • Quota drops are silent by design: when the project's monthly quota is exhausted the server accepts and drops the report — logged in debug mode, never retried.

Exceptions

Delivery failures are caught inside the SDK and surfaced on the debug log — they are never thrown into your app. The taxonomy in BugBoard\Exceptions exists so that a custom TransportInterface, or your own logging around it, can tell the cases apart:

Exception Raised on Extra
BugBoardException base class for the four below
AuthException 401 / 403 — bad or revoked key
ValidationException 422 — the payload was rejected $fieldErrors (array<string, list<string>>)
RateLimitException 429 — too many reports $retryAfter (?int, seconds)
ServerException 5xx, network failure, timeout

Testing

Disable reporting entirely with enabled: false, or keep the client live but print reports instead of sending them with logLocally: true.

To assert on what your code reported, inject a fake transport — TransportInterface is a single-method seam:

use BugBoard\Client;
use BugBoard\Config;
use BugBoard\Payload;
use BugBoard\TransportInterface;

$transport = new class implements TransportInterface
{
    /** @var list<Payload> */
    public array $sent = [];

    public function send(Payload $payload): void
    {
        $this->sent[] = $payload;
    }
};

$bugboard = new Client(new Config(keyId: 'bbk_test', signingSecret: 'bb_sec_test'), $transport);
$bugboard->critical('Payment failed');
$bugboard->flush();

// $transport->sent[0]->severity === 'critical'

Contributing

Bug reports and pull requests are welcome — see CONTRIBUTING.md. Please read our Code of Conduct and report security issues per SECURITY.md.

License

MIT © BugBoard