Work · 2026
jsDevLogging
Logging for browser and Node with no runtime dependencies, usable from a script tag in PHP projects.
At a glance
- No runtime dependencies, 17 KB minified as a browser bundle
- Transports for console, HTTP, WebSocket, localStorage and file
- Buffers entries offline and sends them once the connection is back
- Middleware pipeline for enriching, filtering and sampling entries
- 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 libraries besides. There is not much to be won on features. All of them do assume a bundler, though.
A traditional PHP site has none. There is a template with a script tag in it, and when
something goes wrong in the browser the server never hears about it. For that case there is a
17 KB IIFE bundle: add the script tag, point HttpTransport at a hand-written logs.php, and
the errors from the browser land somewhere you actually see 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>
No dependencies
dependencies is an empty object. Whatever the library needs is in the library, including a
small event emitter with exactly the three methods ConfigManager uses.
The reason is a practical one. A logger runs on every request, and above all it keeps running when other things have already stopped working. A transitive dependency nobody ever read is particularly awkward in that position.
Offline behaviour
You need logs most urgently when something is not working, and in a browser that often means the connection is gone. A transport that discards its entries at exactly that moment is not much help.
BufferedTransport therefore listens for the browser’s offline events, keeps collecting into
the buffer instead of sending, and sends everything once the connection is back. The HTTP
transport batches entries anyway and retries failed sends with a growing delay.
Sub-path exports
Transports, formatters and middleware sit behind sub-path exports of their own. Write to the console only and you never import the WebSocket transport, so it never reaches your bundle, no tree-shaking involved.
Alongside that: ESM and CJS side by side, complete .d.ts types, and 152 tests that run before
every npm publish.