net-os / laravel-netos-debug
Ships debugbar data for every request to the NetOS Debug desktop app.
Fund package maintenance!
Requires
- php: ^8.4
- barryvdh/laravel-debugbar: ^4.4
- illuminate/contracts: ^12.0||^13.0
- illuminate/http: ^12.0||^13.0
- illuminate/support: ^12.0||^13.0
- spatie/laravel-package-tools: ^1.16
- spatie/laravel-ray: ^1.40
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.14
- orchestra/testbench: ^10.0||^11.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Ships the full Laravel Debugbar payload for every finished request to the NetOS Debug desktop app, which renders it as a debugbar clone in its Requests view.
Not on Packagist; install it straight from the repository.
Installation
Add the repository, then require it as a dev dependency:
"repositories": [ { "type": "vcs", "url": "https://github.com/net-os-com/laravel-netos-debug" } ]
composer require --dev net-os/laravel-netos-debug
That is the whole setup. The package registers its own middleware and applies the Debugbar options it needs; there is nothing to publish and nothing to wire up by hand.
It ships requests only in local by default, and reads the NetOS Debug host and
port from your existing ray.php — including remote_path / local_path, so
paths from a Docker container stay clickable on the host.
Configuration
Publish the config only if you want to change something:
php artisan vendor:publish --tag=netos-debug-config
| Key | Default | Meaning |
|---|---|---|
enabled |
null |
null defers to environments; an explicit value wins either way |
environments |
['local'] |
Where requests are shipped when enabled is null |
except |
debugbar, telescope, horizon, ignition | Paths never shipped, matched with Request::is() |
max_payload_bytes |
512 KB | Larger payloads are trimmed before sending |
configure_debugbar |
true |
Whether the package applies the Debugbar options below |
collections.path |
openapi |
Where saved API requests are written, relative to the app root |
Debugbar options this package applies
| Option | Set to | Why |
|---|---|---|
collectors.route |
true |
Route name, action and middleware |
collectors.cache |
true |
Cache hits, misses, writes and forgets |
collectors.auth |
true |
The authenticated user |
options.db.with_params |
false |
The view wants raw SQL with ? plus separate bindings |
options.db.backtrace |
true |
Per-query backtrace |
options.db.timeline |
true |
Queries on the shared timeline |
with_params is the one worth knowing about. Debugbar defaults to substituting
bindings into the SQL string; the Requests view expects placeholders alongside a
separate bindings list so it can toggle between them and rebuild a copyable
statement. Left on, that toggle silently does nothing.
These are applied while registering, not while booting: Debugbar reads them
during its own boot(), and the boot order of auto-discovered providers is not
guaranteed.
Set configure_debugbar to false to own these yourself.
How it works
The middleware does its work in terminate(), after the response has been sent,
so neither shaping nor an unreachable desktop app adds latency. Debugbar has
normally already collected by then, on RequestHandled; getData() collects
lazily if it has not. Everything is wrapped in rescue() — a debug tool must
never be able to fail a request.
It is deliberately not wrapped in defer(). Laravel's
InvokeDeferredCallbacks is prepended to the middleware stack, so it terminates
before this middleware does, and a callback queued here would simply be
dropped.
Known gaps
Three fields cannot be filled from Debugbar 4 and are sent as null or
degraded:
explain— Debugbar 4 returns a handle for an on-demand AJAX call, not the rows, and only when its storage is open.hint— Debugbar 4 dropped hints. No loss: the desktop app derives its own duplicate and N+1 warnings.trace[].callable— Debugbar's frames carry no class or method, only a normalized path, a view name, a middleware alias or"Route binding".
When Debugbar filters out every frame of a query through its own
exclude_paths, that query's source comes back empty.
Development
composer analyse # PHPStan level 6 composer format # Pint
There is no test suite; verification is a real request against a running app with NetOS Debug open.
Saved API requests
The desktop app's API tab is a client for this application: you pick a route, fill it in, send it, and read the response. A request worth keeping is saved through this package, into this repository:
php artisan netos-debug:collections list php artisan netos-debug:collections read desks php artisan netos-debug:collections forget desks get-desks echo '{"name":"List desks","method":"GET","path":"/desks"}' \ | php artisan netos-debug:collections save desks
One file per collection, under collections.path — openapi/desks.yaml by
default — as an OpenAPI 3.1 document:
openapi: 3.1.0 info: title: Desks version: 1.0.0 paths: /desks: get: operationId: get-desks summary: 'List desks' parameters: - name: X-Tenant in: header schema: type: string example: '{{ tenant }}' x-netos-debug: name: 'List desks' bodyMode: none auth: impersonate
OpenAPI because these files are meant to be read in a pull request and opened by
something other than one app; YAML because a changed header should be one
changed line in a diff. What OpenAPI has no field for — which body editor was
open, how the request authenticates — lives under x-netos-debug, which any
other reader will ignore.
Nothing secret is written. A value that resolves at send time is stored as
the reference, {{ token }}, not as what it stands for; the values live in the
desktop app's own storage. The authentication mode is saved, but never who was
impersonated and never a token.
Saving the same verb and path twice replaces that operation rather than adding a
near-duplicate, and emptying a collection deletes its file instead of leaving an
openapi: 3.1.0 with nothing under it.