Browser Drain
Most observability tools focus on server-side logs. The browser drain gives you a framework-agnostic way to send structured logs from the browser to any HTTP endpoint — no vendor SDK, no framework coupling.
Quick Start
import { initLogger, log } from 'evlog'
import { createBrowserLogDrain } from 'evlog/browser'
const drain = createBrowserLogDrain({
drain: { endpoint: 'https://logs.example.com/v1/ingest' },
})
initLogger({ drain })
log.info({ action: 'page_view', path: location.pathname })
How It Works
log.info()/log.warn()/log.error()push events into a memory buffer- Events are batched by size (default 25) or time interval (default 2 s)
- Batches are sent via
fetchwithkeepalive: trueso requests survive page navigation - When the page becomes hidden (tab switch, navigation), buffered events are flushed via
navigator.sendBeaconas a fallback - Your server endpoint receives a
DrainContext[]JSON array and processes it however you like
Two-Tier API
createBrowserLogDrain(options)
High-level, pre-composed: creates a pipeline with batching, retry, and auto-flush on visibilitychange. Returns a PipelineDrainFn<DrainContext> directly usable with initLogger({ drain }).
import { initLogger, log } from 'evlog'
import { createBrowserLogDrain } from 'evlog/browser'
const drain = createBrowserLogDrain({
drain: { endpoint: 'https://logs.example.com/v1/ingest' },
pipeline: { batch: { size: 50, intervalMs: 5000 } },
})
initLogger({ drain })
log.info({ action: 'click', target: 'buy-button' })
createBrowserDrain(config)
Low-level transport function. Use this when you want full control over the pipeline configuration:
import { createBrowserDrain } from 'evlog/browser'
import { createDrainPipeline } from 'evlog/pipeline'
import type { DrainContext } from 'evlog'
const transport = createBrowserDrain({
endpoint: 'https://logs.example.com/v1/ingest',
})
const pipeline = createDrainPipeline<DrainContext>({
batch: { size: 100, intervalMs: 10000 },
retry: { maxAttempts: 5 },
})
const drain = pipeline(transport)
Configuration Reference
BrowserDrainConfig
| Option | Default | Description |
|---|---|---|
endpoint | — | (required) Full URL of the server ingest endpoint |
headers | — | Custom headers sent with each fetch request (e.g. Authorization, X-API-Key) |
timeout | 5000 | Request timeout in milliseconds |
useBeacon | true | Use sendBeacon when the page is hidden |
BrowserLogDrainOptions
| Option | Default | Description |
|---|---|---|
drain | — | (required) BrowserDrainConfig object |
pipeline | { batch: { size: 25, intervalMs: 2000 }, retry: { maxAttempts: 2 } } | Pipeline configuration overrides |
autoFlush | true | Auto-register visibilitychange flush listener |
sendBeacon Fallback
useBeacon is enabled (the default) and the page becomes hidden, the drain automatically switches from fetch to navigator.sendBeacon. This ensures logs are delivered even when the user closes the tab or navigates away — no data loss on page exit.sendBeacon has a browser-imposed payload limit (~64 KB). If the payload exceeds this, the drain throws an error. Keep batch sizes reasonable (the default of 25 is well within limits).
Authentication
Pass custom headers to protect your ingest endpoint:
const drain = createBrowserLogDrain({
drain: {
endpoint: 'https://logs.example.com/v1/ingest',
headers: {
'Authorization': 'Bearer ' + token,
},
},
})
headers are applied to fetch requests only. The sendBeacon API does not support custom headers — when the page is hidden and sendBeacon is used, headers are not sent. If your endpoint requires authentication, consider validating via a session cookie (credentials: 'same-origin' is set by default) or disable sendBeacon with useBeacon: false.Server Endpoint
Your server needs a POST endpoint that accepts a DrainContext[] JSON body. Here are examples for common frameworks:
Express
app.post('/v1/ingest', express.json(), (req, res) => {
for (const entry of req.body) {
console.log('[BROWSER]', JSON.stringify(entry))
}
res.sendStatus(204)
})
Hono
app.post('/v1/ingest', async (c) => {
const body = await c.req.json()
for (const entry of body) {
console.log('[BROWSER]', JSON.stringify(entry))
}
return c.body(null, 204)
})
Full Control
Combine createBrowserDrain with createDrainPipeline for maximum flexibility:
import { initLogger, log } from 'evlog'
import type { DrainContext } from 'evlog'
import { createBrowserDrain } from 'evlog/browser'
import { createDrainPipeline } from 'evlog/pipeline'
const pipeline = createDrainPipeline<DrainContext>({
batch: { size: 100, intervalMs: 10000 },
retry: { maxAttempts: 5, backoff: 'exponential' },
maxBufferSize: 500,
onDropped: (events) => {
console.warn(`Dropped ${events.length} browser events`)
},
})
const drain = pipeline(createBrowserDrain({
endpoint: 'https://logs.example.com/v1/ingest',
timeout: 3000,
}))
initLogger({ drain })
log.info({ action: 'app_init' })
// Flush on page unload
window.addEventListener('beforeunload', () => drain.flush())
Next Steps
- Adapters Overview — Available built-in adapters
- Pipeline — Batching, retry, and buffer overflow handling
- Custom Adapters — Build your own drain function