opticore-profiler
v1.0.4
Published
Web debug toolbar
Maintainers
Readme
OptiCore Profiler
OptiCore profiler : collect → persist by token → display deferred.
Installation
npm install opticore-profilerRequires opticore-express and opticore-router in the host app (both are
declared dependencies and installed automatically) and Node.js >= 18.
The 4 principles, and where they live
Collectors observe, they never intrude.
DataCollector(types.ts) definescollect(ctx, error?) / getName() / getData() / reset().RequestCollector/TimeCollector/MemoryCollectorread only from thereq/resobjects the middleware already has.DatabaseCollectorandLoggerCollectorare fed by decorating a DB driver/client's prototype method andLoggerCore.prototype.{info,warn,error,debug,success}once, at bootstrap (instrumentDatabase/instrumentLogger) — business code keeps callingdb.query(...)/logger.info(...)completely unmodified, and unaware profiling exists.instrumentDatabaseis not tied to any one database: it wraps whichever method you point it at and turns that method's own arguments into{ sql, bindings }via a smallextractfunction you supply, so it works the same way for MySQL, MariaDB, Postgres, OracleDB, MongoDB, or any other driver. Ready-made presets (instrumentMySQL,instrumentMariaDB,instrumentPostgres,instrumentOracleDB,instrumentMongoDB) cover the common ones — seeinstrumentation/database.instrumentation.ts. The link between "a query just ran" and "which request is that for" isAsyncLocalStorage(context.ts), the Node analogue of what a DI container gives Symfony's collectors implicitly. Register custom collectors on the registry the middleware exposes:profiler.registry.register(() => new MyCollector()).Persistence is decoupled from collection, keyed by token. Every profiled request gets a short token (
token.ts, 6–8 alphanumeric chars). Onres.on('finish')— i.e. after the response has already been sent, so this adds zero perceived latency — every collector'sgetData()is assembled into aProfileand handed to aProfilerStorage(MemoryStorageorFileStorage, under.opticore-profiler-cache/by default).X-Debug-TokenandX-Debug-Token-Linkheaders are set on every profiled response. Automatic purge runs after each write (retentionMs, default 24h).Display is deferred and AJAX-loaded, not embedded. The middleware never renders a toolbar into the page it's decorating. It buffers HTML responses (
middleware/htmlInjector.ts) and, only when the response istext/html, splices a handful of lines before</body>(middleware/snippet.ts): a stylesheet<link>and a<script>that fetchesGET ${routePrefix}/wdt/:tokenand swaps it in. That fragment, plusGET ${routePrefix}(list) andGET ${routePrefix}/:token(detail, one panel per collector), are the only 3 profiler routes — all excluded from profiling themselves.assets/js/toolbar.jsalso wrapsfetchandXMLHttpRequestclient-side so AJAX calls the page itself makes (which carry the sameX-Debug-Tokenheader, since they're profiled requests too) show up live in the toolbar's HTTP list.Ephemeral by default, safe by construction.
enableddefaults tofalse— must be explicitly turned on (NODE_ENV === 'development').RequestCollectormasks configurable sensitive keys (security/masker.ts, default list inDEFAULT_SENSITIVE_KEYS:authorization,cookie,password,token,session, ...) in headers/cookies/query/body before the data is ever persisted. Nunjucks'autoescape: truecovers template output;security/escape.tsandtoolbar.js's ownescapeHtml()cover the two spots that build HTML by hand.profiler.clear()purges everything on demand.
Integration example
import { express } from "opticore-express";
import { OptiCoreMySQLDriver } from "opticore-mysqldb";
import { LoggerCore } from "opticore-logger";
import {
opticoreProfiler,
profilerErrorHandler,
registerProfilerViews,
createProfilerRouter,
instrumentMySQL,
instrumentLogger,
FileStorage,
} from "opticore-profiler";
const app = express();
// Decorate infrastructure once, at bootstrap — never touches business code.
instrumentMySQL(OptiCoreMySQLDriver);
instrumentLogger(LoggerCore);
const profiler = opticoreProfiler({
enabled: process.env.NODE_ENV === "development",
storage: new FileStorage(".profiler-cache"), // any writable directory of your choosing
});
app.use(profiler); // 1. collection + token + deferred display
registerProfilerViews(app, profiler); // 2. view engine, static assets, "/" page
app.use(/* ...your feature routers, including createProfilerRouter(profiler)... */);
app.use(profilerErrorHandler()); // 3. after routes: feeds ExceptionCollector
app.use(yourOwnErrorHandler); // your app's error handling is untouchedInstrumenting other databases
instrumentMySQL is one preset among several, all built on the same
driver-agnostic instrumentDatabase. Use whichever preset matches your
stack, or call instrumentDatabase directly for anything else:
import { Pool } from "pg";
import {
instrumentPostgres, // pg's query(text, values)
instrumentMariaDB, // mariadb's query(sql, values)
instrumentOracleDB, // oracledb's execute(sql, binds)
instrumentMongoDB, // MongoDB's Collection CRUD methods
instrumentDatabase, // the generic engine, for anything else
} from "opticore-profiler";
instrumentPostgres(Pool);
instrumentMariaDB(MariaDbConnection);
instrumentOracleDB(OracleConnection);
instrumentMongoDB(MongoCollection);
// Any other driver: wrap its query method and describe how to read its arguments.
instrumentDatabase(SomeDriver, {
type: "my-db",
method: "runQuery",
extract: (sql: string, params?: unknown[]) => ({ sql, bindings: params }),
});