Work · 2026
jsDevLogging
A logging library for browser and Node — free of runtime dependencies, and usable from a single script tag on PHP sites.
At a glance
- No runtime dependencies — 17 KB minified as a browser bundle
- Transports for console, HTTP, WebSocket, localStorage and file
- Buffers while offline and replays once the connection returns
- Middleware pipeline for enriching, filtering and sampling
- Sub-path exports, so unused transports never reach the bundle
- 152 tests, ESM and CJS side by side, complete type definitions
Why another logging library
There is pino, there is winston, there is consola and a dozen more. You do not win that comparison on features. What is interesting is the point at which all of them stop: they assume a bundler.
A PHP site has none. There is a template, inside it a script tag, and when something goes wrong
in the browser the server never hears about it. That is the gap jsDevLogging fills — a 17 KB
IIFE bundle, one single script tag, an HttpTransport pointed at a hand-written logs.php,
and the errors from the browser land where somebody actually looks at them.
<script src="/assets/browser.min.js"></script>
<script>
const logger = jsDevLogging.createLogger({
transports: [new jsDevLogging.HttpTransport({ url: '/api/logs.php' })],
});
logger.error('Checkout failed', { orderId: 4711 });
</script>
Zero dependencies, meant literally
dependencies is an empty object. No emitter polyfill, no formatter, nothing — whatever the
library needs is in the library, including a small event emitter of its own carrying exactly
the three methods ConfigManager uses.
That is not purism, it is arithmetic. A logger is infrastructure: it runs on every request, it keeps running when a good deal else has already broken, and it is the worst conceivable place for a transitive dependency nobody ever read.
The most interesting part is the offline case
You need logs most urgently when something is not working — and in a browser, “something is not working” often enough means “the connection is gone”. A transport that drops its entries at precisely that moment is useless in the situation it was built for.
So BufferedTransport listens for the browser’s offline events, keeps collecting into the
buffer instead of sending, and replays everything once the connection comes back. The HTTP
transport batches entries anyway and retries failed sends with a growing delay.
What you do not use, you do not load
Transports, formatters and middleware sit behind sub-path exports of their own. Write to the console only and the WebSocket transport never reaches your bundle — not because a bundler was clever enough to throw it back out, but because it was never imported in the first place.
Alongside that: ESM and CJS side by side, complete .d.ts types, and 152 tests that have to
pass on every npm publish.