npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@symbo.ls/analyze

v3.14.779

Published

Runtime audit logger for smbls apps. Errors, warnings, lifecycle traces, browser events, network calls, and session replay — designed for smbls's declarative model so the runtime is the source of truth.

Readme

@symbo.ls/analyze

Runtime audit logger for smbls apps. Errors, warnings, lifecycle traces, browser events, network calls, and session replay — designed for smbls's declarative model so the runtime is the source of truth.

Think of it as Grafana Faro built for DOMQL: error collection by default, deep tracing in debug mode, and a clean transformer→sink pipeline for analytics, AI agents, and e2e replay.

At a glance

  • Auto-registered by smbls when analyze: true|{...} is set on create().
  • Errors and warnings on by default, everything else opt-in.
  • Dormant until hydration completes — no noise from initial render, no first-paint cost beyond registration. Pre-hydration errors are buffered and drained at activation.
  • Transformer → sinks pipeline: events flow through pure transforms before reaching destinations. Default: [redact → enrich → summarize] → [console, memory].
  • Browser-event capture for clicks, keys, forms, scroll, viewport, performance, console — all opt-in, all redacted by default.
  • Network capture chains with @symbo.ls/fetch (smbls-aware events with cache key, mode, transform name) AND wraps window.fetch + XHR for non-smbls traffic.
  • Session replay uses smbls's element-tree-derived rendering to record state + input events instead of DOM mutations. Tiny payload, perfect fidelity.
  • Cached error ring buffer queryable at runtime via context.analyze.query() — for in-app debug panels and AI agents.

Defaults

import { create } from 'smbls'

create(App, {
  analyze: true   // → errors + warnings, redact → enrich → summarize → [console, memory]
})

In production, this captures window.onerror, unhandledrejection, lifecycle handler throws, and el.warn / el.error calls — nothing else.

Configuration

create(App, {
  analyze: {
    enabled: true,
    level: 'warn',                  // error | warn | info | debug | trace

    capture: {
      // on by default
      errors: true,
      warnings: true,

      // browser tracking — off by default
      pointer: false,
      keyboard: false,
      forms: false,
      scroll: false,
      viewport: false,
      network: false,
      performance: false,
      navigation: false,
      console: false,

      // smbls internals — debug-only
      lifecycle: false,
      state: false,
      updates: false,

      // presets — expand into combinations of the above
      replay: false,                // full session-replay set
      telemetry: false              // analytics-friendly subset
    },

    // throttling (ms)
    throttle: {
      pointermove: 80,
      mousemove: 80,
      scroll: 100,
      resize: 200
    },

    // privacy
    redact: ['password', 'token', /secret/i, /ssn/i],
    maskFormValues: true,
    maskStrategy: 'mask',           // 'mask' | 'hash' | 'redact'

    // ring-buffer cache for memory sink
    cache: {
      max: 500,
      dedupeWindowMs: 1000
    },

    // pipeline
    transformers: ['enrich', 'summarize'],   // 'redact' is always first
    sinks: ['console', 'memory'],

    // hydration gating
    captureDuringHydration: false,

    // debug overrides
    debug: false,                   // or set ?analyze=debug in URL
    onEvent: null                   // optional catch-all (ev) => void
  }
})

What gets captured

Always-on: errors and warnings

  • window.onerror and unhandledrejection (after activation).
  • Lifecycle handler throws (onClick, onUpdate, etc.). Element-core routes these through triggerLifecycle('error', ...) so analyze sees them with element context. Falls back to console.error when no plugin is installed.
  • el.warn(...) and el.error(...) from element prototype.

Browser events (opt-in)

| Category | Captures | Notes | |---|---|---| | pointer | click, dblclick, pointermove, contextmenu | move events throttled | | keyboard | keydown, keyup | key codes + modifiers only, never values | | forms | input, change, submit | values masked by default | | scroll | window + element scroll position | throttled | | viewport | resize, orientationchange, visibilitychange | + initial snapshot at activation | | network | fetch + XHR | URL, method, status, duration | | performance | LCP, CLS, INP, longtasks, paint | PerformanceObserver | | navigation | route changes via DOMQL renderRouter | no listeners — free | | console | console.{log,warn,error,debug} proxy | console sink uses saved originals — no recursion |

Element targeting uses the DOMQL key path (e.g. App > Sidebar > MenuItem_3), built by walking up data-key DOM attributes. Stable across renders. Falls back to a CSS selector for events on non-DOMQL nodes.

smbls internals (debug-only)

  • lifecycle — init, create, render, complete, done, attachNode, lazyLoad, frame, beforeRemove, remove.
  • state — stateInit, stateCreated, stateUpdate, beforeStateUpdate. Coarse-grained: which keys changed, by whom.
  • updates — every update() call. Only useful when actively debugging; high volume.

Network capture (chained with fetch plugin)

@symbo.ls/fetch calls context.analyze.emitNetwork(...) at the start, success, and error of every runFetch and runMutation. The call is a no-op when analyze isn't installed — so projects without analyze pay nothing.

Default-off, including in the remote preset. The analyzed server discards un-opted-in logType=network envelopes at ingest (server cd64446f), so the emitter no longer captures or ships them by default — no fetch/XHR wrapping, no batching, no egress. Re-enable with the same levers the server honors: debug: true (or ?analyze=debug), which also stamps app.debug on every envelope so the server's per-envelope lever accepts the rows end-to-end; or an explicit capture: { network: true } / runtime state.setCapture('network', true) — the client half of the per-workspace Organization.settings.analyzedNetworkCapture opt-in.

When analyze IS installed and network: true is set, you get smbls-aware events with:

{
  type: 'network',
  hook: 'fetch.start' | 'fetch.success' | 'fetch.error',
  source: 'symbo-fetch',
  mode: 'query' | 'mutation',
  from: 'users', method: 'select',
  cacheKey: 'users:select::en',
  durationMs: 12.4,
  ok: true,
  status: 200
}

In addition, window.fetch and XMLHttpRequest are wrapped to catch any non-smbls traffic (third-party SDKs, raw fetches in user code). Beacon traffic is filtered out via the X-Analyze-Beacon header to avoid capturing analyze's own events.

Session replay (smbls-native)

Conventional session replay (rrweb, FullStory) records DOM mutations and replays them into a clean DOM. That's heavy, lossy with CSS-in-JS, and re-implements a rendering pipeline that smbls already owns.

Because every visible byte in a smbls app is derived from element tree + state, replay only needs:

replay payload  =  initial state snapshot
                +  input event log (clicks, keys, forms, viewport, scroll)
                +  state mutations  (the stateUpdate hook)

To replay: restart the app with the snapshot, then replay input events in order. The DOM regenerates itself.

Enable with:

analyze: {
  level: 'info',
  capture: { replay: true },
  sinks: [{ type: 'memory', max: 5000 }]
}

Then context.analyze.replay() returns { initialSnapshot, events } ready to ship.

Transformer pipeline

Every captured event flows through a single ordered pipeline:

hook fires → build event → transformers[] → sinks[]

A transformer is (event) => event | null | event[]. Return null to drop. Return an array to fan out.

Built-in transformers

| Name | What it does | |---|---| | redact | Walks the event, masks keys matching redact config. Always runs first — cannot be reordered. Disable with redact: false. | | enrich | Adds session id, route, viewport, app id, build hash. Active by default. | | summarize | Replaces raw element refs and Error instances with safe slices. Active by default. | | dedupe | Drops events identical to one within dedupeWindowMs. | | sample(rates) | Drops 1 - rate of events of a given type. |

Custom transformers are functions:

transformers: [
  'enrich',
  'summarize',
  (ev) => ev.level === 'trace' ? null : ev,
  (ev) => ({ ...ev, app: 'workspace' })
]

Sinks

| Sink | Purpose | |---|---| | console | Pretty-printed, color by level. Default. Saves console.error/warn/log references at construction so the console proxy doesn't recurse. | | memory | Ring buffer in-process. Queryable via context.analyze.query(). Default. | | beacon | Batched POST. Falls back to navigator.sendBeacon on pagehide / beforeunload. |

Custom sinks are functions: (event) => void.

sinks: [
  'console',
  { type: 'memory', max: 1000 },
  { type: 'beacon', url: '/analyze', batchMs: 5000 },
  (ev) => myCustomSink(ev)
]

Runtime API

Exposed on context.analyze:

context.analyze.query({ level: 'error', sinceMs: 60000 })   // ring buffer query
context.analyze.snapshot()                                   // full current buffer
context.analyze.flush()                                      // force-send pending beacons
context.analyze.replay()                                     // export replay payload
context.analyze.activate(context)                            // (called by smbls after onCreate)
context.analyze.destroy()                                    // (called by smbls destroy())
context.analyze.pause() / .resume()                          // toggle capture without unregistering
context.analyze.setLevel('debug')                            // change log level at runtime
context.analyze.setCapture('pointer', true)                  // toggle a category at runtime
context.analyze.emit(event)                                  // raw emit (used by element-core)
context.analyze.emitNetwork(data)                            // helper used by @symbo.ls/fetch

Debug mode

Three ways to enable:

// 1. Config
analyze: { debug: true }

// 2. URL parameter (no rebuild)
//    https://app.localhost/?analyze=debug

// 3. Runtime toggle
context.analyze.setLevel('debug')
context.analyze.setCapture('updates', true)

debug: true (or ?analyze=debug) flips on lifecycle, state, updates, and console capture, and sets level to debug. To turn analyze off entirely via URL: ?analyze=off.

How it works

Plugin registration

Auto-registers in packages/smbls/src/createDomql.js when context.analyze is truthy:

if (context.analyze && !hasPlugin('analyze')) {
  const analyzeConfig = context.analyze === true ? {} : context.analyze
  context.analyze = createAnalyzeState(analyzeConfig)
  context.plugins.push(analyzePlugin)
}

So context.analyze IS the live state object — .emit, .query, .activate are all there.

Lifecycle hooks tapped

Hooks dispatched by triggerLifecycle() in packages/element/src/create.js and runPluginHook() in packages/element/src/update.js:

  • error — handler throws + explicit error reports
  • update, beforeUpdate (debug only)
  • stateUpdate, beforeStateUpdate (debug only, also for replay)
  • init, create, render, complete, done (debug only)
  • beforeRemove, remove (debug only)
  • renderRouter (always-on if navigation enabled)

Hydration gate

The plugin sits in context.plugins from creation, but every hook is a guarded no-op until context.analyze.__ready === true. The flag flips in one place: packages/smbls/src/index.js, immediately after app.onCreate fires.

A small pre-ready buffer keeps error events that fire during init — so first-paint crashes still surface, even though the plugin is technically dormant.

Element-core integration

Two small integrations in packages/element/src/:

  1. create.js — lifecycle handler throws now go through triggerLifecycle('error', element, { hook, error }) instead of bare console.error. The framework still falls back to console.error when no plugin handles the hook.
  2. methods.js — el.warn() and el.error() emit through context.analyze first, then fall through to the original env-gated console.warn / throw.

packages/utils/function.js — runPluginHook now returns true when at least one plugin handled the hook, so callers can fall back to default behavior when no plugin captured.

Browser-event listeners

When opt-in browser categories are enabled, the plugin attaches passive listeners on window and document during activate(). Listeners are detached during destroy(app). All listeners are throttled or debounced per the throttle config and route through the same pipeline as lifecycle events.

Privacy

Browser-event capture without redaction is a compliance disaster. The plugin defaults are tuned for that:

  • redact transformer always runs first and cannot be reordered. Disable explicitly with redact: false if you mean it.
  • Form values: masked by default. Opt in per-field with data-analyze="track" (data-analyze="skip" always skips).
  • <input type="password">, <input type="hidden">, and fields matching /email|token|secret|ssn|card|cvv|pin|otp/i are never recorded.
  • Mask strategies: 'mask' (***), 'hash' (32-bit hash with # prefix), 'redact' (drop the field entirely).

Performance

  • Plugin registration: zero cost (one object pushed to context.plugins).
  • Hooks while dormant: one boolean check.
  • Hooks after activation, with category off: one boolean check.
  • Hooks after activation, category on: build event, run pipeline, write to sinks. Sub-microsecond on hot paths with sampling.
  • Browser listeners: passive + throttled.

updates capture is the one hot-path category and is debug-only for that reason.

Comparison

| | rrweb | LogRocket | Sentry SR | analyze | |---|---|---|---|---| | Replay model | DOM mutations | DOM mutations | DOM mutations | state + events | | Payload size | MB | MB | MB | KB | | CSS-in-JS fidelity | partial | partial | partial | perfect | | smbls integration | none | none | none | native | | Privacy default | opt-out | mixed | mixed | opt-in everywhere |

Multi-app caveat

window.fetch and XMLHttpRequest wrappers are global. If multiple smbls apps mount on the same realm and each enables network: true, the wrappers nest. Destroy order matters — the last app destroyed correctly restores window.fetch, but interleaved destroys may leave a stale wrapper. For the multi-app case, install network capture on a single shell app and let nested apps emit through the same context.analyze.

License

CC-BY-NC-4.0