opencons 0.1.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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +382 -0
  3. package/opencons.d.ts +55 -0
  4. package/package.json +73 -0
  5. package/scripts/vendor-d3.js +22 -0
  6. package/src/core/context.js +44 -0
  7. package/src/core/index.js +198 -0
  8. package/src/core/tracer.js +252 -0
  9. package/src/drivers/db-language.js +207 -0
  10. package/src/drivers/detect.js +62 -0
  11. package/src/drivers/drizzle.js +87 -0
  12. package/src/drivers/index.js +43 -0
  13. package/src/drivers/mongoose.js +89 -0
  14. package/src/drivers/mysql2.js +116 -0
  15. package/src/drivers/pg.js +130 -0
  16. package/src/drivers/prisma.js +109 -0
  17. package/src/drivers/record.js +158 -0
  18. package/src/index.js +28 -0
  19. package/src/integrations/nest-lifecycle.js +357 -0
  20. package/src/integrations/nest.js +89 -0
  21. package/src/interceptors/express.js +270 -0
  22. package/src/interceptors/require-hook.js +109 -0
  23. package/src/lib/config.js +139 -0
  24. package/src/lib/errors.js +54 -0
  25. package/src/lib/http-response.js +37 -0
  26. package/src/lib/logger.js +69 -0
  27. package/src/lib/serialize.js +22 -0
  28. package/src/server/static.js +165 -0
  29. package/src/server/ws.js +62 -0
  30. package/src/store/source-cache.js +120 -0
  31. package/src/store/trace-store.js +117 -0
  32. package/src/transform/ast.js +255 -0
  33. package/src/transform/natural-language.js +146 -0
  34. package/src/transform/probe.js +161 -0
  35. package/src/transform/register.js +44 -0
  36. package/src/utils/label.js +26 -0
  37. package/src/utils/observable.js +103 -0
  38. package/widget/app.js +356 -0
  39. package/widget/db-language.js +90 -0
  40. package/widget/graph.js +1167 -0
  41. package/widget/index.html +132 -0
  42. package/widget/styles.css +773 -0
  43. package/widget/timeline.js +57 -0
  44. package/widget/vendor/d3.min.js +2 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RouteGrapher Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,382 @@
1
+ # Opencons
2
+
3
+ **Live runtime execution tracing for Node.js / Express**
4
+
5
+ Opencons automatically captures and visualises the complete execution path of every HTTP request passing through your Express application — in real time, with zero instrumentation code in your handlers.
6
+
7
+ > **Development only.** Opencons is disabled when `NODE_ENV=production` unless you explicitly pass `enabled: true`.
8
+
9
+ ---
10
+
11
+ ## Table of contents
12
+
13
+ - [Features](#features)
14
+ - [Prerequisites](#prerequisites)
15
+ - [Installation](#installation)
16
+ - [Quick start](#quick-start)
17
+ - [Configuration](#configuration)
18
+ - [Environment variables](#environment-variables)
19
+ - [NestJS integration](#nestjs-integration)
20
+ - [Branch tracing (AST)](#branch-tracing-ast)
21
+ - [Database capture](#database-capture)
22
+ - [Widget API](#widget-api)
23
+ - [Trace data model](#trace-data-model)
24
+ - [Project structure](#project-structure)
25
+ - [Run the example](#run-the-example)
26
+ - [Contributing](#contributing)
27
+ - [Roadmap](#roadmap)
28
+ - [License](#license)
29
+
30
+ ---
31
+
32
+ ## Features
33
+
34
+ | Capability | Description |
35
+ |------------|-------------|
36
+ | Request tracing | Method, URL, status, duration per request |
37
+ | Middleware chain | `next()` detection, early-exit reasons, async errors |
38
+ | Branch probing | `if` / `switch` / loops / `try` as diamond nodes (CommonJS `.js`) |
39
+ | DB capture | Drizzle, `pg`, `mysql2`, `mongoose`, Prisma |
40
+ | Live widget | Request list, D3 execution graph, waterfall timeline |
41
+ | NestJS | Guards, interceptors, pipes, controllers |
42
+
43
+
44
+ ---
45
+
46
+ ## Prerequisites
47
+
48
+ - Node.js **18.0.0** or newer
49
+ - An Express 4.x application (or NestJS with the Express adapter)
50
+ - For Nest interceptor tracing: `rxjs` ^7
51
+
52
+ ---
53
+
54
+ ## Installation
55
+
56
+ ```bash
57
+ npm install --save-dev opencons
58
+ ```
59
+
60
+ ---
61
+
62
+ ## Quick start
63
+
64
+ Require Opencons **before** creating your Express app, and register it as the **first** middleware:
65
+
66
+ ```javascript
67
+ const opencons = require('opencons'); // before express()
68
+ const express = require('express');
69
+
70
+ const app = express();
71
+
72
+ app.use(opencons()); // must be first
73
+
74
+ app.use(express.json());
75
+ app.use('/api', require('./routes'));
76
+
77
+ app.listen(3000);
78
+ ```
79
+
80
+ Open the widget while your app runs:
81
+
82
+ ```
83
+ http://localhost:7331
84
+ ```
85
+
86
+ If port 7331 is busy, Opencons tries the next port and logs the actual URL.
87
+
88
+ ---
89
+
90
+ ## Configuration
91
+
92
+ ```javascript
93
+ app.use(opencons({
94
+ port: 7331, // widget + WebSocket port
95
+ enabled: undefined, // set true to force enable in production (not recommended)
96
+ enableWidget: true, // set false in automated tests
97
+ exclude: ['/health'], // routes to ignore
98
+ captureBody: false, // snapshot request bodies on the trace
99
+ captureResponse: false, // snapshot response bodies (res.json / res.send)
100
+ maxTraces: 100, // in-memory ring buffer size
101
+ drivers: {
102
+ mongoose: true,
103
+ drizzle: true,
104
+ pg: true,
105
+ prisma: true,
106
+ mysql2: true,
107
+ },
108
+ transform: {
109
+ enabled: false, // AST branch probing (Phase 2)
110
+ projectRoot: process.cwd(),
111
+ exclude: ['vendor/**'],
112
+ },
113
+ }));
114
+ ```
115
+
116
+ Invalid options throw a `ConfigurationError` at startup with a descriptive message.
117
+
118
+ ### Programmatic access
119
+
120
+ ```javascript
121
+ const middleware = opencons({ enableWidget: false });
122
+ const traces = middleware.getTraces();
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Environment variables
128
+
129
+ Copy [.env.example](.env.example) into your host application. Load env **before** importing Opencons when using transform variables.
130
+
131
+ | Variable | Default | Description |
132
+ |----------|---------|-------------|
133
+ | `NODE_ENV` | — | `production` disables tracing unless `enabled: true` |
134
+ | `OPENCONS_TRANSFORM` | — | `1` or `true` installs AST hook on import |
135
+ | `OPENCONS_ROOT` | `process.cwd()` | Project root for source transforms |
136
+ | `OPENCONS_TRANSFORM_EXCLUDE` | — | Comma-separated globs to skip |
137
+ | `OPENCONS_LOG_LEVEL` | `info` | Set to `debug` for verbose library logs |
138
+
139
+ ---
140
+
141
+ ## NestJS integration
142
+
143
+ ```typescript
144
+ // main.ts — import Opencons before NestFactory.create()
145
+ import { NestFactory } from '@nestjs/core';
146
+ import { NestExpressApplication } from '@nestjs/platform-express';
147
+ import opencons from 'opencons';
148
+ import { AppModule } from './app.module';
149
+
150
+ async function bootstrap() {
151
+ const app = await NestFactory.create<NestExpressApplication>(AppModule);
152
+
153
+ opencons.applyToNest(app, {
154
+ port: 7331,
155
+ exclude: ['/health'],
156
+ });
157
+
158
+ await app.listen(3000);
159
+ }
160
+
161
+ bootstrap();
162
+ ```
163
+
164
+ **What gets traced**
165
+
166
+ | Layer | Traced? |
167
+ |-------|---------|
168
+ | HTTP request / response | Yes |
169
+ | Express middleware | Yes |
170
+ | Nest controllers | Yes |
171
+ | Nest guards / interceptors / pipes | Yes |
172
+
173
+ ### Alternative: `MiddlewareConsumer`
174
+
175
+ ```typescript
176
+ consumer
177
+ .apply(opencons.createNestMiddleware({ port: 7331 }))
178
+ .forRoutes('*');
179
+ ```
180
+
181
+ Prefer `applyToNest()` in `main.ts` immediately after `NestFactory.create()`.
182
+
183
+ ### Naming middleware
184
+
185
+ ```javascript
186
+ app.use(opencons.label('bullAuth', bullAuth));
187
+ ```
188
+
189
+ ---
190
+
191
+ ## Branch tracing (AST)
192
+
193
+ Every `if` / `switch` / `while` / `for` / `try` in **CommonJS `.js` files** under your project root can be probed at load time. Branch decisions appear as diamond nodes in the graph.
194
+
195
+ ```javascript
196
+ app.use(opencons({
197
+ transform: {
198
+ enabled: true,
199
+ projectRoot: process.cwd(),
200
+ exclude: ['dist/vendor/**'],
201
+ },
202
+ }));
203
+ ```
204
+
205
+ For **NestJS** (TypeScript compiled to `dist/`), enable via `.env` so the hook runs when `Opencons` is imported:
206
+
207
+ ```env
208
+ OPENCONS_TRANSFORM=1
209
+ OPENCONS_ROOT=dist/apps/api
210
+ ```
211
+
212
+ ```typescript
213
+ import './load-env'; // loads .env first
214
+ import opencons from 'opencons';
215
+ import { AppModule } from './app.module';
216
+ ```
217
+
218
+ Or use `node -r opencons/register-transform` or `require('opencons/register-transform')()` before other imports.
219
+
220
+ Skip a file with `// opencons-skip` at the top.
221
+
222
+ ---
223
+
224
+ ## Database capture
225
+
226
+ Database queries appear as **blue fork nodes** off the handler that triggered them.
227
+
228
+ | Driver | Package |
229
+ |--------|---------|
230
+ | Drizzle ORM | `drizzle-orm` |
231
+ | PostgreSQL | `pg` |
232
+ | MySQL | `mysql2` |
233
+ | MongoDB | `mongoose` |
234
+ | Prisma | `@prisma/client` |
235
+
236
+ Load `Opencons` before creating database clients. When `drizzle-orm` is installed, Opencons captures at the ORM layer and skips raw `pg`/`mysql2` to avoid duplicates.
237
+
238
+ ```javascript
239
+ opencons.applyToNest(app, {
240
+ drivers: { drizzle: true, mongoose: false },
241
+ });
242
+ ```
243
+
244
+ ---
245
+
246
+ ## Widget API
247
+
248
+ The dev widget server exposes a minimal HTTP API (no authentication — local dev only).
249
+
250
+ ### `GET /api/source`
251
+
252
+ Returns a source snippet for branch peek in the graph.
253
+
254
+ | Query param | Required | Description |
255
+ |-------------|----------|-------------|
256
+ | `file` | Yes | Project-relative path or basename |
257
+ | `line` | No | 1-based line number (default: 1) |
258
+
259
+ **Responses**
260
+
261
+ | Status | Body |
262
+ |--------|------|
263
+ | 200 | `{ file, line, startLine, lines: [{ number, text, highlight }] }` |
264
+ | 400 | `{ error, code: "MISSING_FILE_PARAM" }` |
265
+ | 404 | `{ error, code: "SOURCE_NOT_FOUND" }` |
266
+
267
+ ### WebSocket protocol
268
+
269
+ Connect to `ws://localhost:<port>` (same port as the widget).
270
+
271
+ **Client → server**
272
+
273
+ ```json
274
+ { "type": "get_history", "limit": 50 }
275
+ ```
276
+
277
+ **Server → client**
278
+
279
+ | Type | When |
280
+ |------|------|
281
+ | `trace_start` | Request begins |
282
+ | `trace_update` | Live progress |
283
+ | `trace` | Request completed |
284
+ | `history` | Response to `get_history` |
285
+
286
+ ---
287
+
288
+ ## Trace data model
289
+
290
+ Each request produces a directed acyclic graph (DAG):
291
+
292
+ ```json
293
+ {
294
+ "id": "req_a1b2c3",
295
+ "method": "GET",
296
+ "url": "/api/users/1",
297
+ "status": 200,
298
+ "duration_ms": 12.4,
299
+ "body": null,
300
+ "response": { "id": 1, "name": "Ada" },
301
+ "nodes": [
302
+ { "id": "n1", "type": "request", "label": "GET /api/users/1" },
303
+ { "id": "n2", "type": "middleware", "label": "authMiddleware", "duration_ms": 0.5, "called_next": true },
304
+ { "id": "n3", "type": "response", "label": "200" }
305
+ ],
306
+ "edges": [
307
+ { "from": "n1", "to": "n2" },
308
+ { "from": "n2", "to": "n3" }
309
+ ]
310
+ }
311
+ ```
312
+
313
+ ---
314
+
315
+ ## Project structure
316
+
317
+ ```
318
+ open-route/
319
+ ├── src/
320
+ │ ├── index.js # Package entry — Express patch on import
321
+ │ ├── core/ # Middleware, tracer, AsyncLocalStorage
322
+ │ ├── lib/ # Config, logger, errors, HTTP helpers
323
+ │ ├── interceptors/ # Express + require-hook patches
324
+ │ ├── transform/ # Babel branch probe injection
325
+ │ ├── store/ # Trace ring buffer + source cache
326
+ │ ├── server/ # Widget HTTP + WebSocket
327
+ │ ├── drivers/ # DB driver patches
328
+ │ ├── integrations/ # NestJS helpers
329
+ │ └── utils/ # label(), observable()
330
+ ├── widget/ # Browser dev UI
331
+ ├── examples/sample-app/ # Runnable Express demo
332
+ ├── test/ # Node.js test runner
333
+ ├── scripts/ # Maintenance scripts
334
+ ├── CONTRIBUTING.md
335
+ ├── .env.example
336
+ └── opencons.d.ts
337
+ ```
338
+
339
+ ---
340
+
341
+ ## Run the example
342
+
343
+ ```bash
344
+ npm install
345
+ npm run start:example
346
+ ```
347
+
348
+ 1. Open http://localhost:7331 for the widget
349
+ 2. Fire requests against http://localhost:3000:
350
+
351
+ ```bash
352
+ curl http://localhost:3000/api/public
353
+ curl http://localhost:3000/api/users/42
354
+ curl -H "Authorization: Bearer dev" http://localhost:3000/api/users/42
355
+ curl -X POST http://localhost:3000/api/orders \
356
+ -H "Content-Type: application/json" \
357
+ -H "Authorization: Bearer dev" \
358
+ -d "{\"items\":[1]}"
359
+ ```
360
+
361
+ ---
362
+
363
+ ## Contributing
364
+
365
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for local setup, coding conventions, and test instructions.
366
+
367
+ ---
368
+
369
+ ## Roadmap
370
+
371
+ | Phase | Feature | Status |
372
+ |-------|---------|--------|
373
+ | 1 | Request boundary, middleware chain, widget | Done |
374
+ | 2 | AST branch probing, source peek | Done |
375
+ | 3 | Database driver capture | Done |
376
+ | 4 | TypeScript / ESM transforms, replay mode | Planned |
377
+
378
+ ---
379
+
380
+ ## License
381
+
382
+ MIT — see [LICENSE](LICENSE).
package/opencons.d.ts ADDED
@@ -0,0 +1,55 @@
1
+ import { RequestHandler } from 'express';
2
+
3
+ export interface OpenconsOptions {
4
+ port?: number;
5
+ enabled?: boolean;
6
+ enableWidget?: boolean;
7
+ exclude?: string[];
8
+ captureBody?: boolean;
9
+ captureResponse?: boolean;
10
+ maxTraces?: number;
11
+ drivers?: {
12
+ mongoose?: boolean;
13
+ drizzle?: boolean;
14
+ pg?: boolean;
15
+ prisma?: boolean;
16
+ mysql2?: boolean;
17
+ };
18
+ transform?: {
19
+ enabled?: boolean;
20
+ projectRoot?: string;
21
+ exclude?: string[];
22
+ };
23
+ }
24
+
25
+ export interface TraceGraph {
26
+ id: string;
27
+ method: string;
28
+ url: string;
29
+ status: number | null;
30
+ duration_ms: number;
31
+ body?: unknown;
32
+ response?: unknown;
33
+ nodes: object[];
34
+ edges: object[];
35
+ }
36
+
37
+ export interface OpenconsMiddleware extends RequestHandler {
38
+ getTraces: () => TraceGraph[];
39
+ options: OpenconsOptions;
40
+ __openconsEntry: boolean;
41
+ }
42
+
43
+ export interface Opencons {
44
+ (options?: OpenconsOptions): OpenconsMiddleware;
45
+ applyToNest(
46
+ nestApp: { getHttpAdapter(): { getInstance(): unknown } },
47
+ options?: OpenconsOptions,
48
+ ): OpenconsMiddleware;
49
+ createNestMiddleware(options?: OpenconsOptions): OpenconsMiddleware;
50
+ label<T extends Function>(name: string, handler: T): T;
51
+ }
52
+
53
+ declare const opencons: Opencons;
54
+
55
+ export default opencons;
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "opencons",
3
+ "version": "0.1.0",
4
+ "description": "Opencons — live runtime execution tracer for Node.js / Express",
5
+ "main": "src/index.js",
6
+ "types": "opencons.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./opencons.d.ts",
10
+ "require": "./src/index.js",
11
+ "default": "./src/index.js"
12
+ },
13
+ "./register-transform": {
14
+ "require": "./src/transform/register.js",
15
+ "default": "./src/transform/register.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "src",
20
+ "widget",
21
+ "opencons.d.ts",
22
+ "scripts/vendor-d3.js"
23
+ ],
24
+ "scripts": {
25
+ "test": "node --test test/*.test.js",
26
+ "start:example": "node examples/sample-app/server.js",
27
+ "postinstall": "node scripts/vendor-d3.js"
28
+ },
29
+ "keywords": [
30
+ "express",
31
+ "tracing",
32
+ "debugging",
33
+ "middleware",
34
+ "developer-tools"
35
+ ],
36
+ "author": "patiencemanzen",
37
+ "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/patiencemanzen/opencons.git"
41
+ },
42
+ "homepage": "https://github.com/patiencemanzen/opencons#readme",
43
+ "bugs": {
44
+ "url": "https://github.com/patiencemanzen/opencons/issues"
45
+ },
46
+ "engines": {
47
+ "node": ">=18.0.0"
48
+ },
49
+ "dependencies": {
50
+ "@babel/generator": "^7.27.0",
51
+ "@babel/parser": "^7.27.0",
52
+ "@babel/traverse": "^7.27.0",
53
+ "@babel/types": "^7.27.0",
54
+ "ws": "^8.17.0"
55
+ },
56
+ "peerDependencies": {
57
+ "express": "^4.18.0",
58
+ "rxjs": "^7.0.0"
59
+ },
60
+ "peerDependenciesMeta": {
61
+ "express": {
62
+ "optional": false
63
+ },
64
+ "rxjs": {
65
+ "optional": true
66
+ }
67
+ },
68
+ "devDependencies": {
69
+ "d3": "^7.9.0",
70
+ "express": "^4.21.0",
71
+ "rxjs": "^7.8.1"
72
+ }
73
+ }
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Copy D3 into widget/vendor so the dev widget works offline.
5
+ * Falls back to CDN in index.html when this file is missing.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ const source = path.join(__dirname, '..', 'node_modules', 'd3', 'dist', 'd3.min.js');
12
+ const targetDir = path.join(__dirname, '..', 'widget', 'vendor');
13
+ const target = path.join(targetDir, 'd3.min.js');
14
+
15
+ if (!fs.existsSync(source)) {
16
+ console.warn('[vendor-d3] d3 not installed — widget will use CDN fallback');
17
+ process.exit(0);
18
+ }
19
+
20
+ fs.mkdirSync(targetDir, { recursive: true });
21
+ fs.copyFileSync(source, target);
22
+ console.log('[vendor-d3] Copied d3.min.js to widget/vendor/');
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ const { AsyncLocalStorage } = require('async_hooks');
4
+
5
+ const storage = new AsyncLocalStorage();
6
+
7
+ /**
8
+ * @typedef {Object} TraceContext
9
+ * @property {string} id
10
+ * @property {number} startTime
11
+ * @property {import('./tracer').TraceTracer} tracer
12
+ * @property {string} [scopeNodeId]
13
+ */
14
+
15
+ /**
16
+ * Run a function within an isolated trace context.
17
+ * @param {TraceContext} context
18
+ * @param {() => void} fn
19
+ */
20
+ function runWithContext(context, fn) {
21
+ return storage.run(context, fn);
22
+ }
23
+
24
+ /**
25
+ * @returns {TraceContext | undefined}
26
+ */
27
+ function getCurrentContext() {
28
+ return storage.getStore();
29
+ }
30
+
31
+ /**
32
+ * @returns {import('./tracer').TraceTracer | null}
33
+ */
34
+ function getCurrentTracer() {
35
+ const ctx = getCurrentContext();
36
+ return ctx ? ctx.tracer : null;
37
+ }
38
+
39
+ module.exports = {
40
+ storage,
41
+ runWithContext,
42
+ getCurrentContext,
43
+ getCurrentTracer,
44
+ };