# OptiCore Profiler

OptiCore profiler :
**collect**
**persist by token**
**display deferred**.

## Installation

```bash
npm install opticore-profiler
```

Requires `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

1. **Collectors observe, they never intrude.** `DataCollector` (`types.ts`)
   defines `collect(ctx, error?) / getName() / getData() / reset()`.
   `RequestCollector`/`TimeCollector`/`MemoryCollector` read only from the
   `req`/`res` objects the middleware already has. `DatabaseCollector` and
   `LoggerCollector` are fed by **decorating** a DB driver/client's prototype
   method and `LoggerCore.prototype.{info,warn,error,debug,success}` **once,
   at bootstrap** (`instrumentDatabase`/`instrumentLogger`) — business code
   keeps calling `db.query(...)` / `logger.info(...)` completely unmodified,
   and unaware profiling exists. `instrumentDatabase` is 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 small `extract`
   function 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 — see
   `instrumentation/database.instrumentation.ts`. The link between "a query
   just ran" and "which request is that for" is `AsyncLocalStorage`
   (`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())`.

2. **Persistence is decoupled from collection, keyed by token.** Every
   profiled request gets a short token (`token.ts`, 6–8 alphanumeric chars).
   On `res.on('finish')` — i.e. **after** the response has already been sent,
   so this adds zero perceived latency — every collector's `getData()` is
   assembled into a `Profile` and handed to a `ProfilerStorage`
   (`MemoryStorage` or `FileStorage`, under `.opticore-profiler-cache/` by
   default). `X-Debug-Token` and `X-Debug-Token-Link` headers are set on
   every profiled response. Automatic purge runs after each write
   (`retentionMs`, default 24h).

3. **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 is
   `text/html`, splices a handful of lines before `</body>`
   (`middleware/snippet.ts`): a stylesheet `<link>` and a `<script>` that
   fetches `GET ${routePrefix}/wdt/:token` and swaps it in. That fragment,
   plus `GET ${routePrefix}` (list) and `GET ${routePrefix}/:token` (detail,
   one panel per collector), are the only 3 profiler routes — all excluded
   from profiling themselves. `assets/js/toolbar.js` also wraps `fetch` and
   `XMLHttpRequest` client-side so AJAX calls the page itself makes (which
   carry the same `X-Debug-Token` header, since they're profiled requests
   too) show up live in the toolbar's HTTP list.

4. **Ephemeral by default, safe by construction.** `enabled` defaults to
   `false` — must be explicitly turned on (`NODE_ENV === 'development'`).
   `RequestCollector` masks configurable sensitive keys (`security/masker.ts`,
   default list in `DEFAULT_SENSITIVE_KEYS`: `authorization`, `cookie`,
   `password`, `token`, `session`, ...) in headers/cookies/query/body
   **before** the data is ever persisted. Nunjucks' `autoescape: true` covers
   template output; `security/escape.ts` and `toolbar.js`'s own
   `escapeHtml()` cover the two spots that build HTML by hand. `profiler.clear()`
   purges everything on demand.


## Integration example

```ts
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();


instrumentMySQL(OptiCoreMySQLDriver);
instrumentLogger(LoggerCore);

const profiler = opticoreProfiler({
    enabled: process.env.NODE_ENV === "development",
    storage: new FileStorage(".profiler-cache"), 
});

app.use(profiler);                       
registerProfilerViews(app, profiler);      

app.use(/* ...your feature routers, including createProfilerRouter(profiler)... */);

app.use(profilerErrorHandler());          
app.use(yourOwnErrorHandler);              
```

### Instrumenting 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:

```ts
import { Pool } from "pg";
import {
    instrumentPostgres,   
    instrumentMariaDB,    
    instrumentOracleDB,   
    instrumentMongoDB,   
    instrumentDatabase,  
} from "opticore-profiler";

instrumentPostgres(Pool);
instrumentMariaDB(MariaDbConnection);
instrumentOracleDB(OracleConnection);
instrumentMongoDB(MongoCollection);

instrumentDatabase(SomeDriver, {
    type: "my-db",
    method: "runQuery",
    extract: (sql: string, params?: unknown[]) => ({ sql, bindings: params }),
});
```
