Stop wiring together a logger, an analytics SDK, and an error tracker.
emit-io unifies all three behind a single, zero-dependency API that works identically
on Node.js, Cloudflare Workers, browser, and React Native.
Logs · Analytics · Errors · One API · Node · Edge · Browser · React Native · Zero deps · Fully typed
- Replace 3 tools with 1 — logger + analytics SDK + error tracker behind one typed object. No config drift, no duplicated consent flows.
- Write once, run anywhere — identical API on Node.js, Cloudflare Workers, browser, and React Native.
- Catch event typos at compile time —
EventRegistryaugmentation makes everyemit.event()call fully type-checked. - GDPR-ready by default — built-in consent gate (
{ analytics, errors }) blocks providers until the user opts in. Pre-init buffer replays queued events on consent. - Governed analytics events — define events in YAML, auto-generate typed trackers, detect schema drift in CI.
npm install emit-io-core→ Jump to Quick Start or skip to the full comparison.
- Why emit-io?
- Ecosystem
- Packages
- Features
- Quick Start
- Transports
- Plugins
- Codegen
- OpenTelemetry
- Type-Safe Events
- Child Loggers + ALS Context
- Consent Gate
- Circuit Breaker
- Examples
- Benchmarks
- Migration v2 → v3
- Contributing
- License
The usual pattern: one library for logs, another for product events, another for errors. Three SDK versions to track, three consent flows to wire up, three runtime compatibility matrices to worry about — and a Sentry dsn, a Segment writeKey, and a pino instance living as separate globals across your codebase.
Most TypeScript stacks bolt together three separate systems: a logger (pino/winston) for structured logs, an analytics SDK (Segment/PostHog/GA4) for product events, and an error tracker (Sentry) for crashes — each with its own API, configuration, consent model, and runtime compatibility matrix.
emit-io collapses all three into one type-safe API that runs identically on Node.js, Cloudflare Workers, browser, and React Native:
| pino | winston | Sentry | Segment | emit-io | |
|---|---|---|---|---|---|
| Structured logs (DEBUG→FATAL) | ✅ | ✅ | — | — | ✅ |
| Product analytics events | — | — | — | ✅ | ✅ |
| Error capture | — | — | ✅ | — | ✅ |
| Type-safe event registry | — | — | — | — | ✅ |
| GDPR consent gate | — | — | partial | partial | ✅ |
| YAML-driven codegen | — | — | — | — | ✅ |
| Runs on edge / Cloudflare Workers | — | — | partial | — | ✅ |
| Runs on React Native | — | — | ✅ | ✅ | ✅ |
| Zero runtime deps (core) | — | — | — | — | ✅ |
| Pre-init analytics buffer | — | — | — | — | ✅ |
| Circuit breaker for providers | — | — | — | — | ✅ |
Note: pino and winston are best-in-class loggers; Sentry and Segment are category leaders in error tracking and analytics. This table shows breadth — one API covering all four areas — not depth in any single niche.
import { EmitIoStrategy, JSONTransport, ConsoleTransport, redact, sample, LogLevelEnum } from 'emit-io-core'
const emit = new EmitIoStrategy({
transports: [
new ConsoleTransport({ minLevel: LogLevelEnum.DEBUG, pretty: true }),
new JSONTransport({ minLevel: LogLevelEnum.INFO }),
],
plugins: [
redact({ paths: ['password', 'user.token', '*.secret'] }),
sample({ rate: 0.1, levels: [LogLevelEnum.DEBUG] }),
],
consent: { analytics: true, errors: true },
})
emit.info('Server started', { port: 3000 })
emit.warn('Slow query', { ms: 1200 })
emit.captureError('Auth', 'login_failed', false, new Error('bad token'))
const reqLog = emit.child({ requestId: 'abc-123' })
reqLog.info('handling request')
import { runWithContext } from 'emit-io-core'
await runWithContext({ traceId: 'tx' }, async () => {
reqLog.info('inside trace') // context auto-merged
})emit-io-core ← zero-dep core: transports, plugins, providers, ALS context
├── emit-io-react React hooks + AnalyticsProvider + server actions
├── emit-io-react-native Expo/RN hooks + AppState + navigation tracking
├── emit-io-next Next.js middleware + instrumentRoute
├── emit-io-fastify Fastify plugin + per-request child logger
├── emit-io-hono Hono middleware + ALS propagation
├── emit-io-otel OTel spans (OTelProvider) + OTel logs (OTelTransport)
└── emit-io-codegen CLI: YAML events → typed TS tracker + drift detection
All 8 packages share a single linked version and are released together on every push to main.
Logging
- Log levels — DEBUG, INFO, WARN, ERROR, FATAL with per-transport
minLevelfiltering - Transport system —
ConsoleTransport,JSONTransport,HTTPTransport(batched, retried),DevToolsTransport; fully pluggable - Plugin pipeline —
(entry) => entry | nullfunctions run before every transport:redact,sample,rateLimit,normalizeStack - Child loggers —
emit.child({ requestId })inherits all transports and providers, merges bindings - AsyncLocalStorage context —
runWithContextauto-propagates trace data to every log call in scope (Node + edge) - Zero runtime dependencies in
emit-io-core
Analytics & errors
- Bring-your-own providers — implement the
AnalyticsProviderinterface once (GA4, PostHog, Sentry, custom…) and emit-io fans out to all of them viaevent(),logScreen(),setUser(),captureError() - Type-safe events —
EventRegistrymodule augmentation gives compile-time-checked event names + payloads; falls back to loose strings without augmentation captureError— writes to transports always (consent-independent) and fires analytics providers only ifconsent.errors !== false- Consent gate —
setConsent({ analytics, errors })for GDPR; queued events replay on consent via pre-init buffer - Pre-init buffer — analytics events queued before
init()flush automatically when providers are ready - Circuit breaker — wraps any flaky provider; opens after N failures, self-recovers after cooldown
npm install emit-io-coreSee npm package page for versions and stats.
import { EmitIoStrategy, ConsoleTransport, JSONTransport, LogLevelEnum } from 'emit-io-core'
const emit = new EmitIoStrategy({
transports: [
new ConsoleTransport({ minLevel: LogLevelEnum.DEBUG }),
new JSONTransport({ minLevel: LogLevelEnum.INFO }),
],
})
emit.debug('query', { sql: 'SELECT 1' })
emit.info('started', { port: 3000 })
emit.warn('retrying', { attempt: 2 })
emit.error('db down', { host: 'pg-primary' })
emit.fatal('out of memory')
// Analytics error (fires providers + writes to transport)
emit.captureError('Payments', 'charge_failed', true, err, { orderId: 'x' })
// Analytics event
emit.event('purchase', { total: 99 })
// Feature info (analytics)
emit.logFeature('Auth', 'login_success', { method: 'oauth' })npm install emit-io-core emit-io-reactimport { AnalyticsProvider, useAnalytics, usePageTracking } from 'emit-io-react'
import { EmitIoStrategy } from 'emit-io-core'
const emit = new EmitIoStrategy({ /* ... */ })
function App() {
return (
<AnalyticsProvider client={emit} autoTrack>
<Routes />
</AnalyticsProvider>
)
}
function ProductPage() {
usePageTracking('/products')
const { event } = useAnalytics()
return <button onClick={() => event('add-to-cart', { id: '1' })}>Add</button>
}npm install emit-io-core emit-io-next// middleware.ts
import { withLogger } from 'emit-io-next'
import { NextResponse } from 'next/server'
import { emit } from './lib/emit'
export default withLogger(
async (req) => NextResponse.next(),
{ logger: emit, trackPageviews: true }
)
// app/api/orders/route.ts
import { instrumentRoute } from 'emit-io-next'
import { emit } from './lib/emit'
export const GET = instrumentRoute(
async (req) => Response.json({ ok: true }),
{ logger: emit, eventName: 'get-orders' }
)npm install emit-io-core emit-io-react-nativeimport { AnalyticsProvider, useAnalytics, useScreenTracking } from 'emit-io-react-native'
import { EmitIoStrategy } from 'emit-io-core'
const emit = new EmitIoStrategy({ /* ... */ })
export default function App() {
return (
<AnalyticsProvider client={emit} trackAppState>
<RootStack />
</AnalyticsProvider>
)
}
function HomeScreen() {
const { event } = useAnalytics()
useScreenTracking('Home')
return <Button onPress={() => event('cta-click')} title="Go" />
}All transports support enabled?: boolean (default true). Toggle at runtime via feature flags:
const devtools = new DevToolsTransport({ enabled: false }) // off in production
// later: devtools.enabled = true // enable for debugging| Transport | Description |
|---|---|
ConsoleTransport |
Pretty or plain console output |
JSONTransport |
NDJSON to stdout (or custom write) |
HTTPTransport |
Batched POST with retry + exponential backoff |
DevToolsTransport |
WebSocket to devtools panel; buffers while disconnected |
import { HTTPTransport } from 'emit-io-core'
new HTTPTransport({
url: 'https://logs.example.com/ingest',
minLevel: LogLevelEnum.WARN,
enabled: process.env.NODE_ENV === 'production',
batchSize: 50,
flushIntervalMs: 5000,
maxRetries: 3,
headers: { Authorization: 'Bearer token' },
})import { redact, sample, rateLimit, normalizeStack } from 'emit-io-core'
new EmitIoStrategy({
plugins: [
redact({ paths: ['password', 'user.token', '*.secret'] }),
sample({ rate: 0.05, levels: [LogLevelEnum.DEBUG] }),
rateLimit({ max: 100, windowMs: 1000 }),
normalizeStack({ maxFrames: 10 }),
],
})Plugins are plain functions (entry: LogEntry) => LogEntry | null. Return null to drop the entry.
The standout differentiator: define your analytics events once in YAML, get a fully typed TypeScript tracker, automatic CI drift detection, and JSON Schema / Avro export.
npm install -D emit-io-codegen1. Define events in YAML:
# events.yml
events:
purchase:
description: User completes a purchase
properties:
orderId:
type: string
required: true
total:
type: number
required: true
currency:
type: string
pii: false
page_view:
description: Page viewed
properties:
path:
type: string
required: true2. Generate the typed tracker:
npx emit-io-codegen generate --input events.yml --output src/tracker.tsThis produces a tracker.ts with full EventRegistry module augmentation — so every emit.event('purchase', ...) call is type-checked against your YAML schema.
3. Detect schema drift in CI:
npx emit-io-codegen check --input events.yml --output src/tracker.ts
# Exits non-zero if generated code is out of sync — use in pre-commit or CI4. Export to JSON Schema or Avro:
npx emit-io-codegen export --format json-schema --input events.yml
npx emit-io-codegen export --format avro --input events.ymlPII detection: the codegen flags properties marked pii: true, letting you enforce redaction rules per event in your pipeline.
See examples/15-codegen-workflow.md for a full end-to-end example.
Two complementary paths — use one or both:
import { EmitIoStrategy, ConsoleTransport, LogLevelEnum } from 'emit-io-core'
import { OTelTransport, OTelProvider } from 'emit-io-otel'
const emit = new EmitIoStrategy({
transports: [
new ConsoleTransport({ minLevel: LogLevelEnum.INFO }),
new OTelTransport({ loggerProvider }), // logs → OTel LogRecords
],
providers: [
new OTelProvider(), // events → OTel spans
],
})
// These go to both ConsoleTransport and OTelTransport (as LogRecords)
emit.info('request processed', { status: 200 })
emit.error('db timeout', { host: 'pg-1' })
// These go ONLY to OTelProvider (as spans)
emit.event('purchase', { total: 99 })
emit.logScreen('/checkout')
emit.captureError('Payments', 'charge', true, err)| Export | What | For |
|---|---|---|
OTelTransport |
Each log call → otelLogger.emit() (LogRecord) |
Grafana Loki, Elastic, any OTLP log backend |
OTelProvider |
Each event/screen/error → tracer.startSpan() (Span) |
Grafana Tempo, Jaeger, Honeycomb |
declare module 'emit-io-core' {
interface EventRegistry {
'purchase': { orderId: string; total: number }
'page-view': { path: string }
}
}
emit.event('purchase', { orderId: 'x', total: 99 }) // typed
emit.event('unknown', {}) // TS error// Child logger — inherits all transports and providers, adds bindings
const reqLog = emit.child({ requestId: 'abc-123', userId: 'u-1' })
reqLog.info('request received') // context: { requestId, userId }
// AsyncLocalStorage — auto-merges into every log call in scope
import { runWithContext } from 'emit-io-core'
await runWithContext({ traceId: 'trace-abc' }, async () => {
await processOrder() // all logs inside get traceId automatically
})const emit = new EmitIoStrategy({
consent: { analytics: false, errors: true },
})
// Later, after user consent:
emit.setConsent({ analytics: true })
emit.getConsent() // { analytics: true, errors: true }analytics: false blocks event(), logFeature(), logScreen(), setUser().
errors: false still writes the error to transports but skips analytics providers.
import { circuitBreaker } from 'emit-io-core'
import { PostHogProvider } from './providers/posthog'
const safePostHog = circuitBreaker(new PostHogProvider(), {
failureThreshold: 5,
cooldownMs: 30_000,
onStateChange: (state, name) => console.warn(`[circuit] ${name}: ${state}`),
})
new EmitIoStrategy({ providers: [safePostHog] })Runnable apps per integration in examples/:
| App | Stack |
|---|---|
| next-app | Next.js 15 App Router + emit-io-next (middleware + instrumentRoute) |
| fastify-server | Fastify 5 + emit-io-fastify (plugin + per-request child logger) |
| hono-worker | Cloudflare Worker (Hono 4) + HTTPTransport remote ingest |
| otel-bridge | Node + NodeSDK + OTLP exporters + OTelProvider/OTelTransport |
| react-vite | Vite + React 19 SPA + emit-io-react hooks + provider |
| react-native-expo | Expo SDK 52 + expo-router + emit-io-react-native |
Single-concept snippets in examples/ (files 01-...15-): redact, child loggers, ALS context, HTTP transport, consent, circuit breaker, event registry, codegen workflow.
~1.2M ops/s on Node v22 arm64 (simple info() + JSONTransport to /dev/null). Runs ~2× behind pino in raw throughput; roughly on par with winston in simple scenarios, faster in context-heavy ones. See BENCHMARKS.md for the full breakdown.
See CHANGELOG.md and docs/MIGRATION_v2_to_v3.md for the complete list. Key changes:
- emit.error('Auth', 'login_failed', true, err)
+ emit.captureError('Auth', 'login_failed', true, err)
- emit.info('Auth', 'login_success', { method: 'oauth' })
+ emit.logFeature('Auth', 'login_success', { method: 'oauth' })
- declare global { interface EVENT_TAGS { ... } }
+ declare module 'emit-io-core' { interface EventRegistry { ... } }PRs welcome. Run bun install && bun run test from the repo root.
This repo includes a Claude Code skill with project conventions, build patterns, and common workflows. It's auto-discovered when working in this directory — no setup needed. Just clone and open.
MIT