monitor-api - v1.3.0
    Preparing search index...

    monitor-api - v1.3.0

    monitor-api

    npm version npm downloads bundle size License: MIT TypeScript GitHub issues GitHub stars

    Lightweight, signal-based web app monitoring library.
    Captures FPS, JS heap, long tasks, Web Vitals, network requests, React renders, and custom events — all reactive via ssignal.

    • Signal-based — subscribe to exactly what you need, no polling
    • 5 collectors — Performance, Network, React, Events, Web Vitals
    • Web Vitals — CLS, FCP, INP, LCP, and TTFB via web-vitals
    • React integrationuseSignal, usePerformance, useNetwork, useReact, useEvents, useWebVitals
    • Zero config — works out of the box, tree-shakeable
    • SSR safe — browser collectors no-op outside the browser
    • Production-ready lifecyclestart() is idempotent and stop() restores runtime patches
    • TypeScript-first — fully typed, zero any in the public API
    • Small runtime — depends on ssignal and web-vitals
    npm install monitor-api
    
    import { createMonitor } from 'monitor-api'

    const monitor = createMonitor()
    monitor.start()

    // Subscribe to FPS changes
    monitor.performance.fps.subscribe((fps) => {
    console.log('FPS:', fps)
    })

    // Subscribe to full performance snapshot
    monitor.performance.snapshot.subscribe((snap) => {
    console.log('Performance snapshot:', snap)
    // { fps: 60, fpsHistory: [...], memory: { used: 45.2, total: 2048, percent: 2.2 }, ... }
    })

    Creates and returns a Monitor instance. Does not start collecting — call monitor.start() explicitly.

    import { createMonitor } from 'monitor-api'

    const monitor = createMonitor({
    collectors: ['performance', 'network', 'react', 'events', 'webVitals'], // default: all
    sampleRate: 1, // per-monitor sampling probability from 0 to 1 (default: 1)
    maxHistory: 120, // data points kept per metric; 0 disables history (default: 120)
    networkFilter: (url) => !url.includes('analytics'), // optional
    env: 'development', // 'development' | 'production' (default: 'development')
    })

    monitor.start() // start all collectors
    monitor.stop() // pause (keeps data)
    monitor.destroy() // stop + dispose all signals

    monitor.start() is idempotent. Calling it more than once does not duplicate event listeners, network patches, or React commit hooks.


    monitor-api is designed to run in development, staging, and production browser apps.

    • Importing and creating a monitor is SSR-safe.
    • Browser collectors no-op when window is unavailable.
    • Collection starts only after monitor.start().
    • monitor.stop() and monitor.destroy() restore patched browser APIs.
    • Multiple monitor instances share network and React global hooks; the last active instance restores them.
    • Histories are bounded by maxHistory.
    • Custom event payloads are copied before retention and bounded by depth and UTF-8 byte size.
    • Production reporting starts only after monitor.start() and requires either fetch or a custom transport.

    For production apps, prefer a conservative maxHistory, select only the collectors you need, and use report.transform to send a compact payload.


    Captures FPS, JS heap memory, Long Tasks, and Cumulative Layout Shift (CLS).

    monitor.start()

    // Granular signals — subscribe to only what you need
    monitor.performance.fps.subscribe((fps) => {
    console.log('Current FPS:', fps)
    })

    monitor.performance.memory.subscribe((mem) => {
    if (mem) {
    console.log(`Memory: ${mem.used}MB / ${mem.total}MB (${mem.percent}%)`)
    } else {
    console.log('Memory API not available (non-Chrome browser)')
    }
    })

    monitor.performance.longTasks.subscribe(({ count, lastDuration }) => {
    console.log(`Long tasks: ${count} total, last was ${lastDuration}ms`)
    })

    monitor.performance.cls.subscribe((cls) => {
    console.log('Cumulative Layout Shift:', cls.toFixed(4))
    })

    // Or subscribe to the full snapshot
    monitor.performance.snapshot.subscribe((snap) => {
    console.log('Performance snapshot:', JSON.stringify(snap, null, 2))
    /*
    {
    fps: 58,
    fpsHistory: [60, 59, 58],
    memory: { used: 45.2, total: 2048, percent: 2.2 },
    memoryHistory: [2.1, 2.2, 2.2],
    longTasks: { count: 3, lastDuration: 82.5 },
    cls: 0.0023
    }
    */
    })

    // Utilities
    monitor.performance.clearHistory() // reset fpsHistory + memoryHistory

    Snapshot shape:

    interface PerformanceSnapshot {
    fps: number
    fpsHistory: number[]
    memory: { used: number; total: number; percent: number } | null
    memoryHistory: number[]
    longTasks: { count: number; lastDuration: number | null }
    cls: number
    }

    Note: memory is null on non-Chrome browsers. actualDuration for React components requires a dev build or react-dom/profiling in production.


    Intercepts fetch and XMLHttpRequest transparently. stop()/destroy() restore the original fetch, XMLHttpRequest.prototype.open, and XMLHttpRequest.prototype.send implementations.

    Response bodies are never cloned or consumed for instrumentation. payloadSize uses a valid Content-Length header when available; XHR can also use the byteLength of an already-materialized ArrayBuffer. Otherwise it reports 0 to represent an unknown size.

    While the collector is running, window5s updates when requests enter and leave the five-second window. A one-shot timer targets only the next expiration; it is released by stop(), destroy(), or clearLog().

    monitor.start()

    // Fire on every new request
    monitor.network.onRequest.subscribe((entry) => {
    if (!entry) return
    console.log(`[${entry.initiator.toUpperCase()}] ${entry.method} ${entry.url}`)
    console.log(` Status: ${entry.status} | Latency: ${entry.latency}ms | Size: ${entry.payloadSize} bytes`)
    if (entry.error) console.warn(' Error:', entry.error)
    })

    // Full snapshot with rolling log + 5-second window metrics
    monitor.network.snapshot.subscribe((snap) => {
    const { window5s } = snap
    console.log(`Last 5s: ${window5s.count} requests, avg latency ${window5s.avgLatency}ms, error rate ${(window5s.errorRate * 100).toFixed(1)}%`)
    console.log('All entries:', snap.entries)
    })

    // Dynamic filter
    monitor.network.setFilter((url) => !url.includes('/health'))

    // Clear log
    monitor.network.clearLog()

    Entry shape:

    interface NetworkEntry {
    id: string
    url: string
    method: string // 'GET' | 'POST' | ...
    status: number // 0 if network error
    latency: number // ms
    payloadSize: number // known response bytes; 0 when unknown
    requestSize: number // request body bytes
    initiator: 'fetch' | 'xhr'
    timestamp: number // Date.now()
    error: string | null
    }

    Hooks into window.__REACT_DEVTOOLS_GLOBAL_HOOK__ to capture React renders without touching the component tree.

    Compatible with React 18 and React 19, dev and production builds.

    monitor.start()

    // Fire on every commit batch
    monitor.react.onCommit.subscribe((entry) => {
    if (!entry) return
    console.log(`[React] ${entry.type} <${entry.component}> — ${entry.duration}ms`)
    })

    // Full snapshot with per-component aggregation over retained history
    monitor.react.snapshot.subscribe((snap) => {
    console.log(`Total commits: ${snap.totalCommits}`)
    console.log(`Truncated commits: ${snap.truncatedCommits}`)

    console.log('Slow components (>16ms):')
    snap.slowComponents.forEach((e) => {
    console.log(` <${e.component}> ${e.duration}ms [${e.type}]`)
    })

    console.log('By component:')
    Object.entries(snap.byComponent).forEach(([name, stats]) => {
    console.log(` ${name}: ${stats.renders} renders, avg ${stats.avgDuration}ms`)
    })
    })

    // Adjust slow threshold
    monitor.react.setSlowThreshold(8) // flag components slower than 8ms

    monitor.react.clearLog()

    Traversal is iterative and visits at most 10,000 fibers per commit by default. Set maxFiberVisits to another limit (Infinity disables the cap). Commits that reach the cap increment truncatedCommits. Fibers with actualDuration <= 0 are ignored unless includeZeroDuration: true is configured. Unmounts come from the dedicated React DevTools hook rather than private Fiber flags. They remain in entries/onCommit, but do not inflate render aggregates or slowComponents.

    Render entry shape:

    interface RenderEntry {
    component: string // displayName or function.name
    duration: number // ms (actualDuration — 0 in prod without profiling build)
    timestamp: number
    type: 'mount' | 'update' | 'unmount'
    commitId: number
    }

    Tip: profiling duration is unavailable in standard production builds. Use react-dom/profiling for production measurements, or opt into zero-duration entries explicitly when only component/phase information is needed.


    Custom event bus. The app can emit events without importing the library.

    Emitting events:

    // Option A — import the helper
    import { emitMonitorEvent } from 'monitor-api'

    emitMonitorEvent('user:login', { userId: 42 })
    emitMonitorEvent('route:change', { from: '/home', to: '/settings' })
    emitMonitorEvent('error:caught', { message: 'Network timeout' })

    // Or record directly through the typed monitor facade
    monitor.events.emit('checkout:complete', { total: 49.99 })

    // Option B — native CustomEvent (no import needed)
    window.dispatchEvent(new CustomEvent('app:monitor:event', {
    detail: { label: 'cache:miss', data: { key: 'user_profile' } }
    }))

    Subscribing:

    monitor.start()

    // Fire on each event
    monitor.events.onEvent.subscribe((event) => {
    if (!event) return
    console.log(`[Event] ${event.label}`, event.data)
    })

    // Full snapshot with count by label
    monitor.events.snapshot.subscribe((snap) => {
    console.log('Event log:', snap.entries)
    console.log('Counts by label:', snap.byLabel)
    // { 'user:login': 3, 'route:change': 7, 'error:caught': 1 }
    })

    monitor.events.clearLog()

    Event data is retained as a serializable copy. Malformed, circular, or oversized data is recorded as null, while malformed events without a non-empty, bounded string label are ignored. The defaults allow 256 characters per label, up to 5 nested object/array levels, and 16 KiB per payload; all limits can be overridden in the collector config.


    Collects standard Web Vitals metrics using the web-vitals package:

    • CLS — Cumulative Layout Shift
    • FCP — First Contentful Paint
    • INP — Interaction to Next Paint
    • LCP — Largest Contentful Paint
    • TTFB — Time to First Byte
    monitor.start()

    monitor.webVitals.onMetric.subscribe((metric) => {
    if (!metric) return
    console.log(`[Web Vital] ${metric.name}: ${metric.value} (${metric.rating})`)
    })

    monitor.webVitals.snapshot.subscribe((snap) => {
    console.log('Latest CLS:', snap.cls)
    console.log('Latest INP:', snap.inp)
    console.log('Recent Web Vitals reports:', snap.entries)
    })

    monitor.webVitals.clearLog()

    Metric shape:

    interface WebVitalMetric {
    name: 'CLS' | 'FCP' | 'INP' | 'LCP' | 'TTFB'
    value: number
    delta: number
    rating: 'good' | 'needs-improvement' | 'poor'
    id: string
    navigationType: string
    timestamp: number
    }

    CLS is unitless. FCP, INP, LCP, and TTFB are reported in milliseconds.


    Subscribe to all collectors at once:

    monitor.subscribe((snap) => {
    console.log('Full monitor snapshot at', new Date(snap.timestamp).toISOString())
    console.log(' FPS:', snap.performance.fps)
    console.log(' Pending requests:', snap.network.entries.filter(e => !e.error).length)
    console.log(' LCP:', snap.webVitals.lcp?.value ?? 'n/a')
    console.log(' React commits:', snap.react.totalCommits)
    console.log(' Custom events:', snap.events.entries.length)
    })

    // Or read synchronously
    const snap = monitor.getSnapshot()

    import { createMonitor } from 'monitor-api'
    import { useSignal, usePerformance, useNetwork, useReact, useEvents, useWebVitals } from 'monitor-api/react'

    const monitor = createMonitor()
    monitor.start()

    // Generic — subscribe to any signal
    function FpsDisplay() {
    const fps = useSignal(monitor.performance.fps)
    console.log('Rendering FpsDisplay, fps =', fps)
    return <span>FPS: {fps}</span>
    }

    // Collector-specific hooks
    function PerfPanel() {
    const { fps, memory, cls, longTasks } = usePerformance(monitor)
    console.log('Rendering PerfPanel:', { fps, memory, cls })
    return (
    <div>
    <p>FPS: {fps}</p>
    <p>Memory: {memory ? `${memory.used}MB (${memory.percent}%)` : 'n/a'}</p>
    <p>CLS: {cls.toFixed(4)}</p>
    <p>Long tasks: {longTasks.count}</p>
    </div>
    )
    }

    function NetworkPanel() {
    const { window5s, entries } = useNetwork(monitor)
    console.log('Rendering NetworkPanel, requests in last 5s:', window5s.count)
    return (
    <div>
    <p>{window5s.count} requests / 5s — avg {window5s.avgLatency}ms</p>
    <ul>
    {entries.slice(-5).map(e => (
    <li key={e.id}>{e.method} {e.url}{e.status} ({e.latency}ms)</li>
    ))}
    </ul>
    </div>
    )
    }

    function ReactPanel() {
    const { slowComponents, byComponent, totalCommits } = useReact(monitor)
    console.log('Rendering ReactPanel, total commits:', totalCommits)
    return (
    <div>
    <p>Total commits: {totalCommits}</p>
    <p>Slow components:</p>
    <ul>
    {slowComponents.map((e, i) => (
    <li key={i}>{e.component}{e.duration}ms [{e.type}]</li>
    ))}
    </ul>
    </div>
    )
    }

    function WebVitalsPanel() {
    const { cls, inp, lcp } = useWebVitals(monitor)
    return (
    <div>
    <p>CLS: {cls?.value ?? 'n/a'}</p>
    <p>INP: {inp ? `${inp.value}ms (${inp.rating})` : 'n/a'}</p>
    <p>LCP: {lcp ? `${lcp.value}ms (${lcp.rating})` : 'n/a'}</p>
    </div>
    )
    }

    const monitor = createMonitor({
    env: 'production',
    maxHistory: 60,
    report: {
    endpoint: 'https://my-api.com/metrics',
    interval: 30_000, // send every 30s
    headers: { Authorization: `Bearer ${token}` },
    timeout: 5_000,
    retry: {
    maxAttempts: 3,
    delay: (failedAttempt) => failedAttempt * 1_000,
    },
    transform: (snap) => ({
    fps: snap.performance.fps,
    memory: snap.performance.memory?.percent ?? null,
    errorRate: snap.network.window5s.errorRate,
    webVitals: {
    cls: snap.webVitals.cls,
    inp: snap.webVitals.inp,
    lcp: snap.webVitals.lcp,
    },
    }),
    },
    })

    monitor.start()

    Production reporting is intentionally best-effort: failed report requests are ignored after the optional retry policy is exhausted, so monitoring never breaks the application. Errors from transform, serialization, timeout, and transport setup are contained as well. The reporter keeps at most one delivery in flight and skips interval ticks while it is pending. Authentication can be supplied through headers. A custom transport({ endpoint, payload, body, headers, signal }) can replace fetch; without either transport, the reporter does not start.

    Without transform, the reporter sends a bounded, privacy-safe allowlist: the snapshot timestamp; current FPS, memory percentage, long-task and CLS aggregates; the five-second network aggregate; React commit counts; the retained custom-event count; and Web Vital values, deltas, and ratings. It does not send request URLs, errors, histories, event labels or data, component names, Web Vital IDs, or navigation types. The reporter endpoint is also excluded from NetworkCollector, while any configured network filter continues to apply.

    transform is an explicit opt-in to a custom payload and receives the full snapshot, including potentially sensitive application data. Redact secrets and bound the returned payload before enabling it in production.


    createMonitor({
    // Enable only specific collectors
    collectors: ['performance', 'network'],

    // Or configure each individually
    collectors: {
    performance: true,
    network: { filter: (url) => !url.includes('/analytics') },
    react: {
    slowThreshold: 8, // default 16ms
    maxFiberVisits: 10_000, // default 10,000
    includeZeroDuration: false, // default false
    },
    events: {
    maxLabelLength: 256, // default 256 characters
    maxDataDepth: 5, // default 5 nested object/array levels
    maxDataBytes: 16 * 1024, // default 16 KiB of UTF-8 JSON
    },
    webVitals: { reportAllChanges: true },
    },

    maxHistory: 60, // data points per metric
    sampleRate: 0.25, // one sampling decision per monitor instance
    })

    Collectors that are disabled—or belong to a sampled-out monitor—use inert facades with empty snapshots. Their concrete collectors are not constructed and their signals are not dependencies of the combined snapshot.


    monitor-api/
    ├── dist/
    │ ├── index.jsESM core
    │ ├── index.cjsCJS core
    │ ├── index.d.tstypes
    │ └── react/
    │ ├── index.jsReact hooks (ESM)
    │ ├── index.cjsReact hooks (CJS)
    │ └── index.d.ts

    npm run typecheck
    npm test
    npm run build
    npm run docs:build
    npm run bench
    • docs:build generates TypeDoc HTML in docs/.
    • bench builds the package and runs runtime benchmarks from bench/.
    • Benchmark notes are tracked in BENCH.md.

    MIT — see LICENSE.


    Repository: github.com/ElJijuna/monitor-api