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

@lmstech/monitor

v0.3.0

Published

Lightweight monitoring package for Node apps. Captures server logs, client errors, uncaught exceptions, and unhandled rejections — sends everything to the Observatory dashboard.

Readme

@lmstech/monitor

Lightweight monitoring package for Node apps. Captures server logs, client errors, uncaught exceptions, and unhandled rejections — sends everything to the Observatory dashboard.

If MONITOR_URL and MONITOR_KEY aren't set, the entire package is inert. No patching, no listeners, no network calls.

Four entry points:

| Entry | For | |---|---| | @lmstech/monitor/server | Next.js apps (App Router) | | @lmstech/monitor/node | Any plain Node process — Express, Fastify, a bare http server, a worker/CLI | | @lmstech/monitor/express | Express apps specifically — error middleware + a mountable proxy endpoint | | @lmstech/monitor/client | Browser — React provider + hook (works in Next.js or a plain Vite/React SPA) |

/server, /node, and /express all share the same underlying init/log/batch-queue core — there is no Next.js or React anywhere on the /node or /express import paths. Calling init() via @lmstech/monitor/node (or /server) arms monitorErrorHandler() from @lmstech/monitor/express in the same process — they're not independent copies. This is a real singleton per module system: the package's internal core modules are built unbundled (see tsup.config.ts), so every entry point that imports them resolves to the exact same file on disk, and Node's own module cache does the sharing.

Limitation — don't mix require() and import() of this package in one process. Node treats CommonJS and ESM as two separate module registries, so a process that both require("@lmstech/monitor/node") and import("@lmstech/monitor/express") would load two independent copies of the core (one per registry) — init() in one would not arm the middleware loaded via the other. This is the general "dual package hazard" any dual-format npm package has, not something specific to @lmstech/monitor; pick one module system per process (which is already the normal case for any single Node app) and it doesn't apply.

Setup in a Next.js app

Four files, nine lines.

1. instrumentation.ts (project root)

export async function register() {
  const { init } = await import("@lmstech/monitor/server");
  init({ app: "my-app", env: "production", version: process.env.COMMIT_SHA });
}

Filtering noisy events

Pass an ignore predicate to drop events before they're reported — useful for noisy-but-harmless errors like HTTP socket aborts from client disconnections:

init({
  app: "my-app",
  env: "production",
  ignore: (event) =>
    event.message === "aborted" && event.stack?.includes("abortIncoming") === true,
});

The predicate runs for every event (console.error, uncaughtException, unhandledRejection, log). Return true to drop. A throwing predicate is treated as "do not ignore" — a broken filter won't hide errors.

2. app/api/monitor/route.ts

export { POST } from "@lmstech/monitor/server";

3. app/layout.tsx

import { Monitor } from "@lmstech/monitor/client";

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Monitor app="my-app" env="production">
          {children}
        </Monitor>
      </body>
    </html>
  );
}

4. Environment variables

MONITOR_URL=https://your-dashboard.vercel.app
MONITOR_KEY=your-api-key
COMMIT_SHA=abc123  # typically set by CI (GITHUB_SHA in GitHub Actions)

Don't set these in local dev. The package stays invisible when they're absent.

Setup in a plain Node app (no Express)

One line.

import { init } from "@lmstech/monitor/node";

init({ app: "my-worker", env: "production", version: process.env.COMMIT_SHA });

Call it once, as early as possible in your process's startup. It patches console.error, registers uncaughtException/unhandledRejection handlers, and starts the batch flush timer — same behavior as @lmstech/monitor/server, minus anything Next.js-specific. Use log() for custom events the same way as the server entry (see below).

Setup in an Express app

Three pieces: init, an error middleware, and (if you're also using the browser <Monitor> client) a proxy endpoint.

1. Init — early in your server's startup

import { init } from "@lmstech/monitor/node";

init({ app: "my-api", env: "production", version: process.env.COMMIT_SHA });

2. Error-reporting middleware — mounted LAST, after all routes

import { monitorErrorHandler } from "@lmstech/monitor/express";

app.use(monitorErrorHandler());

Reports the error with request method + route path only. Never the request body, headers, cookies, or query string — those can carry PHI, auth tokens, or session data, and are never read or serialized by this middleware. Always calls next(err) afterward, so your app's own error handling is unaffected whether reporting succeeds, fails, or is a no-op (e.g. init() was never called).

3. Client proxy endpoint — only needed if you're also mounting <Monitor> in a browser client

import { monitorRequestHandler } from "@lmstech/monitor/express";

app.post("/api/monitor", monitorRequestHandler());

This is the Express equivalent of the Next.js POST route re-export — it validates and normalizes the event the browser client posted, then forwards it to the dashboard server-side with MONITOR_KEY attached (the key never reaches the browser). Works with or without a body-parser mounted ahead of it; if none is mounted for this route, it reads the raw request stream itself. Always responds 200.

4. Environment variables

Same as the Next.js setup — MONITOR_URL, MONITOR_KEY, optionally COMMIT_SHA. Unset in local dev; the package stays inert.

Custom logging

Server-side (Next.js, plain Node, or Express)

import { log } from "@lmstech/monitor/server"; // or "@lmstech/monitor/node"

log({ level: "info", message: "Payment processed", meta: { userId: "123" } });

Client-side

import { useMonitor } from "@lmstech/monitor/client";

const { log } = useMonitor();
log({ level: "error", message: "Checkout failed", meta: { step: "payment" } });

useMonitor() must be called inside the <Monitor> provider tree. The <Monitor>/useMonitor() client works unchanged in a plain Vite + React SPA — it isn't Next.js-specific. It posts events to /api/monitor on the same origin, so mount monitorRequestHandler() (Express) or the re-exported POST (Next.js) at that path.

Developing the package

Everything runs from the repo root.

pnpm pkg:build       # clean build (tsup)
pnpm pkg:release     # validate + build + version-bump + publish to npm

The build uses tsup (see tsup.config.ts) to emit both ESM and CJS, plus matching .d.ts/.d.cts, for every entry — so require("@lmstech/monitor/...") and import("@lmstech/monitor/...") both resolve to real files. Two sub-configs: server/node/express build unbundled (bundle: false) so core/, shared/, and types.ts compile to real, individually-shared files instead of being duplicated into each entry's bundle — see the "shared runtime state" note above. client stays a single self-contained bundle (it never touches the server-side core) and its own tsup sub-config forces its output to start with "use client" without affecting the other entries.

Testing

cd packages/monitor
pnpm test            # run tests once
pnpm test:watch      # watch mode
pnpm typecheck       # type-check without emitting

Package structure

src/
  types.ts              # all public types (EventLevel, LogPayload, InitOptions, etc.)
  core/                  # framework-agnostic core — no Next.js or React imports
    init.ts             # console.error patching, process handlers, startup event
    log.ts              # custom server-side log()
    batch.ts            # in-memory queue, 5s / 50-event flush
    send.ts             # fire-and-forget fetch to /api/ingest
    proxy.ts            # validate/normalize/forward a browser-origin event (shared by server + express)
  server/
    index.ts            # exports: init, log, POST — re-exports core + adds the Next.js route
    route.ts            # Next.js App Router POST handler for the client-side proxy
  node/
    index.ts            # exports: init, log — plain re-export of core, for any non-Next Node app
  express/
    index.ts            # exports: monitorErrorHandler, monitorRequestHandler
    middleware.ts        # (err, req, res, next) error-reporting middleware — method/route meta only
    handler.ts           # mountable POST handler for the client-side proxy (duck-typed vs. Express, no express dependency)
  client/
    index.ts            # exports: Monitor, useMonitor
    context.tsx          # React context for app/env
    monitor.tsx          # error + unhandledrejection listeners, sendBeacon
    use-monitor.ts       # hook for custom client-side logging
  shared/
    serialize.ts         # safe meta serialization (circular refs, depth/size limits)
    limits.ts             # MAX_MESSAGE_LENGTH, shared across every reporting path
    url.ts                 # stripQueryString — never report a URL's query string

core/ and shared/ (plus types.ts) are built unbundled — each file compiles 1:1 to its own file under dist/, so every entry point (server, node, express) that imports them shares the exact same module instance at runtime instead of getting its own inlined copy. See the "shared runtime state" note above, and the long comment at the top of tsup.config.ts, for why this matters.

Four entry points: @lmstech/monitor/server, @lmstech/monitor/node, @lmstech/monitor/express, and @lmstech/monitor/client.

Publishing

  1. Bump the version in packages/monitor/package.json.
  2. From the repo root: pnpm pkg:publish.

That's it. The script builds first, then publishes dist/ to npm.

First time only: make sure you're logged in (npm login) and the @lmstech npm org exists with public access. The publish script uses --access public for the scoped package.