@singhak/nodeui-core 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,6 +1,17 @@
1
1
  import { IncomingMessage, ServerResponse } from 'node:http';
2
2
  import { Readable } from 'node:stream';
3
3
 
4
+ interface OtlpOptions {
5
+ /** Collector base URL (e.g. `http://localhost:4318`) or a full `/v1/traces` URL. */
6
+ endpoint: string;
7
+ /** `service.name` resource attribute. Default `OTEL_SERVICE_NAME` or `nodeui-app`. */
8
+ serviceName?: string;
9
+ /** Extra HTTP headers, e.g. an auth header for a hosted collector. */
10
+ headers?: Record<string, string>;
11
+ /** Export interval. Default 2000 ms. */
12
+ intervalMs?: number;
13
+ }
14
+
4
15
  /**
5
16
  * Shared typed contract for the NodeUI developer console.
6
17
  *
@@ -8,7 +19,8 @@ import { Readable } from 'node:stream';
8
19
  * contract. Every provider returns a {@link ProviderResult}; every REST
9
20
  * endpoint returns an {@link ApiEnvelope} which is the same shape.
10
21
  */
11
- type BuiltInPanelId = 'health' | 'memory' | 'cpu' | 'event-loop' | 'heap-snapshot' | 'startup' | 'requests' | 'env' | 'routes' | 'logs' | 'metrics' | 'outgoing';
22
+
23
+ type BuiltInPanelId = 'health' | 'memory' | 'cpu' | 'event-loop' | 'heap-snapshot' | 'startup' | 'requests' | 'env' | 'routes' | 'logs' | 'metrics' | 'outgoing' | 'errors' | 'queries';
12
24
  /**
13
25
  * Panel identifier. Built-in panels are listed in {@link BuiltInPanelId};
14
26
  * plugins may register any other string id (lowercase letters, digits, `-`).
@@ -34,6 +46,16 @@ interface NodeUIConfig {
34
46
  activationReason: string;
35
47
  /** Whether secret masking is applied to panel output. Default true. */
36
48
  maskSecrets: boolean;
49
+ /** What extra request detail is recorded. */
50
+ requestDetail: Required<RequestDetailOptions>;
51
+ /** `'always'` runs outgoing/query/log capture from startup; `'lazy'` only while a panel is open. */
52
+ capture: 'always' | 'lazy';
53
+ /** Journal file for persisting recent activity across restarts, or null. */
54
+ persistFile: string | null;
55
+ /** Rotation threshold for the journal. */
56
+ persistMaxBytes: number;
57
+ /** OTLP/HTTP trace export settings, or null when disabled. */
58
+ otlp: OtlpOptions | null;
37
59
  /** Idle time after which background samplers stop. Default 60000. */
38
60
  inactivityTimeoutMs: number;
39
61
  /** TTL for mutation confirmation nonces. Default 60000. */
@@ -154,6 +176,30 @@ interface RequestEntry {
154
176
  durationMs: number;
155
177
  timestampMs: number;
156
178
  ip: string;
179
+ /** Matched route pattern (e.g. `/users/:id`) when the framework exposes it. */
180
+ route?: string;
181
+ /** Query parameters (secret-looking keys redacted). */
182
+ query?: Record<string, string>;
183
+ /** Request headers (credentials always redacted). */
184
+ headers?: Record<string, string>;
185
+ /** Captured request body (opt-in, size-capped, masked). */
186
+ requestBody?: string;
187
+ requestBodyTruncated?: boolean;
188
+ /** Captured response body (opt-in, size-capped, masked). */
189
+ responseBody?: string;
190
+ responseBodyTruncated?: boolean;
191
+ }
192
+ /** The optional, privacy-sensitive part of a {@link RequestEntry}. */
193
+ type RequestDetail = Pick<RequestEntry, 'query' | 'headers' | 'requestBody' | 'requestBodyTruncated' | 'responseBody' | 'responseBodyTruncated'>;
194
+ interface RequestDetailOptions {
195
+ /** Record query parameters. Default true. */
196
+ query?: boolean;
197
+ /** Record request headers (credential headers are always redacted). Default true. */
198
+ headers?: boolean;
199
+ /** Record request/response bodies of textual content types. Default false. */
200
+ bodies?: boolean;
201
+ /** Max bytes kept per body. Default 4096, max 1048576. */
202
+ maxBodyBytes?: number;
157
203
  }
158
204
  interface RouteStat {
159
205
  method: string;
@@ -208,6 +254,8 @@ interface LogEntry {
208
254
  level: LogLevel;
209
255
  message: string;
210
256
  timestamp: number;
257
+ /** Id of the app request being handled when this was logged. */
258
+ requestId?: number;
211
259
  }
212
260
  interface LogsData {
213
261
  entries: LogEntry[];
@@ -229,6 +277,53 @@ interface OutgoingRequestEntry {
229
277
  durationMs: number;
230
278
  timestampMs: number;
231
279
  error?: string;
280
+ /** Id of the app request that triggered this call. */
281
+ requestId?: number;
282
+ }
283
+ interface QueryEntry {
284
+ id: number;
285
+ /** Driver or ORM the query came from, e.g. `pg`, `mysql2`, `prisma`. */
286
+ system: string;
287
+ /** Statement text. Parameter values are never recorded. */
288
+ sql: string;
289
+ durationMs: number;
290
+ timestampMs: number;
291
+ rowCount?: number;
292
+ error?: string;
293
+ /** Id of the app request that issued the query. */
294
+ requestId?: number;
295
+ /** At or above the configured `slowQueryMs`. */
296
+ slow?: boolean;
297
+ /** Same statement repeated within one request (N+1 suspect). */
298
+ nPlusOne?: boolean;
299
+ repeats?: number;
300
+ }
301
+ interface QueriesData {
302
+ total: number;
303
+ failed: number;
304
+ slow: number;
305
+ nPlusOneGroups: number;
306
+ slowQueryMs: number;
307
+ entries: QueryEntry[];
308
+ }
309
+ type ErrorSource = 'request' | 'uncaught' | 'rejection' | 'manual';
310
+ /** Errors with the same fingerprint (type, message shape, top frames) grouped together. */
311
+ interface ErrorGroup {
312
+ id: string;
313
+ name: string;
314
+ message: string;
315
+ stack: string;
316
+ source: ErrorSource;
317
+ count: number;
318
+ firstSeenMs: number;
319
+ lastSeenMs: number;
320
+ lastRequestId?: number;
321
+ lastRoute?: string;
322
+ lastStatus?: number;
323
+ }
324
+ interface ErrorsData {
325
+ total: number;
326
+ groups: ErrorGroup[];
232
327
  }
233
328
  interface OutgoingData {
234
329
  total: number;
@@ -316,9 +411,13 @@ interface ActivationDecision {
316
411
  declare function resolveActivation(env: Record<string, string | undefined>): ActivationDecision;
317
412
  /** True when the socket address is a loopback address. */
318
413
  declare function isLoopbackAddress(address: string | undefined): boolean;
414
+ /** True when `entry` is an exact IP or an IPv4/IPv6 CIDR that {@link matchesAddress} understands. */
415
+ declare function isValidAddressEntry(entry: string): boolean;
319
416
  /**
320
- * True when `address` matches one of `allowed`, each an exact IP or an IPv4
321
- * CIDR (e.g. `172.17.0.0/16`, the default Docker bridge network).
417
+ * True when `address` matches one of `allowed`, each an exact IP or a CIDR
418
+ * (e.g. `172.17.0.0/16`, the default Docker bridge network, or `fd00::/8`).
419
+ * IPv4-mapped IPv6 addresses compare as their IPv4 form; IPv6 literals compare
420
+ * by value, so `::1` equals `0:0:0:0:0:0:0:1`.
322
421
  */
323
422
  declare function matchesAddress(address: string | undefined, allowed: readonly string[]): boolean;
324
423
  /** Extracts the lower-cased hostname from a `Host` header value (no port). */
@@ -391,6 +490,12 @@ declare class ProviderRegistry {
391
490
  all(): NodeUIProvider[];
392
491
  }
393
492
 
493
+ /**
494
+ * Directory holding the prebuilt console (`index.html` and assets). It ships
495
+ * inside this package, so the UI always matches the API it talks to. Use it to
496
+ * serve the console from your own host or gateway.
497
+ */
498
+ declare function uiAssetsDir(): string;
394
499
  interface StaticAsset {
395
500
  content: Readable;
396
501
  contentType: string;
@@ -523,6 +628,89 @@ declare class HealthProvider implements NodeUIProvider<HealthData> {
523
628
  }>;
524
629
  }
525
630
 
631
+ /** Same normalized statement this many times within one request is flagged as N+1. */
632
+ declare const N_PLUS_ONE_THRESHOLD = 5;
633
+ type Recorder$1 = (entry: Omit<QueryEntry, 'id'>) => void;
634
+ /** Loads an optional driver from the host app's dependency tree. */
635
+ type DriverLoader = (name: string) => unknown;
636
+ /** Collapses literals so repeated statements compare equal. */
637
+ declare function normalizeSql(sql: string): string;
638
+ /**
639
+ * Refcounted instrumentation of installed database drivers (`pg`, `mysql2`),
640
+ * resolved from the host app's dependencies. Drivers that are not installed are
641
+ * skipped. The last release restores the original methods.
642
+ */
643
+ declare function interceptQueries(record: Recorder$1, load?: DriverLoader): () => void;
644
+ interface QueriesProviderOptions {
645
+ size: number;
646
+ slowQueryMs: number;
647
+ loader?: DriverLoader;
648
+ }
649
+ /** Recent database queries with slow-query and N+1 flags. */
650
+ declare class QueriesProvider implements NodeUIProvider<QueriesData> {
651
+ private readonly options;
652
+ readonly id: "queries";
653
+ private buffer;
654
+ private nextId;
655
+ private release;
656
+ private failed;
657
+ /** Called with every newly recorded entry (persistence hook). */
658
+ onRecord?: (entry: QueryEntry) => void;
659
+ constructor(options: QueriesProviderOptions);
660
+ start(): void;
661
+ stop(): void;
662
+ record(entry: Omit<QueryEntry, 'id'>): void;
663
+ /** Re-inserts journaled entries without re-emitting them. */
664
+ restore(entries: readonly QueryEntry[]): void;
665
+ get(): {
666
+ ok: true;
667
+ data: QueriesData;
668
+ };
669
+ }
670
+
671
+ /** A single occurrence in a serializable form (for the persistence journal). */
672
+ interface ErrorEvent {
673
+ name: string;
674
+ message: string;
675
+ stack: string;
676
+ source: ErrorSource;
677
+ at: number;
678
+ requestId?: number;
679
+ route?: string;
680
+ status?: number;
681
+ }
682
+ /** Groups equal failures: same type, same message shape (numbers and ids collapsed), same origin. */
683
+ declare function fingerprintError(name: string, message: string, stack: string): string;
684
+ /**
685
+ * Groups errors by fingerprint with counts. Process-level failures are observed
686
+ * with `uncaughtExceptionMonitor`, which never changes Node's crash behaviour
687
+ * (unlike an `uncaughtException` or `unhandledRejection` listener, which would
688
+ * suppress the default exit).
689
+ */
690
+ declare class ErrorsProvider implements NodeUIProvider<ErrorsData> {
691
+ readonly id: "errors";
692
+ private groups;
693
+ private total;
694
+ private listener;
695
+ /** Called with every newly recorded occurrence (persistence hook). */
696
+ onRecord?: (event: ErrorEvent) => void;
697
+ /** Starts observing process-level failures. Idempotent. */
698
+ attach(): void;
699
+ detach(): void;
700
+ record(error: unknown, source: ErrorSource, context?: {
701
+ requestId?: number;
702
+ route?: string;
703
+ status?: number;
704
+ }): void;
705
+ /** Re-applies journaled occurrences without re-emitting them. */
706
+ restore(events: readonly ErrorEvent[]): void;
707
+ private apply;
708
+ get(): {
709
+ ok: true;
710
+ data: ErrorsData;
711
+ };
712
+ }
713
+
526
714
  type Recorder = (entry: Omit<OutgoingRequestEntry, 'id'>) => void;
527
715
  /**
528
716
  * Refcounted instrumentation of outgoing HTTP calls made via `http`,
@@ -537,10 +725,14 @@ declare class OutgoingProvider implements NodeUIProvider<OutgoingData> {
537
725
  private nextId;
538
726
  private failed;
539
727
  private release;
728
+ /** Called with every newly recorded entry (persistence hook). */
729
+ onRecord?: (entry: OutgoingRequestEntry) => void;
540
730
  constructor(size: number);
541
731
  start(): void;
542
732
  stop(): void;
543
733
  record(entry: Omit<OutgoingRequestEntry, 'id'>): void;
734
+ /** Re-inserts journaled entries without re-emitting them. */
735
+ restore(entries: readonly OutgoingRequestEntry[]): void;
544
736
  get(): {
545
737
  ok: true;
546
738
  data: OutgoingData;
@@ -568,8 +760,14 @@ declare class RequestsProvider implements NodeUIProvider<RequestsData> {
568
760
  readonly id: "requests";
569
761
  private buffer;
570
762
  private nextId;
763
+ /** Called with every newly recorded entry (persistence hook). */
764
+ onRecord?: (entry: RequestEntry) => void;
571
765
  constructor(size: number);
572
- record(entry: Omit<RequestEntry, 'id'>): void;
766
+ /** Reserves an id up front so in-flight work can be attributed to the request. */
767
+ reserveId(): number;
768
+ record(entry: Omit<RequestEntry, 'id'>, id?: number): void;
769
+ /** Re-inserts journaled entries without re-emitting them. */
770
+ restore(entries: readonly RequestEntry[]): void;
573
771
  get length(): number;
574
772
  get(): {
575
773
  ok: true;
@@ -670,6 +868,21 @@ declare class LogsProvider implements NodeUIProvider<LogsData> {
670
868
  };
671
869
  }
672
870
 
871
+ declare const DEFAULT_REQUEST_DETAIL: Required<RequestDetailOptions>;
872
+
873
+ /** Id of the app request the current async call chain belongs to, if any. */
874
+ declare function currentRequestId(): number | undefined;
875
+ /**
876
+ * Runs `fn` inside the request's context when it was recorded by the NodeUI
877
+ * middleware (otherwise just calls `fn`). For adapters that own the `next` call.
878
+ */
879
+ declare function runInRequestContext<T>(req: IncomingMessage, fn: () => T): T;
880
+ /**
881
+ * Attributes the rest of the current async chain to the request. For adapters
882
+ * (Fastify) whose hooks cannot wrap the downstream handler in `run`.
883
+ */
884
+ declare function enterRequestContext(req: IncomingMessage): void;
885
+
673
886
  interface SseStream {
674
887
  send(payload: unknown): void;
675
888
  heartbeat(): void;
@@ -702,6 +915,38 @@ interface NodeUIOptions {
702
915
  env?: Record<string, string | undefined>;
703
916
  /** Whether secret masking applies to panel output. Default true. */
704
917
  maskSecrets?: boolean;
918
+ /**
919
+ * Extra detail recorded per request: query, headers (credentials always
920
+ * redacted) and, opt-in, textual bodies (or `NODEUI_CAPTURE_BODIES=true`).
921
+ * `false` records none of it.
922
+ */
923
+ captureRequestDetail?: boolean | RequestDetailOptions;
924
+ /**
925
+ * Keep recent requests, outgoing calls, queries and errors in an NDJSON
926
+ * journal so they survive a restart (or `NODEUI_PERSIST_FILE`). Off by
927
+ * default. The file is private (0600), masked, and rotated at `maxBytes`
928
+ * (default 5 MiB). It holds whatever the console records, so keep it out of
929
+ * version control.
930
+ */
931
+ persist?: string | {
932
+ file: string;
933
+ maxBytes?: number;
934
+ };
935
+ /**
936
+ * When the outgoing-call, query and log instrumentation runs. `'always'`
937
+ * (default) records from startup, so you can open the console after a problem
938
+ * and still see what led to it. `'lazy'` installs the hooks only while a panel
939
+ * is open and removes them after `inactivityTimeoutMs`, for zero cost when
940
+ * nobody is looking (or `NODEUI_CAPTURE=lazy`). Buffers are bounded either way.
941
+ */
942
+ capture?: 'always' | 'lazy';
943
+ /**
944
+ * Export requests, outgoing calls and queries as OTLP/HTTP JSON spans to a
945
+ * collector (or `NODEUI_OTLP_ENDPOINT`). Off by default. Enabling it keeps
946
+ * outgoing and query instrumentation running and sends (masked) telemetry to
947
+ * the given endpoint, so point it at a local collector.
948
+ */
949
+ otlp?: string | OtlpOptions;
705
950
  /** Idle time after which background samplers stop. Default 60000. */
706
951
  inactivityTimeoutMs?: number;
707
952
  /** TTL for mutation confirmation nonces. Default 60000. */
@@ -732,6 +977,10 @@ interface NodeUIOptions {
732
977
  healthChecks?: Record<string, HealthCheck>;
733
978
  /** Max recorded outgoing HTTP calls. Default 200. */
734
979
  outgoingLogSize?: number;
980
+ /** Max recorded database queries. Default 200. */
981
+ queryLogSize?: number;
982
+ /** Queries at or above this duration are flagged slow. Default 100 ms. */
983
+ slowQueryMs?: number;
735
984
  }
736
985
  interface NodeUIServer {
737
986
  readonly config: NodeUIConfig;
@@ -755,6 +1004,35 @@ interface NodeUIServer {
755
1004
  level: LogLevel;
756
1005
  message: string;
757
1006
  }): void;
1007
+ /**
1008
+ * Records a database query from an ORM or driver hook that NodeUI does not
1009
+ * patch itself (Sequelize `logging`, TypeORM logger, ...). Parameters are not recorded.
1010
+ */
1011
+ recordQuery(query: {
1012
+ system: string;
1013
+ sql: string;
1014
+ durationMs: number;
1015
+ rowCount?: number;
1016
+ error?: string;
1017
+ }): void;
1018
+ /**
1019
+ * Subscribes to a Prisma client's query events. Create the client with
1020
+ * `log: [{ emit: 'event', level: 'query' }]`.
1021
+ */
1022
+ trackPrisma(client: {
1023
+ $on(event: 'query', cb: (e: {
1024
+ query: string;
1025
+ duration: number | bigint;
1026
+ }) => void): void;
1027
+ }): void;
1028
+ /**
1029
+ * Records a handled error (e.g. from a framework error hook) so it appears in
1030
+ * the Errors panel, attributed to the current request when there is one.
1031
+ */
1032
+ recordError(error: unknown, context?: {
1033
+ route?: string;
1034
+ status?: number;
1035
+ }): void;
758
1036
  /** Stops all timers and background samplers. */
759
1037
  shutdown(): void;
760
1038
  }
@@ -766,4 +1044,4 @@ type NodeUIMiddleware = (req: IncomingMessage, res: ServerResponse, next: () =>
766
1044
  declare function serializeEnvelope<T>(envelope: ApiEnvelope<T>, mask?: boolean): string;
767
1045
  declare function createNodeUI(options?: NodeUIOptions): NodeUIServer;
768
1046
 
769
- export { AUTH_COOKIE, type ActivationDecision, type ApiEnvelope, type BuiltInPanelId, type ConfigData, type ConfirmIssued, ConfirmationStore, type CpuClock, type CpuData, CpuProvider, type CpuUsageSnapshot, DEFAULT_LOG_SIZE, type EnvData, type EnvEntry, EnvProvider, type EventLoopClock, EventLoopLagProvider, type EventLoopSample, type GuardOptions, type GuardResult, type HealthCheck, type HealthCheckResult, type HealthData, HealthProvider, type HeapSnapshotData, type HeapSnapshotPanelData, HeapSnapshotProvider, type LogEntry, type LogLevel, type LogsData, LogsProvider, type MemoryData, MemoryProvider, type MetricsBucket, type MetricsData, MetricsProvider, type NodeUIConfig, type NodeUIMiddleware, type NodeUIOptions, type NodeUIProvider, type NodeUIServer, type OutgoingData, OutgoingProvider, type OutgoingRequestEntry, type PanelId, type ProviderContext, type ProviderError, ProviderRegistry, type ProviderResult, type RequestEntry, type RequestsData, RequestsProvider, type RequestsSummary, RingBuffer, type RouteEntry, type RouteStat, type RoutesData, RoutesProvider, SECRET_KEY_PATTERN, SECRET_MASKED, Sampler, type SamplerOptions, type SseStream, type StartupData, type StartupMark, StartupTracker, type StaticAsset, createGuard, createNodeUI, extractRoutes, hostnameFromHostHeader, interceptConsole, interceptOutgoing, isLoopbackAddress, isLoopbackHostname, maskSecretText, maskSecrets, matchesAddress, resolveActivation, resolveStaticAsset, serializeEnvelope, startSse, summarizeRequests };
1047
+ export { AUTH_COOKIE, type ActivationDecision, type ApiEnvelope, type BuiltInPanelId, type ConfigData, type ConfirmIssued, ConfirmationStore, type CpuClock, type CpuData, CpuProvider, type CpuUsageSnapshot, DEFAULT_LOG_SIZE, DEFAULT_REQUEST_DETAIL, type EnvData, type EnvEntry, EnvProvider, type ErrorGroup, type ErrorSource, type ErrorsData, ErrorsProvider, type EventLoopClock, EventLoopLagProvider, type EventLoopSample, type GuardOptions, type GuardResult, type HealthCheck, type HealthCheckResult, type HealthData, HealthProvider, type HeapSnapshotData, type HeapSnapshotPanelData, HeapSnapshotProvider, type LogEntry, type LogLevel, type LogsData, LogsProvider, type MemoryData, MemoryProvider, type MetricsBucket, type MetricsData, MetricsProvider, N_PLUS_ONE_THRESHOLD, type NodeUIConfig, type NodeUIMiddleware, type NodeUIOptions, type NodeUIProvider, type NodeUIServer, type OutgoingData, OutgoingProvider, type OutgoingRequestEntry, type PanelId, type ProviderContext, type ProviderError, ProviderRegistry, type ProviderResult, type QueriesData, QueriesProvider, type QueryEntry, type RequestDetail, type RequestDetailOptions, type RequestEntry, type RequestsData, RequestsProvider, type RequestsSummary, RingBuffer, type RouteEntry, type RouteStat, type RoutesData, RoutesProvider, SECRET_KEY_PATTERN, SECRET_MASKED, Sampler, type SamplerOptions, type SseStream, type StartupData, type StartupMark, StartupTracker, type StaticAsset, createGuard, createNodeUI, currentRequestId, enterRequestContext, extractRoutes, fingerprintError, hostnameFromHostHeader, interceptConsole, interceptOutgoing, interceptQueries, isLoopbackAddress, isLoopbackHostname, isValidAddressEntry, maskSecretText, maskSecrets, matchesAddress, normalizeSql, resolveActivation, resolveStaticAsset, runInRequestContext, serializeEnvelope, startSse, summarizeRequests, uiAssetsDir };