First-party PHP client for the didnt.run ping API: correct, non-blocking instrumentation by default.

Maintainers

Statistics

Installs: 5

Dependents: 0

Suggesters: 0

Stars: 0

v1.0.1 2026-08-06 12:03 UTC

This package is not auto-updated.

Last update: 2026-08-07 12:54:31 UTC


README

Instrument a scheduled job against didnt.run with one line. The SDK's contract: your job is never slowed beyond a hard time budget and never failed by the watchdog — every transport problem is swallowed (and logged if you hand it a PSR-3 logger).

Install

composer require didntrun/sdk

Use

use DidntRun\Sdk\Watchdog;

// One env var wires everything: https://{ping-token}@it.didnt.run[?budget_ms=2000&connect_timeout_ms=1000]
$watchdog = Watchdog::fromDsn($_SERVER['DIDNT_RUN_DSN']);

// Recommended: bracket the work. ONE completion ping — measured duration on success,
// exception context on failure (the exception is rethrown; your job's semantics don't change).
$report = $watchdog->run(DailyReportJob::class, fn () => $this->generate());

// Or fire the bare "I ran" ping yourself:
$watchdog->ping('app.daily-report');                        // any string id
$watchdog->ping(DailyReportJob::class, durationMs: 1234);   // FQCNs work — see "Job identity"

// Or send explicit lifecycle pings — manual bracketing when start and finish
// live in different processes or code paths (see "Lifecycle bracketing"):
$watchdog->start('app.daily-report');
$watchdog->success('app.daily-report', durationMs: 1234);
$watchdog->fail('app.daily-report');

ping() returns bool (delivered vs. swallowed) if you want to observe delivery; you never have to.

Job identity

Ids pass through an identity-defining normalizer: leading \ stripped, \. — so App\Jobs\SyncOrders becomes the job App.Jobs.SyncOrders. Renaming a class therefore registers a NEW job (the old one goes stale and will alert): after a rename, retire or rename the old job in the admin UI.

Failure semantics

  • Misconfiguration (bad DSN, empty token, or a token containing non-printable-ASCII characters) throws InvalidArgumentException at construction — fail fast at wiring time. After that, nothing throws.
  • Each ping gets a wall-clock budget (default 2 s, connect 1 s) with one quick reconnect retry inside it. HTTP errors are never retried; 401 is logged as error (your token is wrong).
  • Oversized meta (> 64 KiB body) is dropped; the ping still goes out.

Wiring

Symfony (config/services.yaml):

services:
    DidntRun\Sdk\Watchdog:
        factory: ['DidntRun\Sdk\Watchdog', 'fromDsn']
        arguments: ['%env(DIDNT_RUN_DSN)%', null, '@logger']

Nette (config/services.neon):

services:
    - DidntRun\Sdk\Watchdog::fromDsn(::getenv('DIDNT_RUN_DSN'))

Plain script: $watchdog = Watchdog::fromDsn($_SERVER['DIDNT_RUN_DSN']);

Bring your own HTTP client (optional): pass a Psr18Transport wrapping your PSR-18 client (requires psr/http-client + psr/http-factory). Note the time budget is then only as good as your client's own timeout config.

Lifecycle bracketing

run() sends one completion ping by default. Opt into bracketing and it also emits a start ping before the work runs: the start anchor is duration-immune, so it tightens the inferred cadence for jobs whose duration is non-trivial or variable (a terminal-only anchor drifts with run length).

Enable it globally (constructor arg, or the lifecycle DSN param) with a per-call override:

// Global default via DSN: https://{token}@it.didnt.run?lifecycle=true
$watchdog = Watchdog::fromDsn($_SERVER['DIDNT_RUN_DSN']);
$watchdog->run(DailyReportJob::class, fn () => $this->generate());   // start + completion

// Per-call override wins (null = inherit the instance default):
$watchdog->run(DailyReportJob::class, fn () => $this->generate(), lifecycle: true);

A bracketed run has two budget windows — one ping before the work, one after — so its worst-case added wall-clock is ~2× the per-ping budget, straddling the work. For work a single closure can't wrap (it spans processes, or start and finish live in different code), call start() / success() / fail() directly instead.

Server requirement: bracketing needs a ping-api that understands lifecycle kinds. Against an older, kind-blind self-hosted server, leave lifecycle off — a separate start ping would double-count runs in schedule inference.

Requirements

PHP ≥ 8.3, ext-curl. Only Composer dependency: psr/log.

License

MIT — see LICENSE.

Development happens in the didnt.run monorepo; this repository is a read-only subtree split. Please report issues against the SDK there rather than opening merge requests here.