import { EventEmitter } from 'events'; import { Request, Response, RequestHandler, NextFunction, Application } from 'opticore-express'; import { TFeatureRoutes } from 'opticore-router'; /** * Structural type covering any driver/client class — deliberately loose since every * DB library shapes its prototype differently. * * `prototype` is typed as `object`, not `Record`: a real * TypeScript class (e.g. `mysql2`'s `Connection`) has no index signature on * its instance type, so it isn't assignable to `Record` — * callers would be forced to cast (`as unknown as InstrumentableClassInterface`) * at every `instrumentMySQL(...)`/`instrumentDatabase(...)` call site. `object` * accepts any class as-is; the instrumentation functions do their own * internal cast to index into the prototype by method name. */ interface InstrumentableClassInterface { prototype: object; } interface QueryDescriptorInterface { sql: string; bindings?: unknown[]; } interface DatabaseInstrumentationOptionsInterface { /** * Label recorded on every query so collectors/UI can tell drivers apart * (e.g. "mysql", "postgres", "mongodb", "mariadb", "oracledb"). * */ type: string; /** * Name of the prototype method that runs a query/command * (e.g. "query", "execute", "makeQuery", "find"). * */ method: string; /** * Turns the wrapped method's own arguments into what gets recorded — every driver shapes * these differently. * */ extract: (...args: TArgs) => QueryDescriptorInterface; /** * Extra path/name fragments to skip while walking the stack for the call site, * on top of this file itself. */ excludeFromStack?: string[]; } interface DriverInstrumentationOptionsInterface { /** * Override the prototype method being wrapped, if the driver names it differently. */ method?: string; } interface MongoInstrumentationOptionsInterface { /** * Override the list of Collection methods to wrap. * Defaults to the common CRUD/aggregation ops. */ methods?: string[]; } type SqlArgType = string | { sql: string; [key: string]: unknown; }; interface PostgresQueryConfigInterface { text: string; values?: unknown[]; [key: string]: unknown; } type PostgresQueryArgType = string | PostgresQueryConfigInterface; /** * A DataCollector observes one facet of a request/response cycle. Collectors * never see or alter business code: they are wired up once, at bootstrap, via * decoration (DB client, logger) or via the profiler middleware's own hooks * (res.on('finish'), Express error middleware). `collect()` is called exactly * once per profiled request, after the response has been sent. */ interface CollectorContext { token: string; req: Request; res: Response; /** Date.now() captured when the profiler middleware started handling this request. */ startTime: number; /** process.hrtime.bigint() captured at the same instant, for sub-ms precision. */ startHrTime: bigint; /** process.memoryUsage().heapUsed captured at the same instant. */ heapUsedBefore: number; /** The response body exactly as sent to the client, captured by the profiler middleware's res.end() interception (installHtmlInjection) — undefined when nothing was ever written. */ responseBody?: { buffer: Buffer; contentType: string; }; } interface DataCollector { /** Unique, stable identifier — also the key under which data is stored in Profile.collectors. */ getName(): string; /** Called once, after the response has finished sending. May be async (e.g. flushing buffers). */ collect(ctx: CollectorContext, error?: Error | null): void | Promise; /** Returns the JSON-serializable data gathered by this collector. */ getData(): T; /** Restores the collector to its initial state. Registry creates one instance per request, so * reset() mainly matters for collectors that are reused (e.g. tests). */ reset(): void; } type CollectorFactory = () => DataCollector; interface Profile { token: string; ip: string; method: string; url: string; statusCode: number; /** Unix ms timestamp — when the request started. */ time: number; /** Total request duration in ms — convenience mirror of the "time" collector's data. */ duration: number; /** Response Content-Type — convenience mirror of the "request" collector's data, kept at the top level (like duration) so lightweight index entries (FileStorage.find(), which never load collector payloads) can still tell an HTML page navigation apart from a JSON/API response. */ contentType?: string; /** collector name -> collector.getData() */ collectors: Record; } interface ProfilerStorage { write(profile: Profile): Promise | void; read(token: string): Promise | Profile | undefined; /** Most recent profiles first. */ find(limit?: number): Promise | Profile[]; /** Removes profiles older than `olderThanMs` (relative to Date.now()). No-op if omitted from options. */ purge(olderThanMs?: number): Promise | void; clear(): Promise | void; } interface SecurityConfig { /** Header/body/query keys that get masked before being persisted or displayed (case-insensitive). */ sensitiveKeys: string[]; maxUrlLength: number; } interface ProfilerOptions { /** Master switch. Defaults to false — must be explicitly enabled (e.g. NODE_ENV === 'development'). */ enabled: boolean; /** URL prefix under which the profiler UI/API is mounted. Requests under this prefix are never profiled. */ routePrefix: string; storage: ProfilerStorage; /** Collector factories to run for every profiled request. Defaults to the 6 built-in collectors. */ collectors?: CollectorFactory[]; /** Retention window in ms for automatic purge (0 disables automatic purge). */ retentionMs?: number; security?: Partial; /** Max number of AJAX/main requests remembered in the toolbar's HTTP list. */ maxRequests?: number; /** * Paths never profiled — no token, no toolbar injection, no entry in the * list/SSE/toolbar HTTP-requests table. Exact match or a trailing "/*" * prefix match (e.g. "/.well-known/*"). Defaults to the browser-noise * requests every site gets for free (favicon, Chrome DevTools probe) so * the HTTP-requests table only ever shows requests the app actually * handles as business logic. */ excludePaths?: string[]; } declare const DEFAULT_EXCLUDE_PATHS: readonly string[]; declare const DEFAULT_SENSITIVE_KEYS: readonly string[]; interface ISqlQuery { sql: string; duration: number; timestamp: number; bindings?: unknown[]; error?: string; type?: string; /** Origin call site, captured non-intrusively at the DB client decoration point. */ stack?: string; } interface ILogEntry { level: "debug" | "info" | "warning" | "error" | "critical" | "deprecation"; message: string; timestamp: number; context?: Record; } interface IRouteInfo { path: string; method: string; handler?: string; middlewares?: string[]; params?: Record; controller?: string; } interface IRequestInfo { method: string; url: string; headers: Record; query: Record; body: Record; params: Record; ip: string; cookies?: Record; protocol?: string; hostname?: string; /** `req.session`'s own data properties (e.g. express-session) — undefined when no session middleware is installed, distinct from an empty/absent session object so the Session tab can tell "not tracked" apart from "tracked but empty". */ session?: Record; } interface IResponseInfo { statusCode: number; statusMessage: string; headers: Record; contentType?: string; /** Parsed from Set-Cookie response header(s), one entry per cookie name -> value — masked per-name like request cookies (see Masker), not blanket-redacted like the raw Set-Cookie header in `headers`. */ cookies?: Record; body?: unknown; } /** * Central registry of collector factories. The profiler middleware asks the * registry for a fresh set of collector *instances* at the start of every * profiled request (never reusing instances across requests — that would * leak data between unrelated requests running concurrently). * * Applications register custom collectors the same way built-ins are * registered: `registry.register(() => new MyCollector())`. */ declare class CollectorRegistry { private readonly factories; register(factory: CollectorFactory): void; unregister(name: string): void; has(name: string): boolean; names(): string[]; /** Builds one fresh instance per registered factory, keyed by collector name. */ createAll(): Map; } interface OpticoreProfiler extends RequestHandler { registry: CollectorRegistry; options: Readonly> & { security: SecurityConfig; }>; /** Purges every stored profile. */ clear(): Promise | void; /** * Emits a "profile" event, with the lightweight IndexEntry-shaped * summary, right after every profile is persisted — the push side of the * profiler list's live updates (GET /_profiler/stream), so the list page * never has to poll the server. */ events: EventEmitter; } /** * Builds the profiler middleware. This is the single integration point an * application needs: `app.use(opticoreProfiler({ storage }))`. * * Per request (when enabled and outside routePrefix): * 1. generates a token and a fresh set of collector instances, * 2. runs the rest of the request inside an AsyncLocalStorage context so * instrumented code (DB driver, logger) can reach those collectors, * 3. on res.on('finish'), asks every collector for its data, assembles a * Profile and hands it to `storage.write()` — after the response has * already been sent, so this adds no perceived latency, * 4. optionally rewrites an HTML response to inject the toolbar loader * snippet before ``. */ declare function opticoreProfiler(options: Partial & { storage: ProfilerOptions["storage"]; }): OpticoreProfiler; /** * Express error-handling middleware (4-arg signature) that records the error * into the current request's ExceptionCollector, then forwards it unchanged * via next(err) — it never sends a response itself, so the application's own * error handling (JSON error responses, custom error pages, ...) behaves * exactly as if the profiler weren't there. Mount it after all routes, same * as any other Express error middleware: * * app.use(...routes) * app.use(profilerErrorHandler()) * app.use(myAppErrorHandler) */ declare function profilerErrorHandler(): (err: Error, _req: Request, _res: Response, next: NextFunction) => void; /** * Wires the profiler's *presentation* layer onto an Express app: the * Nunjucks view engine, static assets (CSS/JS) and the "/" landing page. * The profiler's own data routes (list/detail/wdt) are registered * separately via `createProfilerRouter(profiler)`, alongside the * application's other feature routers — see README.md for the full * integration example. * * Call this once, before `app.use(opticoreProfiler(...))` handles any * request, so the view engine and static assets are ready. */ declare function registerProfilerViews(app: Application, profiler: OpticoreProfiler): void; declare function createProfilerRouter(profiler: OpticoreProfiler): TFeatureRoutes; /** * Decorates any database driver/client's prototype so every query or command * it runs is timed and recorded against the request currently active on the * profiler's AsyncLocalStorage context — application code calling * `db.query(...)`, `collection.find(...)`, etc. stays completely unaware * this happens. Not tied to any single database: pass the method to wrap * and a small `extract` function turning that method's own arguments into * `{ sql, bindings }`, and it works for MySQL, MariaDB, Postgres, OracleDB, * MongoDB, or any other driver — see the presets below for ready-made ones. * Call once per (class, method) at bootstrap, before any request is * handled. Idempotent: re-wrapping the same method on the same class is a * no-op, so it's safe to call more than once (e.g. under hot reload). */ declare function instrumentDatabase(DriverClass: InstrumentableClassInterface, options: DatabaseInstrumentationOptionsInterface): void; /** * Extractor for drivers exposing * `query(sql, values)` / `makeQuery(sql, values)` * where `sql` is a string or a `{ sql }` object (mysql2, mariadb, opticore-mysqldb, ...). * */ declare function sqlQueryExtractor(sql: SqlArgType, values?: unknown[]): QueryDescriptorInterface; /** * Extractor for `pg`-style `query(text | { text, values }, values?)`. */ declare function postgresQueryExtractor(query: PostgresQueryArgType, values?: unknown[]): QueryDescriptorInterface; /** * Extractor for `oracledb`-style `execute(sql, binds?)`. * Named binds (an object rather than an array) aren't recorded — only positional binds are. */ declare function oracleQueryExtractor(sql: string, binds?: unknown[] | Record): QueryDescriptorInterface; /** * Extractor factory for MongoDB-style per-operation methods (`find`, `insertOne`, ...), * which take a document/filter instead of SQL text. */ declare function mongoCommandExtractor(methodName: string): (...args: unknown[]) => QueryDescriptorInterface; /** * Instruments a MySQL driver exposing `makeQuery(sql, values)` (e.g. `opticore-mysqldb`). */ declare function instrumentMySQL(DriverClass: InstrumentableClassInterface, options?: DriverInstrumentationOptionsInterface): void; /** * Instruments a MariaDB driver exposing `query(sql, values)`. */ declare function instrumentMariaDB(DriverClass: InstrumentableClassInterface, options?: DriverInstrumentationOptionsInterface): void; /** * Instruments a Postgres client exposing `query(text | config, values)` (e.g. `pg`'s `Client`/`Pool`). */ declare function instrumentPostgres(DriverClass: InstrumentableClassInterface, options?: DriverInstrumentationOptionsInterface): void; /** * Instruments an OracleDB connection exposing `execute(sql, binds)` (e.g. `oracledb`'s `Connection`). */ declare function instrumentOracleDB(DriverClass: InstrumentableClassInterface, options?: DriverInstrumentationOptionsInterface): void; /** * Instruments a MongoDB `Collection` class, wrapping each of its CRUD/aggregation * methods (find, insertOne, updateOne, ...). * */ declare function instrumentMongoDB(CollectionClass: InstrumentableClassInterface, options?: MongoInstrumentationOptionsInterface): void; interface ILogSuccessArg { title?: string; message?: string; } interface ILogInfoArg { title?: string; message?: string; } interface ILogWarnArg { title?: string; message: string; } interface ILogErrorArg { message: string; title?: string; errorType?: unknown; stackTrace?: unknown; httpCodeValue?: unknown; } interface ILogDeprecationArg { title?: string; message: string; } interface LoggerCoreLike { success(arg: ILogSuccessArg): void; info(arg: ILogInfoArg): void; warn(arg: ILogWarnArg): void; error(arg: ILogErrorArg): void; debug(message: string): void; deprecation(arg: ILogDeprecationArg): void; } interface LoggerCoreCtor { prototype: LoggerCoreLike; } /** * Decorates LoggerCore's prototype so every log call the application makes * (through its normal `logger.info(...)`, `logger.error(...)`, etc.) is * mirrored into the active request's LoggerCollector, in addition to * whatever the logger already does (console/file/remote transports keep * working unchanged). Call once at bootstrap. Idempotent. */ declare function instrumentLogger(LoggerClass: LoggerCoreCtor): void; interface IFormatLoggerLike { title?: string; typeName?: string; message?: string; } interface OpticoreLoggerLike { opticoreLog(arg: IFormatLoggerLike): void; serverLog(arg: IFormatLoggerLike): void; watcherLog(arg: IFormatLoggerLike): void; } interface OpticoreLoggerCtor { prototype: OpticoreLoggerLike; } /** * Decorates OpticoreLogger's prototype (opticore-server-logger) so every * `serverLog`/`opticoreLog`/`watcherLog` call — the "[Server]"/"[OptiCore]"/ * "[OptiCore Watcher]" banners — is mirrored into the active request's * LoggerCollector too, exactly like instrumentLogger does for LoggerCore. * Without this, only opticore-logger's calls ever reached the "Log * Messages" panel — anything logged through opticore-server-logger during * a request (e.g. dependency-container or request-lifecycle messages) * silently never showed up there. Call once at bootstrap. Idempotent. */ declare function instrumentServerLogger(OpticoreLoggerClass: OpticoreLoggerCtor): void; /** * Generates a short (default 8 chars) alphanumeric token by rejection-sampling * random bytes against a 36-symbol alphabet, so every character is unbiased. */ declare function generateToken(length?: number): string; declare function isValidToken(token: unknown): token is string; /** * Looks up the named collector for the request currently being handled on * this async chain. Returns undefined outside of a profiled request (e.g. * profiling disabled, or the call happened outside any HTTP request) — * callers must treat that as "do nothing", never throw. */ declare function getActiveCollector(name: string): T | undefined; /** * Circular buffer of the N most recent profiles, kept in process memory. * Lost on restart — pick FileStorage when profiles need to survive that. */ declare class MemoryStorage implements ProfilerStorage { private readonly limit; private readonly profiles; private readonly order; constructor(limit?: number); write(profile: Profile): void; read(token: string): Profile | undefined; find(limit?: number): Profile[]; purge(olderThanMs?: number): void; clear(): void; count(): number; } /** * Persists profiles as JSON files under `/.json`, plus a single * `/index.json` holding lightweight metadata (no collector payloads) * for the last N profiles, most-recent-first — reading the index to list * profiles never has to open every per-token file, mirroring Symfony's * FileProfilerStorage split between index and full data. */ declare class FileStorage implements ProfilerStorage { private readonly dir; private readonly maxIndexSize; private readonly indexPath; constructor(dir?: string, maxIndexSize?: number); private ensureDir; private profilePath; private readIndex; private writeIndex; write(profile: Profile): void; read(token: string): Profile | undefined; find(limit?: number): Profile[]; purge(olderThanMs?: number): void; clear(): void; private deleteFile; } /** * Masks sensitive values before they are persisted or rendered. Applied to * headers, cookies, query and body objects captured by RequestCollector — * masking happens at collection time (not at render time) so redacted data * never even reaches disk in FileStorage. */ declare class Masker { private readonly keys; constructor(sensitiveKeys?: readonly string[]); isSensitive(key: string): boolean; /** Shallow-masks a flat object (headers, query, cookies, single-level body). */ maskShallow>(obj: T | undefined | null): T; /** Recursively masks nested objects/arrays (request/response bodies). */ maskDeep(value: unknown, depth?: number): unknown; } interface RequestCollectorData { method: string; url: string; statusCode: number; statusMessage: string; request: IRequestInfo; response: IResponseInfo; route: IRouteInfo; } /** * Observes the request/response objects purely by reading their already-public * properties inside collect() — nothing here touches how the application * builds its request or sends its response. */ declare class RequestCollector implements DataCollector { private readonly masker; private data; constructor(masker?: Masker); getName(): string; /** Parses the buffered response body as JSON when the response declared a JSON content-type — undefined otherwise (HTML pages, empty bodies, non-JSON payloads), so the detail view falls back to its "no body captured" state instead of dumping raw bytes. */ private parseResponseBody; /** Parses Set-Cookie response header(s) into name -> value, mirroring how request cookies are captured — masked per cookie name (Masker), not blanket-redacted like the raw "set-cookie" header. */ private parseResponseCookies; /** * Reads `req.session` (express-session or any middleware that assigns a * plain, JSON-serializable object there) — own data properties only, * methods (save/destroy/reload/...) live on the prototype so a plain * Object.entries() already excludes them. Returns undefined (not `{}`) * when there's no session object at all, so the Session tab can tell * "this app doesn't use sessions" apart from "session exists but empty". */ private parseSession; collect(ctx: CollectorContext): void; getData(): RequestCollectorData; reset(): void; } interface TimePhase { name: string; /** ms elapsed since the request started. */ offset: number; } interface TimeCollectorData { startedAt: number; duration: number; phases: TimePhase[]; } /** * Total wall-clock duration plus any named checkpoints other code marks * along the way (e.g. "middleware.done", "handler.done") via mark(). * mark() is looked up through the request's AsyncLocalStorage-scoped * collector instance (context.getActiveCollector), so it never requires * threading a token through business code. */ declare class TimeCollector implements DataCollector { private data; private readonly phases; getName(): string; /** Records a named checkpoint at the current elapsed time, relative to the active request's start. */ mark(name: string): void; collect(ctx: CollectorContext): void; getData(): TimeCollectorData; reset(): void; } interface MemoryCollectorData { heapUsedBefore: number; heapUsedAfter: number; delta: number; } /** Heap usage before the request started vs. right after the response finished. */ declare class MemoryCollector implements DataCollector { private data; getName(): string; collect(ctx: CollectorContext): void; getData(): MemoryCollectorData; reset(): void; } /** * Holds the queries/commands recorded for one request, from any database * driver. Instances are never populated by business code directly — see * instrumentation/database.instrumentation.ts, which decorates a DB * driver/client once at bootstrap and pushes into whichever DatabaseCollector * is active on the current AsyncLocalStorage context. */ declare class DatabaseCollector implements DataCollector { private readonly queries; getName(): string; record(query: Omit): void; collect(_ctx: CollectorContext): void; getData(): ISqlQuery[]; reset(): void; } /** * Holds the log entries emitted for one request, grouped implicitly by level * (the view layer groups/counts them). Like DatabaseCollector, entries are * pushed in real time by instrumentation/logger.instrumentation.ts — business * code keeps calling `logger.info(...)` / `logger.error(...)` unmodified. */ declare class LoggerCollector implements DataCollector { private readonly logs; getName(): string; record(entry: Omit): void; collect(_ctx: CollectorContext): void; getData(): ILogEntry[]; byLevel(level: ILogEntry["level"]): ILogEntry[]; reset(): void; } interface ExceptionData { name: string; message: string; stack: string; } interface ExceptionCollectorData { error: ExceptionData | null; } /** * Captures the error that made a request fail, if any. Fed by: * - middleware/errorHandler.middleware.ts, an Express error-handling * middleware the app mounts after its routes (standard Express wiring — * not a change to business logic, same as mounting any other middleware). * - the process-level uncaughtException/unhandledRejection fallback, for * errors that escape Express's error-handling chain entirely. */ declare class ExceptionCollector implements DataCollector { private error; getName(): string; setError(err: Error): void; collect(_ctx: CollectorContext, error?: Error | null): void; getData(): ExceptionCollectorData; reset(): void; } export { type CollectorContext, type CollectorFactory, CollectorRegistry, DEFAULT_EXCLUDE_PATHS, DEFAULT_SENSITIVE_KEYS, type DataCollector, DatabaseCollector, type DatabaseInstrumentationOptionsInterface, type DriverInstrumentationOptionsInterface, ExceptionCollector, FileStorage, type InstrumentableClassInterface, LoggerCollector, MemoryCollector, MemoryStorage, type MongoInstrumentationOptionsInterface, type OpticoreProfiler, type PostgresQueryArgType, type PostgresQueryConfigInterface, type Profile, type ProfilerOptions, type ProfilerStorage, type QueryDescriptorInterface, RequestCollector, type SecurityConfig, type SqlArgType, TimeCollector, createProfilerRouter, generateToken, getActiveCollector, instrumentDatabase, instrumentLogger, instrumentMariaDB, instrumentMongoDB, instrumentMySQL, instrumentOracleDB, instrumentPostgres, instrumentServerLogger, isValidToken, mongoCommandExtractor, opticoreProfiler, oracleQueryExtractor, postgresQueryExtractor, profilerErrorHandler, registerProfilerViews, sqlQueryExtractor };