@inso_web/els-client
v0.5.2
Published
Lightweight TypeScript client for the Error Logs Service (ELS) — capture, batch and ship errors from the browser and Node.js. Zero dependencies, retry with Retry-After support, silent fail, sendBeacon, traceId idempotency, universal (browser + Node >=18).
Downloads
1,312
Maintainers
Readme
@inso_web/els-client
Lightweight TypeScript client for the Inso Error Logs Service (ELS) — a managed SaaS for centralised event logging (debug → fatal) with AI-assisted error triage. Batches and ships events from Node.js and the browser with zero runtime dependencies.
Pino-compatible API (
info/warn/error/debug/fatal/child) — drop-in replacement for Pino, Winston, and Loki transports without an extra package.
Table of contents
- What you get
- Install
- Quick Start
- Use as a logger (Pino-compatible)
- Browser & Node patterns
- When to use the client vs the queue
- Core concepts
- Configuration
- Migration
- Versioning (
appVersion) - API
- Quick reference
- Why ELS
- Examples
- Other ELS SDKs
- Pricing
- License
What you get
ELS ships with a built-in admin dashboard. Every event captured by this SDK lands there with full-text search, faceted filtering, AI-assisted diagnosis, and version-aware regression detection.
| | |
|---|---|
|
|
|
| Virtual table with facet sidebar (app, env, version, source, level, browser, IP, category). Live mode auto-refreshes every 5s. | Full event metadata: timestamps, geo, env, app version, fingerprint, session, repetition cards, in-session correlation. |
|
|
|
| Parsed stack trace + AI-assisted diagnosis: what broke, where, how to fix. | Timeline, donuts, top URLs/IPs, hourly heatmap, version-regression widget. |
Install
npm install @inso_web/els-client
# or
pnpm add @inso_web/els-client
# or
yarn add @inso_web/els-clientRequirements: Node.js 18+ or any browser with a global fetch.
Quick Start
import { ELSClient } from '@inso_web/els-client';
// One instance per app
export const log = new ELSClient({
apiKey: process.env.ELS_API_KEY!,
appSlug: 'my-app',
serviceName: 'api',
deploymentEnv: 'PRODUCTION',
appVersion: process.env.BUILD_VERSION, // see "Versioning"
minLevel: 'info',
});
log.info('Server started on port 3000');
log.warn({ userId: 42 }, 'High request rate');
log.error(err, 'Database query failed');Each log.*(...) call ships a structured event to ELS — fire-and-forget, non-blocking, never throws.
Don't have an API key yet? Sign up at lk.insoweb.ru — takes under a minute.
Use as a logger (Pino-compatible)
ELSClient implements a Pino-compatible Logger interface. Use it as a regular logger — no extra library.
// Context-bound child loggers
const reqLog = log.child({ requestId: 'r1', userId: 42 });
reqLog.info('processing');
reqLog.error(err, 'failed');Behaviour:
- Methods are fire-and-forget. They do not return a Promise, do not throw, and never break the host app.
- Transport errors are logged to
console.error. Your code keeps running. - Anything below
minLevelis dropped before send.
Level mapping to ELS level:
| SDK method | ELS level |
|---|---|
| fatal | critical |
| error | error |
| warn | warning |
| info | info |
| debug | debug |
| trace | debug |
Browser & Node patterns
Browser: global error handlers
import { ELSClient, ELSQueue } from '@inso_web/els-client';
const client = new ELSClient({ /* ... */ });
const queue = new ELSQueue(client, {
flushIntervalMs: 5_000,
maxBatchSize: 20,
useBeacon: true, // auto-flush on pagehide via sendBeacon
});
window.addEventListener('error', (e) => {
queue.enqueue({
message: e.message,
url: location.href,
stack: e.error?.stack,
level: 'error',
source: 'client',
});
});
window.addEventListener('unhandledrejection', (e) => {
queue.enqueue({
message: String(e.reason?.message ?? e.reason),
url: location.href,
stack: e.reason?.stack,
level: 'error',
source: 'client',
});
});Node.js: process-level handlers
process.on('uncaughtException', (err) => {
void client.sendError({
message: err.message,
stack: err.stack,
url: process.argv[1] ?? 'node',
level: 'critical',
source: 'server',
});
});
process.on('unhandledRejection', (reason) => {
const err = reason instanceof Error ? reason : new Error(String(reason));
void client.sendError({
message: err.message,
stack: err.stack,
url: 'process://unhandledRejection',
level: 'error',
source: 'server',
});
});Edge runtimes / Workers
Works out of the box — the client only uses the global fetch. No Node-only APIs.
When to use the client vs the queue
| Scenario | Use |
|---|---|
| Single Node service, plenty of CPU, want fewest moving parts | ELSClient directly — log.info(...), log.error(...) |
| Browser, want to survive page unload | ELSClient + ELSQueue with useBeacon: true |
| Bursty traffic in Node, want to coalesce events | ELSClient + ELSQueue with maxBatchSize > 1 |
| Need synchronous delivery confirmation | await client.sendError(entry) (low-level path) |
| Many child contexts (per request, per user) | log.child({ ...bindings }) |
Both ELSClient and ELSQueue route through the same wire format.
Core concepts
Fire-and-forget vs delivery-confirmed
High-level methods (log.info, log.error, …) are non-blocking. They never throw and never return a Promise the caller has to await. For critical paths where you need confirmation:
try {
await client.sendError({ message: 'payment failed', level: 'critical', /* ... */ });
} catch (e) {
// network / 5xx / 429 — safe to retry from the caller
}Bindings & child loggers
const tenantLog = log.child({ tenant: 'acme', region: 'eu-west-1' });
tenantLog.info('worker started');
// Server receives meta: { tenant: 'acme', region: 'eu-west-1' }Children are cheap — create one per request, per job, per session.
Silent failure
The client never crashes the host. On transport errors it writes to console.error and drops the event (fire-and-forget). If you need durability across crashes, use ELSQueue with useBeacon: true in the browser or sleep on Node-side process.on('beforeExit', () => client.flush()).
Configuration
| Option | Type | Default | Description |
|---|---|---|---|
| apiKey | string | — | Application API key (required) |
| appSlug | string | — | Application slug (required) |
| deploymentEnv | 'DEV' \| 'STAGING' \| 'PRODUCTION' | 'DEV' | Environment marker |
| serviceName | string | — | Service / module name inside the app |
| appVersion | string | — | App version (any format, ≤128 chars) |
| timeout | number | 10000 | HTTP request timeout, ms |
| retries | number | 3 | Retries on network errors and 429 |
| authHeader | 'bearer' \| 'x-api-key' | 'bearer' | Key transport header |
| minLevel | LogLevel | 'info' | Minimum level to send |
| loggerDefaults | Record<string, unknown> | {} | Default fields injected into every log |
Migration
From Pino
@inso_web/els-client exposes the same info / warn / error / debug / fatal / child API surface — switching is one line per file.
Before:
import pino from 'pino';
const log = pino({
level: 'info',
transport: { target: 'pino-pretty' },
});
log.info({ userId: 42 }, 'user fetched');
log.error(err, 'query failed');
const reqLog = log.child({ requestId: 'r1' });After:
import { ELSClient } from '@inso_web/els-client';
const log = new ELSClient({
apiKey: process.env.ELS_API_KEY!,
appSlug: 'my-app',
minLevel: 'info',
});
log.info({ userId: 42 }, 'user fetched');
log.error(err, 'query failed');
const reqLog = log.child({ requestId: 'r1' });| Pino concept | ELS equivalent | Notes |
|---|---|---|
| pino({ level: 'info' }) | new ELSClient({ minLevel: 'info' }) | Same priority ordering |
| log.info(obj, msg) | log.info(obj, msg) | Identical signature |
| log.child(bindings) | log.child(bindings) | Bindings merge into meta |
| pino.transport({ target: 'pino-loki' }) | Not needed | Built-in HTTP transport |
| pretty printing | Use console.log in dev | ELS is a remote sink, not a TTY printer |
Gotchas:
- ELS sends to the network; Pino's TTY-pretty output has no equivalent. Keep
console.logfor local dev. pino.multistreamhas no direct mapping — capture to ELS andconsole.logseparately if you need both.- The standard
serializersoption is not supported — pre-shape objects in your code or useBeforeSend.
From Winston
Before:
import winston from 'winston';
const log = winston.createLogger({
level: 'info',
format: winston.format.json(),
defaultMeta: { service: 'api' },
transports: [
new winston.transports.Console(),
new winston.transports.Http({ host: 'logs.example.com', path: '/' }),
],
});
log.info('user logged in', { userId: 42 });
log.error('payment failed', { error: err });After:
import { ELSClient } from '@inso_web/els-client';
const log = new ELSClient({
apiKey: process.env.ELS_API_KEY!,
appSlug: 'my-app',
serviceName: 'api',
minLevel: 'info',
loggerDefaults: { service: 'api' },
});
log.info({ userId: 42 }, 'user logged in');
log.error(err, 'payment failed');| Winston concept | ELS equivalent | Notes |
|---|---|---|
| winston.createLogger({ level }) | new ELSClient({ minLevel }) | Same idea |
| defaultMeta | loggerDefaults | Merged into every event |
| transports: [...] | Built-in HTTP transport | One destination, no plugin packages |
| winston.format.json() | Always JSON on the wire | Format choice is not exposed |
| child(bindings) | child(bindings) | Same behaviour |
Gotchas:
- Winston accepts positional
(message, meta). ELS follows Pino's(meta, message). Re-order arguments at call sites. - Console output is not multiplexed — keep
console.logseparately if you want both. winston-daily-rotate-filehas no analogue — that role is server-side in ELS.
From Bunyan
Before:
import bunyan from 'bunyan';
const log = bunyan.createLogger({
name: 'api',
level: 'info',
streams: [{ stream: process.stdout }],
});
log.info({ userId: 42 }, 'fetched');
const reqLog = log.child({ reqId: 'r1' });After:
import { ELSClient } from '@inso_web/els-client';
const log = new ELSClient({
apiKey: process.env.ELS_API_KEY!,
appSlug: 'api',
minLevel: 'info',
});
log.info({ userId: 42 }, 'fetched');
const reqLog = log.child({ reqId: 'r1' });| Bunyan concept | ELS equivalent | Notes |
|---|---|---|
| bunyan.createLogger({ name }) | new ELSClient({ appSlug, serviceName }) | name ≈ serviceName |
| level: 'info' | minLevel: 'info' | Same string values |
| streams: [...] | Not needed | HTTP transport built-in |
| child(bindings) | child(bindings) | Identical |
Gotchas:
- Bunyan's
bunyanCLI for pretty-printing JSON files does not apply — events live in the ELS dashboard. - Custom serializers (
serializers.req,serializers.err) have no direct option — shape objects before logging, or useBeforeSend.
From console.log
Before:
console.log('User logged in', userId);
console.warn('Rate limit close', { current, limit });
console.error('Payment failed', err);After:
import { ELSClient } from '@inso_web/els-client';
const log = new ELSClient({
apiKey: process.env.ELS_API_KEY!,
appSlug: 'my-app',
});
log.info({ userId }, 'User logged in');
log.warn({ current, limit }, 'Rate limit close');
log.error(err, 'Payment failed');| console method | ELS method | Notes |
|---|---|---|
| console.log / info | log.info | |
| console.warn | log.warn | |
| console.error | log.error | First arg can be Error or string |
| console.debug | log.debug | Below default minLevel: 'info' — bump to capture |
Gotchas:
- Variadic positional formatting (
'%s %d') is not supported. Convert to structured fields ({ userId, count }). - Keep
console.*for local dev / ephemeral debug; remote events should go via the client.
From @sentry/node
Before:
import * as Sentry from '@sentry/node';
Sentry.init({
dsn: 'https://[email protected]/1',
environment: 'production',
release: process.env.BUILD_VERSION,
});
Sentry.captureException(err);
Sentry.captureMessage('payment timeout', 'warning');
Sentry.setUser({ id: '42', email: '[email protected]' });After:
import { ELSClient } from '@inso_web/els-client';
const client = new ELSClient({
apiKey: process.env.ELS_API_KEY!,
appSlug: 'my-app',
deploymentEnv: 'PRODUCTION',
appVersion: process.env.BUILD_VERSION,
});
client.error(err);
client.warn('payment timeout');
client.child({ user: { id: '42', email: '[email protected]' } });| Sentry concept | ELS equivalent | Notes |
|---|---|---|
| dsn | apiKey + appSlug | DSN is split into three explicit fields |
| environment | deploymentEnv | Same idea, fixed enum |
| release | appVersion | Any string ≤128 chars |
| captureException(err) | client.error(err) | |
| captureMessage(msg, level) | client.<level>(msg) | Pick a method per level |
| setUser({ id, email }) | client.child({ user: { id, email } }) | Or pass via loggerDefaults |
| beforeSend | BeforeSend (on ELSQueue) | Same role |
| Source maps upload | Not provided | If critical — keep Sentry alongside |
| Performance / tracing | Not provided | ELS focuses on logging, not APM |
Gotchas:
- ELS does not symbolicate via source maps. Ship readable stacks (preserve names through bundlers) or pair with another tool.
- Breadcrumbs have no direct concept — use child loggers per request to carry context.
- Sentry's
scopepush/pop pattern maps to ephemeralchildloggers.
From pino-loki
Before:
import pino from 'pino';
import { pinoLoki } from 'pino-loki';
const log = pino({
level: 'info',
}, pinoLoki({ host: 'http://loki.internal:3100' }));After:
import { ELSClient } from '@inso_web/els-client';
const log = new ELSClient({
apiKey: process.env.ELS_API_KEY!,
appSlug: 'my-app',
minLevel: 'info',
});| pino-loki concept | ELS equivalent | Notes |
|---|---|---|
| Loki labels | appSlug, serviceName, deploymentEnv, fields in meta | All of the above are queryable in the dashboard |
| Batching options | Built-in (ELSQueue) | Defaults are sane; tune if needed |
Gotchas:
- Loki is label-cardinality-sensitive; ELS is not — you can put high-cardinality user IDs in
metafreely. - LogQL has no equivalent — ELS uses a faceted UI with full-text search over
message,metakeys.
Versioning (appVersion)
The appVersion field powers ELS analytics for regression detection — “which errors first appeared in the latest release.”
new ELSClient({
// ...
appVersion: process.env.BUILD_VERSION, // or import.meta.env.VITE_BUILD_VERSION
});ELS accepts any string up to 128 characters and auto-detects the format:
| Type | Examples |
|---|---|
| date-compact | 20260507120000 |
| semver | 1.2.3, 1.0.0-rc.1, 2.0.0+build.123 |
| calver | 2026.05, 26.05.07, 2026.5.7 |
| date-iso | 2026-05-07, 2026-05-07T12:00:00Z |
| git-sha | a1b2c3d, a1b2c3d4e5f6... |
| prefixed | v1.2.3, release-2026.05, main-a1b2c3d |
| opaque | production, nightly, customLabel |
The analytics layer sorts timelines semantically within one format and falls back to min(receivedAt) per version when the dataset mixes formats.
Recommended setup — set BUILD_VERSION=$(date -u +%Y%m%d%H%M%S) in CI. Lexicographic = chronological, always unique, readable.
API
class ELSClient implements Logger {
constructor(config: ELSConfig);
// High-level (Pino-compatible)
fatal(obj: object | string | Error, msg?: string, ...args: unknown[]): void;
error(obj: object | string | Error, msg?: string, ...args: unknown[]): void;
warn(obj: object | string, msg?: string, ...args: unknown[]): void;
info(obj: object | string, msg?: string, ...args: unknown[]): void;
debug(obj: object | string, msg?: string, ...args: unknown[]): void;
trace(obj: object | string, msg?: string, ...args: unknown[]): void;
child(bindings: Record<string, unknown>): Logger;
flush(): Promise<void>;
// Low-level
sendError(entry: ErrorEntry): Promise<void>;
sendBatch(entries: ErrorEntry[]): Promise<BatchResult | null>;
}
class ELSQueue {
constructor(client: ELSClient, opts?: QueueOptions);
enqueue(entry: ErrorEntry): void;
flush(): Promise<void>;
stop(): void;
}Types ErrorEntry, ErrorLevel, ErrorSource, DeploymentEnv, BatchResult, QueueOptions are exported.
Quick reference
| Need | Use |
|---|---|
| Quick Node capture | new ELSClient({ apiKey, appSlug }) |
| Browser with page-unload safety | ELSClient + ELSQueue({ useBeacon: true }) |
| Bursty Node service | ELSClient + ELSQueue({ maxBatchSize: 50 }) |
| Per-request bindings | log.child({ requestId, userId }) |
| Delivery confirmation | await client.sendError(entry) |
| Suppress noisy levels | minLevel: 'warn' |
| Mask PII before send | ELSQueue({ beforeSend: e => mask(e) }) |
| Health probe | await client.health() |
| Graceful Node shutdown | process.on('beforeExit', () => client.flush()) |
Why ELS
ELS for Node.js is a focused logging SaaS, not a full observability suite. It optimises for capture speed, AI-driven triage, and a low integration cost.
- Lower weight. ~3 KB gzip in the browser, single dependency-free package on Node — no transitive deps.
- Zero external API calls. Only
POST /errors[/batch]andGET /health. No trackers, no manifests. - AI-assisted diagnosis on every stack trace, out of the box — no add-ons, no extra setup.
- 5-minute integration. Install → set API key → done. Same wire format works for
.NET,JVM,Gowhen you expand. - Predictable price. Tariffs live in the dashboard, not in a per-event spreadsheet.
Detailed comparison
| Category | ELS | Sentry | Datadog / New Relic | Grafana Loki | LogRocket / Logtail / BetterStack | |---|---|---|---|---|---| | Hosting model | Managed SaaS | SaaS or self-hosted | SaaS only | Self-hosted / Grafana Cloud | SaaS | | SDK runtime deps | Zero | Medium (sub-SDKs, integrations) | Heavy (agent + tracing) | Promtail / agent | Medium | | Typical integration time | ~5 min | 10–20 min | 30–60 min | Hours to days | 10–20 min | | AI-assisted triage | Built-in | Paid add-on | Paid add-on | None | None | | Error grouping / fingerprint | Yes | Yes | Yes | Manual via LogQL | Partial | | Source-map upload | No | Yes | Yes | n/a | Partial | | Session replay (frontend) | No | Paid | Paid | n/a | Yes (core) | | Distributed tracing / APM | No | Partial | Yes (core) | Yes with Tempo | No | | Infrastructure metrics | No | No | Yes (core) | Yes with Mimir | No | | Free tier log retention | 24 hours | 30 days (limited volume) | Trial only | Self-cost | 3–30 days | | Russian-language support / docs | Native | Community | Limited | Community | None |
When ELS is the wrong choice
- You need a single vendor for APM + logs + metrics under one bill — go Datadog or New Relic.
- Your frontend bug triage relies on DOM session replay — go LogRocket or Sentry Replay.
- You ship a public mobile app and need crash symbolication + ANR detection — Firebase Crashlytics or Sentry Mobile.
For everything else — backend errors, frontend JS errors, request logs, structured app events with version-aware analytics — ELS is built to be the cheapest path to a working dashboard.
→ Sign up at lk.insoweb.ru to grab an API key.
Examples
Runnable samples in ./examples/:
node-basic— minimal Node sendernode-batch— batch + queue with graceful shutdownbrowser-vanilla— esm.sh CDN HTML page
Other ELS SDKs
Same wire format, same dashboard — pick by stack.
Node.js family
@inso_web/els-client— base TS / Node / browser client (this repo)@inso_web/els-express— Express middleware@inso_web/els-next— Next.js helpers (App + Pages router)@inso_web/els-nest— NestJS module@inso_web/els-react— React Provider, hooks, ErrorBoundary@inso_web/els-vue— Vue 3 plugin
Other stacks
Inso.Els— .NET (Core + ASP.NET Core + ILogger)io.github.official-inso:els-core— Java + Spring Boot starter + SLF4Jgithub.com/official-inso/els-go— Go
Pricing
Free tier — 24-hour log retention. See lk.insoweb.ru for the full tariff matrix.
License
MIT © INSOWEB
