Search by

debugbundle / sdk-php

owenfar

DebugBundle SDK for PHP

v2.1.1 2026-11-06 13:09 UTC

README

Patch 2.1.1 corrects native logger exception evidence and capture safety while preserving public APIs and event schemas. Upgrade the DebugBundle distribution to adopt the correction.

PHP SDK for DebugBundle.

make evidence-performance compares identical exception/recovery calls with evidence disabled and enabled, including a full warning queue, with networking disabled. It reports capture time and PHP peak/live allocated memory after fixture setup, and checks native previous causes, original-error priority and existing retention ceilings. Memory measurements describe the PHP allocator, not cumulative allocation churn or total process RSS. make smoke-fpm separately verifies the existing request-end delivery deadline with evidence disabled and enabled.

Packagist CI License

Use this package to capture PHP backend exceptions, request metadata, Monolog records, runtime context, and probe data. It supports vanilla PHP plus Laravel, Symfony, Monolog, and browser relay adapters.

Requires PHP 8.2 or newer.

Evidence in 2.1.0

Version 2.1.0 adds bounded native previous-exception graphs and structured locations while retaining the original redacted error fields. Normal additional exception evidence is limited to 32 KiB; fatal evidence uses at most two nodes, eight frames per node and 4 KiB including operation metadata. Arguments and object dumps are excluded from the graph. Custom throwable formatters are not invoked. Native fatal file/line evidence is separate from the SDK's constructed trace. PHP 8.5 can supply a native fatal backtrace when its existing setting permits it; the SDK never enables that setting. Earlier runtimes report partial evidence.

New fields require proof that the complete delivery path and readers accept them. Discovery adds debugbundle_evidence=1 to existing config/ingestion requests; capture makes no discovery request. Short-lived PHP processes and file/custom transports may set ['evidenceCapabilities' => ['version' => 1, 'features' => ['exception_evidence_v1', 'recovery_v1']]] as an explicit assertion of complete-path support. Missing, unknown or revoked proof preserves legacy exception delivery. Only a negotiated invalid_event can retry the same original exception without optional additions; policy/security rejections and standalone context do not use that fallback.

captureExceptionEvidence defaults to true behind verified support. Explicit recovery additionally requires 'captureRecoveryObservations' => true, recovery_v1 and capture-policy permission; the minimal preset always disables it. Call captureRecovery(['failureRef' => $freshUuidV4, 'attemptId' => $attemptUuidV4, 'state' => 'attempt_completed', 'evidenceScope' => $scope]) for an explicit technical observation. States are attempt_started, attempt_completed, attempt_failed, recovery_observed and failure_persisted. An HTTP 200 or a completed attempt does not assert recovery. Optional fields are operation, statusCode, durationMs and ordinary correlation; arbitrary application data belongs in existing context/probes. Never use user identifiers or URLs as these fresh references. Include the same failureRef and scope with the original exception. Page scope also requires the same session correlation.

runWithOperationContext(['kind' => 'job', 'name' => $technicalName, 'limitMs' => $knownLimit], $callback) supplies an operation scope and monotonic elapsed time. Kinds are job, command and request. Results and thrown application errors are preserved. Existing Laravel/Symfony request owners and concurrent Fibers are isolated; completion/reset revokes ownership. Unknown names and limits stay unknown. The SDK never infers a wall-clock request limit from PHP's execution-time setting.

Recovery uses the ordinary bounded queue and mandatory privacy/hook policy. A per-instance tracker admits at most 50 failures, five attempts and 20 observations per five-minute window; tracker metadata and pending diagnostic payloads share a 64 KiB budget. ACK/drop/reset releases pending ownership without renewing the window. Valid hook identity/type replacements remain supported and are checked against the resulting event's policy. Complete returned envelopes are checked against the vendored accepting Draft 7 schema and semantic evidence rules. Hook JSON is copied without application serializers, clone handlers or foreign-object inspection. Invalid/throwing hooks preserve the protected original; exhausted validation and unfit valid replacements are withheld without undoing redaction. Mandatory privacy output is checked again before policy and delivery.

Native HTTP uses byte-sized ordered prefixes under the receiving 256-KiB limit, including wrapper/comma bytes. A flush shares the existing 250-ms monotonic budget across chunks; pending tails retain their ownership for a later explicit attempt. Request-end delivery remains one bounded best-effort request. Optional SDK evidence is reduced before withholding an unfit primary. PHP empty-map array idioms are projected into JSON objects using the accepting schema. A first fatal capture uses already canonical SDK shapes without allocating that interpreter. Relays count injected server credentials, reject unfit events before acceptance, and share the same native forwarding budget. Legacy custom success remains accepted; only complete canonical ACKs mark a durable spool delivered. Exclusive private markers preserve existing files and do not follow symlinks. PHP request-end loss and custom-transport limitations described below still apply.

PHP relays preserve typed context, graphs and browser observations after privacy, intersect support with downstream proof and keep legacy responses unchanged. Opted-in responses add a descriptor and indexed evidence_errors; the existing string errors array remains available. Local/spooled readers require explicit complete-path proof. Passive browser collection belongs to the browser SDK; PHP only preserves that evidence.

Local qualification: make check-docker, make coverage-docker, make evidence-runtime-docker, make smoke-worktree and make smoke-fpm. Publication and installed-site adoption remain separate release actions.

Automatic capture and application filtering

captureErrors() checks the current error_reporting() mask for each error. Warnings, notices and deprecations suppressed with @ or excluded by that mask are ignored. Enabled errors still follow the existing PHP handler behavior. Explicit captureException() calls and fatal shutdown capture remain available independently of the warning mask.

Exception capture uses the throwable class as the message when getMessage() is empty, before beforeSend and duplicate suppression. Nonempty messages (including "0") and the original stack are preserved, subject to mandatory redaction. Captures without request context use empty JSON objects for request headers and query parameters.

Version 2.0 delivery limit

Version 2.0 keeps the ordinary Composer install and requires no resident collector. Capturing a batch no longer calls the network. PHP's shutdown hook makes one best-effort send of at most 25 priority-selected events and 256 KiB, with exceptions ahead of routine logs. The built-in stream timeout is 250 ms but is advisory; DNS, slow peers, and custom transports may exceed it. The send occupies a PHP worker while it runs, and unsent events are lost when the request ends or the endpoint is unavailable. Use an application-owned background runner if your deployment requires stronger delivery or zero request-end wait.

init() no longer calls configFetcher. If you supply one, invoke refreshRemoteConfig() from a controlled background or administrative task; do not call it on the request path. flush() is an explicit synchronous operation intended for CLI, diagnostics, or controlled shutdown. Existing installed 1.x versions retain their prior behavior until a major upgrade. Review the request-end worker-cost and loss tradeoff before upgrading.

Installation

composer require debugbundle/sdk-php

Quick Start

<?php

use DebugBundle\DebugBundle;

DebugBundle::init([
    'projectToken' => getenv('DEBUGBUNDLE_PROJECT_TOKEN'),
    'service' => 'checkout-api',
    'environment' => 'production',
]);

DebugBundle::captureErrors();
DebugBundle::captureExceptions();
DebugBundle::captureShutdown();

Capture handled errors, logs, messages, and probes explicitly:

DebugBundle::captureException($throwable);
DebugBundle::captureLog('payment retry failed', 'warning', ['order_id' => $orderId]);
DebugBundle::captureMessage('worker started');
DebugBundle::probe('checkout.cart', ['item_count' => count($cart->items)]);

Framework Integrations

Runtime Integration
Laravel Service provider, request middleware, exception handler decoration, and log tap
Symfony Bundle/subscriber integration and Monolog service handler
Monolog DebugBundle\Logging\DebugBundleHandler
Vanilla PHP captureErrors(), captureExceptions(), and captureShutdown()

Example apps live in examples/laravel and examples/symfony.

Browser Relay

PHP backends can host the browser relay endpoint used by @debugbundle/sdk-browser.

Runtime Integration
Laravel DebugBundle\Framework\Laravel\DebugBundleRelayMiddleware
Symfony DebugBundle\Framework\Symfony\DebugBundleRelayController
Generic PHP DebugBundle\Relay\BrowserRelayHandler

The relay validates JSON batches, enforces same-origin defaults or explicit allowed origins, strips browser-supplied trust fields, keeps the server-side project token private, and supports local-only file writes or durable connected forwarding.

Relay behavior and defaults:

  • Same-origin is the default when allowedOrigins is omitted; split frontend/backend deployments must set an explicit allowlist.
  • Relay requests must use Content-Type: application/json and stay below the default 256 KB body limit.
  • Per-IP rate limiting defaults to 60 requests per minute.
  • projectMode="local-only" writes browser events to .debugbundle/local/events and never forwards them.
  • projectMode="connected" writes durable spool files by default and forwards with the server-side projectToken only.
  • Leaving projectMode unset disables local writes and forwarding; accepted batches only surface through onAccept.
  • Connected relay mode without a usable project token still accepts and can spool events, but forwarding remains disabled until the server provides credentials. This is the relay missing token behavior.

Configuration Reference

Configuration sources and precedence:

  • The SDK only reads the array passed to DebugBundle::init([...]) or DebugBundleSdk::init([...]).
  • Environment variables, Laravel config files, Symfony service definitions, or other framework-native settings are convenience sources that your application maps into that init array; the SDK does not read them directly.
  • Explicit init arguments always win because they are the only configuration source the runtime consumes.
  • Capture-policy fields are server-owned and are not accepted in local SDK config. The SDK learns capture policy through GET /v1/sdk/config and applies it locally before transport.
Option Default Purpose
projectToken required for connected capture Write-only DebugBundle project token. Blank or missing tokens disable connected capture and leave the SDK status at disconnected.
service php-service Service name shown on incidents and bundles.
environment development Runtime environment such as production, staging, or development.
endpoint https://api.debugbundle.com/v1/events Ingestion endpoint for connected mode or self-hosting.
enabled true Disable all capture without removing instrumentation.
logger none Optional Monolog logger to attach.
logLevel warning Minimum captured log severity.
sampleRate 1.0 Fraction of events to keep before transport.
batchSize 25 Events per batch before flushing.
redactFields common sensitive fields Additional field names to redact.
maxProbeLabels 50 Maximum distinct probe labels buffered in memory.
maxProbeEntriesPerLabel 10 Maximum entries retained per probe label.
probeFlushOnError true Attach buffered probe data to captured exceptions.
probesPollInterval 60000 Remote probe config poll interval in milliseconds.
configFetcher none Custom remote-config fetch callable for tests or advanced routing.

Framework-native wiring:

  • Laravel: initialize the SDK through your service provider or config bootstrap, then register DebugBundleMiddleware, DebugBundleExceptionHandler, and DebugBundleLogTap where those surfaces apply.
  • Symfony: initialize the SDK in your container, then register DebugBundleEventSubscriber, DebugBundleRelayController, and the Monolog service handler.
  • Generic PHP: initialize with DebugBundle::init([...]), then opt into captureErrors(), captureExceptions(), and captureShutdown().

Install Examples by Mode

Vanilla PHP:

<?php

use DebugBundle\DebugBundle;

DebugBundle::init([
    'projectToken' => getenv('DEBUGBUNDLE_PROJECT_TOKEN'),
    'service' => 'worker',
    'environment' => 'production',
]);

DebugBundle::captureErrors();
DebugBundle::captureExceptions();
DebugBundle::captureShutdown();

Laravel:

<?php

use DebugBundle\DebugBundle;
use DebugBundle\Framework\Laravel\DebugBundleLogTap;

DebugBundle::init([
    'projectToken' => env('DEBUGBUNDLE_PROJECT_TOKEN'),
    'service' => 'checkout-api',
    'environment' => 'production',
]);

// config/logging.php
'tap' => [DebugBundleLogTap::class];

Symfony:

services:
  DebugBundle\DebugBundleSdk:
    calls:
      - method: init
        arguments:
          -
            projectToken: '%env(DEBUGBUNDLE_PROJECT_TOKEN)%'
            service: 'checkout-api'
            environment: 'production'

Monolog logger integration:

<?php

use DebugBundle\DebugBundleSdk;
use Monolog\Logger;

$sdk = new DebugBundleSdk();
$sdk->init([
    'projectToken' => getenv('DEBUGBUNDLE_PROJECT_TOKEN'),
    'service' => 'checkout-api',
    'environment' => 'production',
    'logger' => new Logger('checkout'),
]);

Connected browser relay:

<?php

use DebugBundle\Framework\Symfony\DebugBundleRelayController;

$relay = new DebugBundleRelayController([
    'allowedOrigins' => ['https://app.example.com'],
    'projectMode' => 'connected',
    'projectToken' => getenv('DEBUGBUNDLE_PROJECT_TOKEN'),
    'endpoint' => 'https://api.debugbundle.com/v1/events',
]);

Local-only browser relay:

<?php

use DebugBundle\Relay\BrowserRelayHandler;

$relay = new BrowserRelayHandler([
    'allowedOrigins' => ['http://localhost:3000'],
    'projectMode' => 'local-only',
    'localEventsDir' => '.debugbundle/local/events',
]);

There is no separate zero-install fallback for the PHP SDK itself in V1. The nearest low-friction path is the browser relay mounted on an existing PHP application.

Runtime and Framework Support

Surface Minimum compatibility version Recommended production version Installed-base compatibility lane Rolling CI lane Out of scope
PHP runtime 8.2 8.4 8.2 and 8.3 remain supported for installed-base coverage 8.2, 8.3, 8.4 8.1 and older
Laravel 11.x latest 11.x patch 11.x compatibility support repo tests cover the Illuminate 11.x adapter lane Laravel 10.x and older
Symfony HttpFoundation / HttpKernel 7.2+ latest 7.2+ patch line 7.2+ compatibility support repo tests cover the Symfony 7.2+ adapter lane Symfony 6.x and older
Monolog 3.x latest 3.x patch 3.x compatibility support package dependency and tests install Monolog 3.x Monolog 2.x and older

Post-V1 planned expansions from spec/sdk-language-targets.md remain out of scope here: Slim, Laminas, native async runtimes, WordPress plugin behavior beyond the dedicated plugin repo, and queue-worker-specific adapters outside the shared PHP core.

Dependency Alignment

debugbundle/sdk-php ships as one package in V1, so there is no multi-package BOM or Composer plugin family to align.

  • Pin one debugbundle/sdk-php version across your web and worker repos when you want identical SDK behavior everywhere.
  • Keep Monolog inside the supported 3.x lane.
  • Keep Laravel inside the supported 11.x lane and Symfony HttpFoundation / HttpKernel inside the supported 7.2+ lane when you rely on the shipped framework adapters.
  • Public snippets should not mix SDK package versions across separate repos or examples.

Laravel Logging

For Laravel's Monolog-backed stack, add the SDK tap class to a channel in config/logging.php:

use DebugBundle\Framework\Laravel\DebugBundleLogTap;

'channels' => [
    'stack' => [
        'driver' => 'stack',
        'channels' => ['single'],
        'tap' => [DebugBundleLogTap::class],
    ],
],

Symfony Logging

For Symfony applications using MonologBundle, register the SDK handler as a Monolog service:

services:
  DebugBundle\Logging\DebugBundleHandler:
    arguments:
      $sdk: '@DebugBundle\DebugBundleSdk'

monolog:
  handlers:
    debugbundle:
      type: service
      id: DebugBundle\Logging\DebugBundleHandler
      level: warning

Safety Defaults

  • SDK failures are caught internally and do not crash the host process.
  • Sensitive fields are redacted before transport.
  • Duplicate event storms are suppressed locally.
  • Runtime context excludes environment variables.
  • Browser relay requests cannot smuggle server-side credentials, and the relay enforces credential isolation by forwarding with only the server-owned project token.

Service Naming

  • Use one stable backend service name per deployable, such as checkout-api, billing-worker, or admin-api.
  • Keep browser relay traffic on the browser-owned service name by default, for example checkout-web; the PHP relay preserves the browser service unless you explicitly override service or environment in relay options.
  • When multiple PHP deployables share one DebugBundle project, give each deployable its own service value instead of reusing one generic name.
  • Reuse the same environment label across related surfaces, for example production on both checkout-web and checkout-api, so incident and bundle correlation stays readable.

Safe Startup and Status

  • The SDK never crashes the host process when configuration is invalid, transport calls fail, or remote config responses are malformed.
  • DebugBundle::init(['projectToken' => '']) or any missing or blank connected token leaves capture disabled and DebugBundle::getStatus() returns disconnected.
  • Rate-limited transports move the status to degraded until the retry window expires.
  • Three consecutive transport failures move the status to disconnected until a later successful flush.
  • DebugBundle::getLastEventAt() returns the Unix timestamp in milliseconds of the last successful delivery, or null before the first success.
  • Connected mode without a usable project token does not silently report healthy connected capture.

First-Event Verification

Use the repo-local smoke target to prove a fresh install end to end against a mock ingestion endpoint:

make smoke

That command builds a Composer archive, installs the package into a fresh consumer fixture, emits an application-owned captureMessage() plus captureRequest() event, submits a browser relay request through the Symfony relay controller, validates the received event envelopes, and confirms both paths reach the mock ingestion endpoint with the expected service, environment, SDK metadata, and correlation fields.

For published-package verification during release validation, run the same smoke path against Packagist:

php smoke/run_app_driven_smoke.php --package debugbundle/sdk-php:1.5.0

For a manual verification snippet inside your own app:

<?php

use DebugBundle\DebugBundle;

DebugBundle::init([
  'projectToken' => getenv('DEBUGBUNDLE_PROJECT_TOKEN'),
  'service' => 'checkout-api',
  'environment' => 'staging',
]);

DebugBundle::captureMessage('debugbundle first-event verification', 'error');
DebugBundle::flush();
var_dump(DebugBundle::getStatus(), DebugBundle::getLastEventAt());

Development

composer install
composer test
composer typecheck
make smoke

CI validates Composer metadata, PHPUnit, PHPStan, the app-driven smoke path, event schema fixtures, real HTTP transport coverage, and coverage gates.

Documentation

License

Apache-2.0. See LICENSE.

Delivery acknowledgement and retry limits

The built-in HTTP transport requires the canonical ingestion acknowledgement (accepted, rejected, and errors). Empty responses, unrelated JSON, malformed counts, or invalid rejection indices retain the full batch with backoff. Valid acknowledgements remove accepted and terminally rejected events and retry only the indexed retryable rejections; an all-rejected batch does not advance lastEventAt.

File transports and explicitly supplied custom/legacy transports retain their documented bodyless success fallback. A custom transport that returns acknowledgement fields must return the complete canonical shape. Retry hints (delay seconds or HTTP dates) are bounded to five minutes and measured from response receipt, including partial/protocol acknowledgements and retryable server failures. Without a server hint, existing retry timing is preserved. Failures remain contained within the SDK's existing delivery path.

Probe checkpoint guidance

Application instrumentation supplies probe labels and bounded data. Deploy a new checkpoint before activating it. Remote activation affects future matching calls when policy permits; it does not inject code or recover unretained measurements. Built-in runtime/error/device collectors keep their documented SDK settings.

Use fixed labels and allowlisted technical states: media.preview.checkpoint (stage/input kind/attempt), auth.session.checkpoint (observed session presence and result), job.dependency.checkpoint (named dependency/attempt/result), or cache.preview.checkpoint (hit/miss/coarse age bucket). Omit customer identifiers, tokens/cookies, cache keys, URLs and raw payloads. HTTP 200 or attempt completion alone does not establish semantic recovery.

Empty probe data means no matching retained observation for the selected sample window. It does not confirm activation delivery to every SDK or execution of a label. Check the deployed SDK/wrapper version, instrumentation, scope, expiry, policy, sampling and delivery receipts. See the probe recipes.