Official BugBoard SDK for PHP โ€” report bugs as cards on your project board, with first-class Laravel and Symfony support.

v1.1.1 2026-07-20 17:29 UTC

This package is auto-updated.

Last update: 2026-07-20 17:30:45 UTC


README

CI Packagist Version License: MIT

The official BugBoard SDK for PHP. Report bugs 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.

๐Ÿ“– Usage guide โ€” in-depth integration for plain PHP, Laravel, and Symfony: exception handlers, queue workers, Monolog, config caching, testing, and troubleshooting.

Contents

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');
}

If you have disabled package discovery (extra.laravel.dont-discover in your composer.json), register the provider yourself in bootstrap/providers.php:

return [
    App\Providers\AppServiceProvider::class,
    BugBoard\Laravel\BugBoardServiceProvider::class,
];

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, mixed $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').

The description accepts anything โ€” no json_encode() needed. A Throwable contributes its message and trace, arrays and objects are pretty-printed as JSON, and Arrayable objects (a Laravel Request, an Eloquent model) are unwrapped via toArray() first:

BugBoard::critical('Validation failed', $request);
BugBoard::major('Bad cart state', ['user_id' => $user->id, 'items' => $cart->items]);

Mind what you attach: request input and model attributes routinely contain secrets. See What the description accepts.

$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 event allowance is exhausted โ€” or the project is paused or archived โ€” the server accepts and drops the report. Logged, never retried, never thrown into your app.

  • The SDK then stops sending: after a drop it discards reports locally instead of sending them, until the drop is expected to have cleared (the next midnight UTC for a spent allowance, 30 minutes for a paused or archived project). One report is let through afterwards to check, so reporting resumes on its own.

    On Laravel and Symfony this is wired to your application cache automatically, so the suppression holds across requests. Standalone PHP-FPM users get it only within a single request unless they pass a QuotaStore โ€” see Quota suppression.

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