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
@@ -0,0 +1,158 @@
1
+ 'use strict';
2
+
3
+ const { getCurrentTracer, getCurrentContext, runWithContext } = require('../core/context');
4
+ const { buildDbNodeLanguage } = require('./db-language');
5
+
6
+ const MAX_QUERY_LEN = 500;
7
+ const MAX_PARAMS = 12;
8
+
9
+ /**
10
+ * @param {unknown} value
11
+ */
12
+ function safeParams(value) {
13
+ if (value == null) return undefined;
14
+
15
+ if (Array.isArray(value)) {
16
+ return value.slice(0, MAX_PARAMS).map((item) => safeParamValue(item));
17
+ }
18
+
19
+ if (typeof value === 'object') {
20
+ const out = {};
21
+ const entries = Object.entries(value).slice(0, MAX_PARAMS);
22
+ for (const [key, item] of entries) {
23
+ out[key] = safeParamValue(item);
24
+ }
25
+ return out;
26
+ }
27
+
28
+ return safeParamValue(value);
29
+ }
30
+
31
+ /**
32
+ * @param {unknown} value
33
+ */
34
+ function safeParamValue(value) {
35
+ if (value == null) return value;
36
+ if (typeof value === 'string') return value.length > 80 ? `${value.slice(0, 80)}…` : value;
37
+ if (typeof value === 'number' || typeof value === 'boolean') return value;
38
+ if (value instanceof Date) return value.toISOString();
39
+ if (Buffer.isBuffer(value)) return `[Buffer ${value.length}b]`;
40
+ try {
41
+ const text = JSON.stringify(value);
42
+ return text.length > 120 ? `${text.slice(0, 120)}…` : text;
43
+ } catch {
44
+ return '[unserializable]';
45
+ }
46
+ }
47
+
48
+ /**
49
+ * @param {string} query
50
+ */
51
+ function truncateQuery(query) {
52
+ const text = String(query || '').replace(/\s+/g, ' ').trim();
53
+ if (text.length <= MAX_QUERY_LEN) return text;
54
+ return `${text.slice(0, MAX_QUERY_LEN)}…`;
55
+ }
56
+
57
+ /**
58
+ * @param {unknown} result
59
+ */
60
+ function countRows(result) {
61
+ if (result == null) return undefined;
62
+ if (Array.isArray(result)) return result.length;
63
+ if (typeof result === 'object') {
64
+ if ('rowCount' in result && result.rowCount != null) return Number(result.rowCount);
65
+ if ('count' in result && result.count != null) return Number(result.count);
66
+ if ('length' in result && typeof result.length === 'number') return result.length;
67
+ if ('affectedRows' in result) return Number(result.affectedRows);
68
+ }
69
+ return undefined;
70
+ }
71
+
72
+ /**
73
+ * @param {object} payload
74
+ * @param {string} payload.driver
75
+ * @param {string} [payload.operation]
76
+ * @param {string} [payload.query]
77
+ * @param {unknown} [payload.params]
78
+ * @param {number} [payload.rows]
79
+ * @param {number} payload.duration_ms
80
+ * @param {string} [payload.collection]
81
+ * @param {string} [payload.error]
82
+ */
83
+ function recordDbQuery(payload) {
84
+ const tracer = getCurrentTracer();
85
+ const ctx = getCurrentContext();
86
+
87
+ if (!tracer) return;
88
+
89
+ const record = () => {
90
+ const parentId = ctx?.scopeNodeId || tracer.getLastSequentialNodeId();
91
+ const operation = payload.operation || 'query';
92
+ const language = buildDbNodeLanguage({
93
+ driver: payload.driver,
94
+ operation,
95
+ query: payload.query,
96
+ collection: payload.collection,
97
+ rows: payload.rows,
98
+ duration_ms: payload.duration_ms,
99
+ error: payload.error,
100
+ });
101
+
102
+ tracer.addForkNode(parentId, {
103
+ type: 'db',
104
+ label: language.label,
105
+ summary: language.summary,
106
+ db_action: language.db_action,
107
+ db_intent: language.db_intent,
108
+ db_result: language.db_result,
109
+ query: payload.query ? truncateQuery(payload.query) : undefined,
110
+ params: safeParams(payload.params),
111
+ rows: payload.rows,
112
+ duration_ms: payload.duration_ms,
113
+ driver: payload.driver,
114
+ operation,
115
+ collection: payload.collection,
116
+ exit_reason: payload.error,
117
+ });
118
+ };
119
+
120
+ if (ctx) {
121
+ runWithContext(ctx, record);
122
+ } else {
123
+ record();
124
+ }
125
+ }
126
+
127
+ /**
128
+ * @param {() => Promise<unknown> | unknown} fn
129
+ * @param {object} meta
130
+ */
131
+ async function traceDbCall(fn, meta) {
132
+ const start = performance.now();
133
+
134
+ try {
135
+ const result = await fn();
136
+ recordDbQuery({
137
+ ...meta,
138
+ rows: meta.rows ?? countRows(result),
139
+ duration_ms: Math.round((performance.now() - start) * 10) / 10,
140
+ });
141
+ return result;
142
+ } catch (err) {
143
+ recordDbQuery({
144
+ ...meta,
145
+ duration_ms: Math.round((performance.now() - start) * 10) / 10,
146
+ error: err && err.message ? err.message : String(err),
147
+ });
148
+ throw err;
149
+ }
150
+ }
151
+
152
+ module.exports = {
153
+ recordDbQuery,
154
+ traceDbCall,
155
+ truncateQuery,
156
+ safeParams,
157
+ countRows,
158
+ };
package/src/index.js ADDED
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Opencons public entry point.
5
+ *
6
+ * Import-time side effects are intentional:
7
+ * - Express prototype patching so handlers registered after setup are wrapped
8
+ * - Optional AST require hook when OPENCONS_TRANSFORM is set
9
+ *
10
+ * Require this module before express() and call opencons() as the first middleware.
11
+ */
12
+
13
+ const { createOpencons } = require('./core');
14
+ const { patchExpressGlobally } = require('./interceptors/express');
15
+ const { applyToNest, createNestMiddleware } = require('./integrations/nest');
16
+ const { label } = require('./utils/label');
17
+
18
+ require('./transform/register');
19
+ patchExpressGlobally();
20
+
21
+ const opencons = createOpencons;
22
+
23
+ opencons.applyToNest = applyToNest;
24
+ opencons.createNestMiddleware = createNestMiddleware;
25
+ opencons.label = label;
26
+
27
+ module.exports = opencons;
28
+ module.exports.default = opencons;
@@ -0,0 +1,357 @@
1
+ 'use strict';
2
+
3
+ const { getCurrentTracer, getCurrentContext, runWithContext } = require('../core/context');
4
+ const { traceObservable } = require('../utils/observable');
5
+
6
+ let nestPatched = false;
7
+
8
+ /**
9
+ * @param {import('../core/tracer').TraceTracer | null | undefined} tracer
10
+ * @param {string} label
11
+ * @param {number} entered
12
+ * @param {boolean} [calledNext]
13
+ * @param {string} [exitReason]
14
+ * @param {'middleware' | 'controller'} [nodeType]
15
+ */
16
+ function recordStep(tracer, label, entered, calledNext = true, exitReason, nodeType = 'middleware') {
17
+ if (!tracer) return;
18
+
19
+ const duration_ms = Math.round((performance.now() - entered) * 10) / 10;
20
+
21
+ tracer.addNode({
22
+ type: nodeType,
23
+ label,
24
+ duration_ms,
25
+ called_next: calledNext,
26
+ exit_reason: exitReason,
27
+ });
28
+ }
29
+
30
+ /**
31
+ * @param {import('../core/context').TraceContext | undefined} alsContext
32
+ * @param {import('../core/tracer').TraceTracer | null} tracer
33
+ * @param {string} label
34
+ * @param {number} entered
35
+ * @param {boolean} [calledNext]
36
+ * @param {string} [exitReason]
37
+ * @param {'middleware' | 'controller'} [nodeType]
38
+ */
39
+ function recordStepSafe(alsContext, tracer, label, entered, calledNext, exitReason, nodeType) {
40
+ if (alsContext) {
41
+ const { runWithContext } = require('../core/context');
42
+ runWithContext(alsContext, () =>
43
+ recordStep(tracer, label, entered, calledNext, exitReason, nodeType)
44
+ );
45
+ return;
46
+ }
47
+
48
+ recordStep(tracer, label, entered, calledNext, exitReason, nodeType);
49
+ }
50
+
51
+ /**
52
+ * @param {object} interceptor
53
+ */
54
+ function wrapNestInterceptor(interceptor) {
55
+ if (!interceptor || interceptor.__openconsWrapped) {
56
+ return interceptor;
57
+ }
58
+
59
+ if (typeof interceptor.intercept !== 'function') {
60
+ return interceptor;
61
+ }
62
+
63
+ const name = resolveNestComponentName(interceptor, 'Interceptor');
64
+ const original = interceptor.intercept.bind(interceptor);
65
+
66
+ interceptor.intercept = function OpenconsIntercept(context, next) {
67
+ const tracer = getCurrentTracer();
68
+ const alsContext = getCurrentContext();
69
+
70
+ if (!tracer) {
71
+ return original(context, next);
72
+ }
73
+
74
+ const entered = performance.now();
75
+
76
+ try {
77
+ const result = original(context, next);
78
+
79
+ return traceObservable(
80
+ result,
81
+ (exitReason) => {
82
+ recordStepSafe(alsContext, tracer, name, entered, true, exitReason);
83
+ },
84
+ alsContext
85
+ );
86
+ } catch (err) {
87
+ recordStepSafe(alsContext, tracer, name, entered, false, `error: ${err.message}`);
88
+ throw err;
89
+ }
90
+ };
91
+
92
+ interceptor.__openconsWrapped = true;
93
+ return interceptor;
94
+ }
95
+
96
+ /**
97
+ * @param {object} guard
98
+ */
99
+ function wrapNestGuard(guard) {
100
+ if (!guard || guard.__openconsWrapped) {
101
+ return guard;
102
+ }
103
+
104
+ if (typeof guard.canActivate !== 'function') {
105
+ return guard;
106
+ }
107
+
108
+ const name = resolveNestComponentName(guard, 'Guard');
109
+ const original = guard.canActivate.bind(guard);
110
+
111
+ guard.canActivate = async function OpenconsCanActivate(context) {
112
+ const tracer = getCurrentTracer();
113
+ const alsContext = getCurrentContext();
114
+
115
+ if (!tracer) {
116
+ return original(context);
117
+ }
118
+
119
+ const entered = performance.now();
120
+
121
+ try {
122
+ const allowed = await original(context);
123
+ recordStepSafe(alsContext, tracer, name, entered, Boolean(allowed), allowed ? undefined : 'denied');
124
+ return allowed;
125
+ } catch (err) {
126
+ recordStepSafe(alsContext, tracer, name, entered, false, `error: ${err.message}`);
127
+ throw err;
128
+ }
129
+ };
130
+
131
+ guard.__openconsWrapped = true;
132
+ return guard;
133
+ }
134
+
135
+ /**
136
+ * @param {object} component
137
+ * @param {string} fallbackSuffix
138
+ */
139
+ function resolveNestComponentName(component, fallbackSuffix) {
140
+ const ctor = component.constructor?.name;
141
+ if (ctor && ctor !== 'Object' && ctor !== 'Function') {
142
+ return ctor;
143
+ }
144
+ return `Anonymous${fallbackSuffix}`;
145
+ }
146
+
147
+ /**
148
+ * @param {import('@nestjs/common').ExecutionContext} context
149
+ */
150
+ function resolveControllerLabel(context) {
151
+ const className = context.getClass()?.name || 'Controller';
152
+ const handler = context.getHandler();
153
+ const handlerName = handler?.name || handler?.displayName || 'handler';
154
+ return `${className}.${handlerName}`;
155
+ }
156
+
157
+ /**
158
+ * Trace controller handlers — must run as the innermost global interceptor.
159
+ */
160
+ class OpenconsControllerInterceptor {
161
+ intercept(context, next) {
162
+ const tracer = getCurrentTracer();
163
+ const alsContext = getCurrentContext();
164
+
165
+ if (!tracer) {
166
+ return next.handle();
167
+ }
168
+
169
+ const label = resolveControllerLabel(context);
170
+ const entered = performance.now();
171
+
172
+ const openController = () => {
173
+ const node = tracer.addNode({
174
+ type: 'controller',
175
+ label,
176
+ duration_ms: null,
177
+ called_next: true,
178
+ });
179
+ if (alsContext) {
180
+ alsContext.scopeNodeId = node.id;
181
+ }
182
+ return node.id;
183
+ };
184
+
185
+ const controllerNodeId = alsContext
186
+ ? runWithContext(alsContext, openController)
187
+ : openController();
188
+
189
+ const completeController = (calledNext, exitReason) => {
190
+ tracer.updateNode(controllerNodeId, {
191
+ duration_ms: Math.round((performance.now() - entered) * 10) / 10,
192
+ called_next: calledNext,
193
+ exit_reason: exitReason,
194
+ });
195
+ };
196
+
197
+ try {
198
+ const result = next.handle();
199
+
200
+ return traceObservable(
201
+ result,
202
+ (exitReason) => {
203
+ if (alsContext) {
204
+ runWithContext(alsContext, () => completeController(true, exitReason));
205
+ return;
206
+ }
207
+ completeController(true, exitReason);
208
+ },
209
+ alsContext
210
+ );
211
+ } catch (err) {
212
+ if (alsContext) {
213
+ runWithContext(alsContext, () => completeController(false, `error: ${err.message}`));
214
+ } else {
215
+ completeController(false, `error: ${err.message}`);
216
+ }
217
+ throw err;
218
+ }
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Patch Nest application methods so globally registered components are traced.
224
+ */
225
+ function patchNestGlobally() {
226
+ if (nestPatched) return;
227
+ nestPatched = true;
228
+
229
+ try {
230
+ const { NestApplication } = require('@nestjs/core/nest-application');
231
+
232
+ const originalInterceptors = NestApplication.prototype.useGlobalInterceptors;
233
+ NestApplication.prototype.useGlobalInterceptors = function (...interceptors) {
234
+ return originalInterceptors.call(
235
+ this,
236
+ ...interceptors.map((item) => wrapNestInterceptor(item))
237
+ );
238
+ };
239
+
240
+ const originalGuards = NestApplication.prototype.useGlobalGuards;
241
+ NestApplication.prototype.useGlobalGuards = function (...guards) {
242
+ return originalGuards.call(this, ...guards.map((item) => wrapNestGuard(item)));
243
+ };
244
+
245
+ const originalPipes = NestApplication.prototype.useGlobalPipes;
246
+ NestApplication.prototype.useGlobalPipes = function (...pipes) {
247
+ return originalPipes.call(this, ...pipes.map((pipe) => wrapNestPipe(pipe)));
248
+ };
249
+ } catch {
250
+ // @nestjs/core not installed — Express-only mode.
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Register the controller interceptor last so it wraps the route handler directly.
256
+ * @param {import('@nestjs/common').INestApplication} nestApp
257
+ */
258
+ function attachControllerTracing(nestApp) {
259
+ nestApp.useGlobalInterceptors(new OpenconsControllerInterceptor());
260
+ }
261
+
262
+ /**
263
+ * @param {import('@nestjs/common').INestApplication} nestApp
264
+ */
265
+ function deferControllerTracingUntilReady(nestApp) {
266
+ let attached = false;
267
+
268
+ const attach = () => {
269
+ if (attached) return;
270
+ attached = true;
271
+ attachControllerTracing(nestApp);
272
+ };
273
+
274
+ const originalListen = nestApp.listen.bind(nestApp);
275
+ nestApp.listen = function OpenconsListen(...args) {
276
+ attach();
277
+ return originalListen(...args);
278
+ };
279
+
280
+ if (typeof nestApp.init === 'function') {
281
+ const originalInit = nestApp.init.bind(nestApp);
282
+ nestApp.init = async function OpenconsInit(...args) {
283
+ attach();
284
+ return originalInit(...args);
285
+ };
286
+ }
287
+ }
288
+
289
+ /** @deprecated Use OpenconsControllerInterceptor via deferControllerTracingUntilReady */
290
+ class OpenconsNestInterceptor {
291
+ intercept(context, next) {
292
+ return new OpenconsControllerInterceptor().intercept(context, next);
293
+ }
294
+ }
295
+
296
+ /**
297
+ * @param {object} pipe
298
+ */
299
+ function wrapNestPipe(pipe) {
300
+ if (!pipe || pipe.__openconsWrapped) {
301
+ return pipe;
302
+ }
303
+
304
+ if (typeof pipe.transform !== 'function') {
305
+ return pipe;
306
+ }
307
+
308
+ const name = resolveNestComponentName(pipe, 'Pipe');
309
+ const original = pipe.transform.bind(pipe);
310
+
311
+ pipe.transform = function OpenconsTransform(value, metadata) {
312
+ const tracer = getCurrentTracer();
313
+ const alsContext = getCurrentContext();
314
+
315
+ if (!tracer) {
316
+ return original(value, metadata);
317
+ }
318
+
319
+ const entered = performance.now();
320
+
321
+ try {
322
+ const result = original(value, metadata);
323
+
324
+ if (result && typeof result.then === 'function') {
325
+ return result
326
+ .then((resolved) => {
327
+ recordStepSafe(alsContext, tracer, name, entered);
328
+ return resolved;
329
+ })
330
+ .catch((err) => {
331
+ recordStepSafe(alsContext, tracer, name, entered, false, `error: ${err.message}`);
332
+ throw err;
333
+ });
334
+ }
335
+
336
+ recordStepSafe(alsContext, tracer, name, entered);
337
+ return result;
338
+ } catch (err) {
339
+ recordStepSafe(alsContext, tracer, name, entered, false, `error: ${err.message}`);
340
+ throw err;
341
+ }
342
+ };
343
+
344
+ pipe.__openconsWrapped = true;
345
+ return pipe;
346
+ }
347
+
348
+ module.exports = {
349
+ patchNestGlobally,
350
+ wrapNestInterceptor,
351
+ wrapNestGuard,
352
+ wrapNestPipe,
353
+ attachControllerTracing,
354
+ deferControllerTracingUntilReady,
355
+ OpenconsControllerInterceptor,
356
+ OpenconsNestInterceptor,
357
+ };
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ const { createOpencons } = require('../core');
4
+ const { patchExpressApp } = require('../interceptors/express');
5
+ const {
6
+ patchNestGlobally,
7
+ deferControllerTracingUntilReady,
8
+ } = require('./nest-lifecycle');
9
+
10
+ /**
11
+ * @param {import('express').Application} expressApp
12
+ * @param {import('express').RequestHandler} handler
13
+ */
14
+ function prependMiddleware(expressApp, handler) {
15
+ if (typeof expressApp.lazyrouter === 'function') {
16
+ expressApp.lazyrouter();
17
+ }
18
+
19
+ const router = expressApp._router;
20
+
21
+ if (!router || !Array.isArray(router.stack)) {
22
+ expressApp.use(handler);
23
+ return;
24
+ }
25
+
26
+ const Layer = require('express/lib/router/layer');
27
+ const layer = new Layer('/', { sensitive: false, strict: false, end: false }, handler);
28
+ router.stack.unshift(layer);
29
+ }
30
+
31
+ /**
32
+ * @param {import('@nestjs/common').INestApplication} nestApp
33
+ * @param {Parameters<typeof createOpencons>[0]} [options]
34
+ * @returns {ReturnType<typeof createOpencons>}
35
+ */
36
+ function applyToNest(nestApp, options) {
37
+ patchNestGlobally();
38
+
39
+ const middleware = createOpencons(options);
40
+ const httpAdapter = nestApp.getHttpAdapter();
41
+
42
+ if (!httpAdapter || typeof httpAdapter.getInstance !== 'function') {
43
+ const { OpenconsError } = require('../lib/errors');
44
+ throw new OpenconsError(
45
+ 'Nest app must use the Express adapter (@nestjs/platform-express).',
46
+ 'NEST_ADAPTER_REQUIRED'
47
+ );
48
+ }
49
+
50
+ const expressApp = httpAdapter.getInstance();
51
+ patchExpressApp(expressApp);
52
+ prependMiddleware(expressApp, middleware);
53
+
54
+ if (typeof httpAdapter.setOnRequestHook === 'function') {
55
+ const previousHook = httpAdapter.onRequestHook;
56
+
57
+ httpAdapter.setOnRequestHook((req, res, next) => {
58
+ middleware(req, res, () => {
59
+ if (previousHook) {
60
+ previousHook(req, res, next);
61
+ return;
62
+ }
63
+ next();
64
+ });
65
+ });
66
+ }
67
+
68
+ // Register controller tracing after the app's own global interceptors (on listen/init).
69
+ deferControllerTracingUntilReady(nestApp);
70
+
71
+ const { logger } = require('../lib/logger');
72
+ logger.info('Attached to Nest (middleware + guards + interceptors + controllers)');
73
+
74
+ return middleware;
75
+ }
76
+
77
+ /**
78
+ * @param {Parameters<typeof createOpencons>[0]} [options]
79
+ * @returns {ReturnType<typeof createOpencons>}
80
+ */
81
+ function createNestMiddleware(options) {
82
+ return createOpencons(options);
83
+ }
84
+
85
+ module.exports = {
86
+ applyToNest,
87
+ createNestMiddleware,
88
+ prependMiddleware,
89
+ };