PHPackages                             bugboard/sdk - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [HTTP &amp; Networking](/categories/http)
4. /
5. bugboard/sdk

ActiveLibrary[HTTP &amp; Networking](/categories/http)

bugboard/sdk
============

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

v1.2.1(4w ago)07MITPHPPHP ^8.2CI passing

Since Jul 15Pushed 4w agoCompare

[ Source](https://github.com/bug-board/bugboard-php)[ Packagist](https://packagist.org/packages/bugboard/sdk)[ Docs](https://github.com/bug-board/bugboard-php)[ RSS](/packages/bugboard-sdk/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (6)Dependencies (30)Versions (7)Used By (0)

BugBoard SDK for PHP
====================

[](#bugboard-sdk-for-php)

[![CI](https://github.com/bug-board/bugboard-php/actions/workflows/ci.yml/badge.svg)](https://github.com/bug-board/bugboard-php/actions/workflows/ci.yml)[![Packagist Version](https://camo.githubusercontent.com/5694df7d94c03da8ec67cc8bf7202f7d2c54641d05cf4db34fea5930650accb9/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f627567626f6172642f73646b2e737667)](https://packagist.org/packages/bugboard/sdk)[![License: MIT](https://camo.githubusercontent.com/08cef40a9105b6526ca22088bc514fbfdbc9aac1ddbf8d4e6c750e3a88a44dca/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d626c75652e737667)](LICENSE)

Official [BugBoard](https://bugboard.dev) 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.

```
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](docs/USAGE.md)** — in-depth integration for plain PHP, Laravel, and Symfony: exception handlers, queue workers, Monolog, config caching, testing, and troubleshooting.
>
> 📁 **[Examples](examples/)** — copy-paste-ready files for every usage mode: plain-PHP quickstart, credentials, encryption, `beforeSend` scrubbing, global exception handling, CLI workers, quota stores, [Laravel](examples/11-laravel/), [Symfony](examples/12-symfony/), and testing.

Contents
--------

[](#contents)

- [Requirements](#requirements)
- [Examples](examples/) — runnable, one file per usage mode
- [Installation](#installation)
- [Installing php-sodium](#installing-php-sodium-for-payload-encryption) — only for payload encryption
- **Framework setup** — [Laravel](#laravel) · [Symfony](#symfony) · [Plain PHP](#plain-php-any-framework)
- [The 16 reporting methods](#the-16-reporting-methods)
- [Configuration](#configuration) — every option, and [scrubbing PII](#scrubbing-pii)
- [Encrypting sensitive reports](#encrypting-sensitive-reports)
- [Delivery semantics](#delivery-semantics) — retries, dedup, quotas, [exceptions](#exceptions)
- [Testing](#testing)
- [Contributing](#contributing) · [License](#license)

Requirements
------------

[](#requirements)

- PHP 8.2+
- Any [PSR-18](https://www.php-fig.org/psr/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
------------

[](#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)
----------------------------------------------

[](#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

[](#1-install-the-extension)

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

#### Debian / Ubuntu

[](#debian--ubuntu)

```
sudo apt update
sudo apt install php8.3-sodium
```

#### RHEL / CentOS / Rocky / AlmaLinux / Fedora

[](#rhel--centos--rocky--almalinux--fedora)

```
sudo dnf install php-sodium
```

#### Alpine

[](#alpine)

```
apk add php83-sodium
```

#### macOS (Homebrew)

[](#macos-homebrew)

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

```
brew install php
```

#### Docker

[](#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

[](#windows)

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

#### Building PHP from source

[](#building-php-from-source)

Configure with `--with-sodium`.

### 2. Enable it

[](#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

[](#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

[](#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
-------

[](#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
-------

[](#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)
-------------------------

[](#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
------------------------

[](#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()`:

lowmedium (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](docs/USAGE.md#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
-------------

[](#configuration)

Every option, its type, and its default:

OptionTypeDefaultPurpose`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:`.`release``string`—Added to every card as tag `release:`.`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 `file_name`/`line_number` — a throwable's throw site, else the call site.`projectRoot``?string`autoTrimmed off `file_name`, so reports read `/app/Foo.php`. Detected from Composer.`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

[](#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
----------------------------

[](#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
------------------

[](#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](docs/USAGE.md#quota-suppression).

### Exceptions

[](#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:

ExceptionRaised onExtra`BugBoardException`base class — and any other 4xx (400, 404, 405, 413, …)—`AuthException`401 / 403 — bad or revoked key—`ValidationException`422 — the payload was rejected`$fieldErrors` (`array`)`RateLimitException`429 — too many reports`$retryAfter` (`?int`, seconds)`ServerException`5xx, network failure, timeout—Only `RateLimitException` and `ServerException` are retried. Any other 4xx is a configuration or payload bug that a retry cannot fix, so it fails on the first attempt.

Testing
-------

[](#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 */
    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
------------

[](#contributing)

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

License
-------

[](#license)

[MIT](LICENSE) © BugBoard

###  Health Score

42

—

FairBetter than 88% of packages

Maintenance94

Actively maintained with recent releases

Popularity6

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity50

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 93.2% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~4 days

Total

6

Last Release

28d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/c166f09a8da333ce89fa9356ba1076d63cda743c14b0608b440f2bcd98c421e9?d=identicon)[s\_b\_1805](/maintainers/s_b_1805)

---

Top Contributors

[![mrsanta79](https://avatars.githubusercontent.com/u/43307935?v=4)](https://github.com/mrsanta79 "mrsanta79 (68 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (4 commits)")[![semantic-release-bot](https://avatars.githubusercontent.com/u/32174276?v=4)](https://github.com/semantic-release-bot "semantic-release-bot (1 commits)")

---

Tags

symfonylaravelloggingmonitoringerror-reportingpsr-18error-monitoringcrash-reportinganalyticsexception handlingobservabilityerror-trackingexception trackingbug-trackingissue-trackingbug trackerbug-reportingbugboardbug-monitoring

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/bugboard-sdk/health.svg)

```
[![Health](https://phpackages.com/badges/bugboard-sdk/health.svg)](https://phpackages.com/packages/bugboard-sdk)
```

###  Alternatives

[telnyx/telnyx-php

Official Telnyx PHP SDK — APIs for Voice, SMS, MMS, WhatsApp, Fax, SIP Trunking, Wireless IoT, Call Control, and more. Build global communications on Telnyx's private carrier-grade network.

36863.5k2](/packages/telnyx-telnyx-php)[getbrevo/brevo-php

Official PHP SDK for the Brevo API.

1024.4M60](/packages/getbrevo-brevo-php)[anthropic-ai/sdk

Anthropic PHP SDK

1751.3M26](/packages/anthropic-ai-sdk)[n1ebieski/ksef-php-client

PHP API client that allows you to interact with the API Krajowego Systemu e-Faktur

9197.7k](/packages/n1ebieski-ksef-php-client)[shopware/app-php-sdk

Shopware App SDK for PHP

15130.8k3](/packages/shopware-app-php-sdk)[trycourier/courier

Courier PHP SDK

16672.8k](/packages/trycourier-courier)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
