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

@reprova/sdk

v0.4.0

Published

Reprova SDK for Node.js: Express error capture, Prisma data-footprint recording, and outbound HTTP call recording for locally runnable reproductions

Readme

@reprova/sdk

Node.js/TypeScript SDK for any Node app — vanilla node:http, Koa, Fastify, Express, background workers. Reprova.init() instruments Node's own HTTP servers, so every request gets capture context (errors, silent 5xxs, headers, trace ids, outbound HTTP calls) with zero framework code. Express and Prisma integrations are optional enrichments: Express adds async-rejection forwarding and parsed request bodies; Prisma adds automatic data footprints (which tables/rows each request touched — what makes an error reproducible, not just visible). The batched, fire-and-forget transport never adds meaningful latency, even when the control plane is unreachable (transport.test.ts; chaos.spec.ts proves it against the real running control plane).

Install

npm install @reprova/sdk

Usage — any Node app

import { Reprova } from '@reprova/sdk';

Reprova.init({ dsn: process.env.REPROVA_DSN, release: gitSha });
// That's it. A plain http.createServer / Koa / Fastify app now captures
// errors, silent 5xx responses, request context, trace ids, outbound calls.

init instruments http.Server/https.Server directly and attaches process-level error handlers (opt-outs: instrumentHttp: false, processHandlers: false; uncaughtException flushes then exits 1). A missing dsn puts the SDK in disabled mode: everything mounts, nothing records.

Background jobs and scripts

await sdk.runJob('nightly-sync', async () => { ... });      // failures ingest as job_failure
await sdk.runWithContext({ name: 'csv-import' }, doImport);  // generic wrapper: capture + rethrow

Data footprints without Prisma (knex, raw SQL, any DAO)

const rows = await knex('invoices').where({ id }).select();
sdk.recordFootprint({ model: 'Invoice', op: 'select', pks: rows.map(r => r.id), count: rows.length });

Reproductions need footprints. Six ORMs record them automatically (below); anything else records them with one recordFootprint call per query site.

| ORM | Helper | Auto PKs | where_shape | | --- | --- | --- | --- | | Prisma | instrumentPrisma / createPrismaExtension | ✅ exact (DMMF) | ✅ | | Kysely | createKyselyPlugin | ✅ (primaryKey opt for composite) | ✅ | | Sequelize | installSequelizeHooks | ✅ exact (model metadata) | ✅ | | TypeORM | installTypeOrmSubscriber | ✅ exact (entity metadata) | ➖ | | Knex / Objection | installKnexHooks | ✅ SELECT; INSERT depends on driver RETURNING | ➖ | | Drizzle | createDrizzleLogger | ❌ shape-only (no result hook) | ✅ |

Express (optional enrichment)

const sdk = Reprova.init({ dsn: process.env.REPROVA_DSN, release: gitSha });
sdk.setupExpress(app);   // async-rejection forwarding + error-middleware capture + parsed bodies

Prisma (optional enrichment — automatic footprints)

One call wires both automatic footprints and migration_id:

const sdk = Reprova.init({ dsn: process.env.REPROVA_DSN, release: gitSha });
await sdk.instrumentPrisma(prisma, { dmmf: Prisma.dmmf });

instrumentPrisma splices the footprint extension onto your existing client in place (so the shared singleton keeps recording) and best-effort reads the latest row of _prisma_migrations into migration_id. It's a no-op when the SDK is disabled (no dsn / replay), so no if (dsn) guard is needed, and it never imports @prisma/client — Prisma stays an optional peer. A missing or unreadable migrations table is non-fatal: migration_id stays 'unknown'. Pass { readMigrationId: false } to set it yourself via sdk.setMigrationId.

Prefer to own the wiring? The extension is still exported directly:

import { createPrismaExtension } from '@reprova/sdk';
const prisma = new PrismaClient().$extends(createPrismaExtension(Prisma.dmmf));

Kysely (optional enrichment — automatic footprints)

Add the plugin at construction; every query then records a footprint:

import { createKyselyPlugin } from '@reprova/sdk';
const db = new Kysely<DB>({ dialect, plugins: [createKyselyPlugin()] });

The plugin reads the compiled AST for the table(s) and operation, the WHERE clause's referenced column names (never values — PII-safe by construction), and the returned rows for primary keys. Joined/subquery tables are recorded as referenced (without PKs). It's a no-op outside a captured request, never imports kysely, and can never break a query (every path is guarded).

Kysely carries no schema metadata at runtime, so PK detection defaults to ['id']. Override for composite or non-id keys:

createKyselyPlugin({ primaryKey: (table) => table === 'membership' ? ['tenantId', 'userId'] : ['id'] });

migration_id is independent of the ORM: read your migration tool's version table (Kysely's default is kysely_migration) and call sdk.setMigrationId(latest), or leave it 'unknown'.

Sequelize (optional enrichment — automatic footprints)

Install once after your models are defined; every query records a footprint:

import { installSequelizeHooks } from '@reprova/sdk';
installSequelizeHooks(sequelize);

Uses Sequelize's lifecycle hooks (afterFind, afterCreate, …) plus each model's tableName / primaryKeyAttributes, so PKs are exact. Hooks are registered per model (and on afterDefine for later ones), so the table is known even when a query returns 0 rows. where_shape = the WHERE object's top-level keys (names only).

TypeORM (optional enrichment — automatic footprints)

Attach the subscriber after dataSource.initialize():

import { installTypeOrmSubscriber } from '@reprova/sdk';
await dataSource.initialize();
installTypeOrmSubscriber(dataSource);

Uses entity lifecycle events (afterLoad, afterInsert, …) and each entity's metadata for exact PKs. TypeORM fires these per entity with no WHERE context, so entries are merged by (table, op) within a request (union of distinct PKs), and there is no where_shape.

Knex / Objection (optional enrichment — automatic footprints)

One listener covers raw Knex and Objection.js (which runs through the same Knex instance):

import { installKnexHooks } from '@reprova/sdk';
installKnexHooks(knex);

Reads the table and operation from Knex's query-response event. SELECT rows yield PKs (via primaryKey(table), default ['id']); UPDATE/DELETE record an affected-row count; INSERT PK fidelity depends on the driver's RETURNING support (full on Postgres, last-id only on sqlite/mysql). No where_shape (Knex has no query AST).

Drizzle (optional enrichment — shape-only)

Drizzle has no result-aware hook, only a logger — so this adapter records the table, operation, and WHERE column names, but cannot extract PKs:

import { createDrizzleLogger } from '@reprova/sdk';
const db = drizzle(client, { logger: createDrizzleLogger() });

For PK-level fidelity with Drizzle, add sdk.recordFootprint(...) at the query sites that matter; the logger gives cheap table-level coverage on top.

Express (continued)

setupExpress(app) can be called before or after your routes; it wires the request-context middleware (repositioned to run first), error capture, the HTTP-5xx hook, and async rejection forwarding in one call. A missing dsn puts the SDK in disabled mode: nothing is recorded or sent, but Express-5 rejection semantics still apply — so apps can call init unconditionally.

Prefer manual control? sdk.requestHandler() / sdk.errorHandler() / wrapResponse are still exported and behave exactly as before:

app.use(sdk.requestHandler());
// ... your routes ...
app.use(sdk.errorHandler());

Every capture carries a W3C trace_id, the git release sha, and the latest applied Prisma migration_id — non-negotiable schema fields the rest of the pipeline depends on.

Async handlers just work

Existing route code needs no changes — no asyncHandler wrapper, no try/catch → next(err):

app.get('/invoices/:id', async (req, res) => {
  const invoice = await db.invoice.find(req.params.id); // a rejection here is
  res.json(invoice);                                    // captured automatically
});

Express 4 normally discards the promise an async handler returns, so a rejection never reaches error middleware. On the first request, the SDK transparently patches the router's dispatch so rejected handler promises are forwarded down the error chain — the exact semantics Express 5 ships natively — with the full request context (headers, body, data footprint, trace id) intact on the capture. Your own error middleware still runs and the client still gets its response; nothing is swallowed. On Express 5 the SDK detects native forwarding and does nothing.

One limitation: a router imported from a different physical copy of express in node_modules (rare — npm normally dedupes to one) isn't covered by the outer app's patch; mount sdk.requestHandler() inside that sub-app to cover its copy too.

Commands

npm run build
npm run lint
npm test