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,146 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @param {string | null | undefined} expr
5
+ */
6
+ function phraseCondition(expr) {
7
+ if (!expr) return null;
8
+
9
+ return expr
10
+ .replace(/\s+/g, ' ')
11
+ .replace(/===/g, ' equals ')
12
+ .replace(/!==/g, ' does not equal ')
13
+ .replace(/==/g, ' equals ')
14
+ .replace(/!=/g, ' does not equal ')
15
+ .replace(/<=/g, ' is at most ')
16
+ .replace(/>=/g, ' is at least ')
17
+ .replace(/</g, ' is less than ')
18
+ .replace(/>/g, ' is greater than ')
19
+ .replace(/&&/g, ' and ')
20
+ .replace(/\|\|/g, ' or ')
21
+ .replace(/\b!/g, 'not ')
22
+ .trim();
23
+ }
24
+
25
+ /**
26
+ * @param {string} kind
27
+ * @param {string | null} condition
28
+ */
29
+ function decisionTitle(kind, condition) {
30
+ const phrase = phraseCondition(condition);
31
+
32
+ if (kind === 'if' || kind === 'ternary') {
33
+ return phrase ? `Checked whether ${phrase}` : 'Checked an if condition';
34
+ }
35
+
36
+ if (kind === 'switch') {
37
+ return phrase ? `Switched on ${phrase}` : 'Evaluated a switch';
38
+ }
39
+
40
+ if (kind === 'while' || kind === 'for') {
41
+ return phrase ? `Loop condition: ${phrase}` : 'Evaluated a loop condition';
42
+ }
43
+
44
+ if (kind === 'catch') {
45
+ return 'Entered a catch block';
46
+ }
47
+
48
+ return phrase ? `Evaluated ${phrase}` : 'Evaluated a condition';
49
+ }
50
+
51
+ /**
52
+ * @param {string} kind
53
+ * @param {unknown} value
54
+ * @param {boolean} hasElse
55
+ */
56
+ function buildIfOutcomes(value, hasElse) {
57
+ const truthy = Boolean(value);
58
+
59
+ const outcomes = [
60
+ {
61
+ key: 'then',
62
+ label: truthy ? 'Then block — ran' : 'Then block — skipped',
63
+ taken: truthy,
64
+ },
65
+ ];
66
+
67
+ if (hasElse) {
68
+ outcomes.push({
69
+ key: 'else',
70
+ label: truthy ? 'Else block — skipped' : 'Else block — pending',
71
+ taken: false,
72
+ });
73
+ }
74
+
75
+ return outcomes;
76
+ }
77
+
78
+ /**
79
+ * @param {string} kind
80
+ * @param {unknown} value
81
+ * @param {boolean} [hasElse]
82
+ */
83
+ function decisionSummary(kind, value, hasElse = false) {
84
+ const truthy = Boolean(value);
85
+
86
+ if (kind === 'if' || kind === 'ternary') {
87
+ if (truthy) return 'Yes — code inside the then branch ran';
88
+ if (hasElse) return 'No — then branch skipped (else may run next)';
89
+ return 'No — then branch skipped';
90
+ }
91
+
92
+ if (kind === 'switch') {
93
+ return `Matched value ${formatValue(value)}`;
94
+ }
95
+
96
+ if (kind === 'while' || kind === 'for') {
97
+ return truthy ? 'Yes — loop body will run' : 'No — loop finished';
98
+ }
99
+
100
+ return truthy ? 'Condition was true' : 'Condition was false';
101
+ }
102
+
103
+ /**
104
+ * @param {unknown} value
105
+ */
106
+ function formatValue(value) {
107
+ if (value === true) return 'true';
108
+ if (value === false) return 'false';
109
+ if (value === null) return 'null';
110
+ if (value === undefined) return 'undefined';
111
+ if (typeof value === 'string') return `"${value}"`;
112
+ return String(value);
113
+ }
114
+
115
+ /**
116
+ * @param {object} node
117
+ */
118
+ function applyElseTaken(node) {
119
+ const outcomes = (node.outcomes || []).map((outcome) => {
120
+ if (outcome.key === 'else') {
121
+ return { ...outcome, label: 'Else block — ran', taken: true };
122
+ }
123
+
124
+ if (outcome.key === 'then') {
125
+ return { ...outcome, label: 'Then block — skipped', taken: false };
126
+ }
127
+
128
+ return outcome;
129
+ });
130
+
131
+ return {
132
+ ...node,
133
+ outcomes,
134
+ taken_outcome: 'else',
135
+ summary: 'No — ran the else branch instead',
136
+ };
137
+ }
138
+
139
+ module.exports = {
140
+ phraseCondition,
141
+ decisionTitle,
142
+ buildIfOutcomes,
143
+ decisionSummary,
144
+ formatValue,
145
+ applyElseTaken,
146
+ };
@@ -0,0 +1,161 @@
1
+ 'use strict';
2
+
3
+ const { getCurrentTracer } = require('../core/context');
4
+ const {
5
+ decisionTitle,
6
+ buildIfOutcomes,
7
+ decisionSummary,
8
+ applyElseTaken,
9
+ } = require('./natural-language');
10
+
11
+ /**
12
+ * @param {string} label
13
+ */
14
+ function parseProbeLabel(label) {
15
+ const parts = String(label).split(':');
16
+ const kind = parts[0] || 'branch';
17
+ const file = parts[1] || '';
18
+ const line = Number(parts[2]) || null;
19
+
20
+ return { kind, file, line };
21
+ }
22
+
23
+ /**
24
+ * Runtime probe injected into transformed application modules.
25
+ *
26
+ * @param {string} label
27
+ * @param {unknown} value
28
+ * @param {string} [conditionText]
29
+ * @param {boolean} [hasElse]
30
+ * @returns {unknown}
31
+ */
32
+ function __rg_probe(label, value, conditionText, hasElse = false) {
33
+ const tracer = getCurrentTracer();
34
+
35
+ if (!tracer) {
36
+ return value;
37
+ }
38
+
39
+ const meta = parseProbeLabel(label);
40
+ const nodeType = meta.kind === 'while' || meta.kind === 'for' ? 'loop' : 'branch';
41
+ const title = decisionTitle(meta.kind, conditionText || null);
42
+ const summary = decisionSummary(meta.kind, value, hasElse);
43
+
44
+ const node = {
45
+ type: nodeType,
46
+ label: title,
47
+ summary,
48
+ value,
49
+ condition: conditionText || undefined,
50
+ has_else: hasElse || undefined,
51
+ duration_ms: null,
52
+ source: meta.file
53
+ ? {
54
+ file: meta.file,
55
+ line: meta.line,
56
+ kind: meta.kind,
57
+ }
58
+ : undefined,
59
+ };
60
+
61
+ if (meta.kind === 'if' || meta.kind === 'ternary') {
62
+ node.outcomes = buildIfOutcomes(value, hasElse);
63
+ node.taken_outcome = Boolean(value) ? 'then' : null;
64
+ }
65
+
66
+ tracer.addNode(node);
67
+
68
+ return value;
69
+ }
70
+
71
+ /**
72
+ * @param {string} label
73
+ */
74
+ function __rg_else_probe(label) {
75
+ const tracer = getCurrentTracer();
76
+
77
+ if (!tracer) {
78
+ return;
79
+ }
80
+
81
+ const meta = parseProbeLabel(label.replace(/^else/, 'if'));
82
+ const lastIf = [...tracer.nodes]
83
+ .reverse()
84
+ .find(
85
+ (node) =>
86
+ node.source?.kind === 'if' &&
87
+ node.source.file === meta.file &&
88
+ node.source.line === meta.line
89
+ );
90
+
91
+ if (lastIf) {
92
+ Object.assign(lastIf, applyElseTaken(lastIf));
93
+ if (typeof tracer._notifyChange === 'function') {
94
+ tracer._notifyChange();
95
+ }
96
+ return;
97
+ }
98
+
99
+ tracer.addNode({
100
+ type: 'branch',
101
+ label: decisionTitle('if', null),
102
+ summary: 'Ran the else branch',
103
+ value: false,
104
+ has_else: true,
105
+ outcomes: [
106
+ { key: 'then', label: 'Then block — skipped', taken: false },
107
+ { key: 'else', label: 'Else block — ran', taken: true },
108
+ ],
109
+ taken_outcome: 'else',
110
+ duration_ms: null,
111
+ source: meta.file
112
+ ? {
113
+ file: meta.file,
114
+ line: meta.line,
115
+ kind: 'else',
116
+ }
117
+ : undefined,
118
+ });
119
+ }
120
+
121
+ /**
122
+ * @param {string} label
123
+ * @param {unknown} error
124
+ */
125
+ function __rg_catch_probe(label, error) {
126
+ const tracer = getCurrentTracer();
127
+
128
+ if (!tracer) {
129
+ return error;
130
+ }
131
+
132
+ const meta = parseProbeLabel(label);
133
+ const message =
134
+ error && typeof error === 'object' && 'message' in error
135
+ ? String(error.message)
136
+ : String(error);
137
+
138
+ tracer.addNode({
139
+ type: 'error',
140
+ label: 'An error was caught',
141
+ summary: message ? `Caught: ${message}` : 'Entered catch block',
142
+ value: message,
143
+ duration_ms: null,
144
+ source: meta.file
145
+ ? {
146
+ file: meta.file,
147
+ line: meta.line,
148
+ kind: 'catch',
149
+ }
150
+ : undefined,
151
+ });
152
+
153
+ return error;
154
+ }
155
+
156
+ module.exports = {
157
+ __rg_probe,
158
+ __rg_else_probe,
159
+ __rg_catch_probe,
160
+ parseProbeLabel,
161
+ };
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const { installRequireHook } = require('../interceptors/require-hook');
5
+
6
+ /**
7
+ * Install the require hook immediately. Import this module BEFORE your
8
+ * application modules load:
9
+ *
10
+ * require('opencons/register-transform');
11
+ *
12
+ * @param {object} [options]
13
+ * @param {string} [options.projectRoot]
14
+ * @param {string[]} [options.exclude]
15
+ */
16
+ function registerTransform(options = {}) {
17
+ installRequireHook({
18
+ projectRoot:
19
+ options.projectRoot ||
20
+ process.env.OPENCONS_ROOT ||
21
+ process.env.ROUTEGRAPHER_ROOT ||
22
+ process.cwd(),
23
+ exclude:
24
+ options.exclude ||
25
+ splitEnvList(process.env.OPENCONS_TRANSFORM_EXCLUDE || process.env.ROUTEGRAPHER_TRANSFORM_EXCLUDE),
26
+ });
27
+ }
28
+
29
+ /**
30
+ * @param {string | undefined} value
31
+ */
32
+ function splitEnvList(value) {
33
+ if (!value) return [];
34
+ return value.split(',').map((item) => item.trim()).filter(Boolean);
35
+ }
36
+
37
+ const transformEnabled =
38
+ process.env.OPENCONS_TRANSFORM || process.env.ROUTEGRAPHER_TRANSFORM;
39
+
40
+ if (transformEnabled === '1' || transformEnabled === 'true') {
41
+ registerTransform();
42
+ }
43
+
44
+ module.exports = registerTransform;
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Assign a display name to a middleware/handler for Opencons traces.
5
+ *
6
+ * @example
7
+ * app.use(Opencons.label('cors', corsFn));
8
+ * app.use(Opencons.label('bullAuth', bullAuth));
9
+ *
10
+ * @param {string} name
11
+ * @param {T} handler
12
+ * @returns {T}
13
+ * @template {Function} T
14
+ */
15
+ function label(name, handler) {
16
+ if (typeof handler !== 'function') {
17
+ return handler;
18
+ }
19
+
20
+ handler.__openconsName = name;
21
+ return handler;
22
+ }
23
+
24
+ module.exports = {
25
+ label,
26
+ };
@@ -0,0 +1,103 @@
1
+ 'use strict';
2
+
3
+ const { createRequire } = require('module');
4
+ const path = require('path');
5
+ const { runWithContext } = require('../core/context');
6
+
7
+ /** @type {{ rxjs: typeof import('rxjs') } | null} */
8
+ let cachedRxjs = null;
9
+
10
+ /**
11
+ * Load rxjs from the host application (Nest consumer), not from Opencons.
12
+ */
13
+ function loadRxjs() {
14
+ if (cachedRxjs) return cachedRxjs;
15
+
16
+ const searchPaths = [
17
+ process.cwd(),
18
+ ...(require.main?.paths || []),
19
+ path.join(process.cwd(), 'node_modules'),
20
+ ];
21
+
22
+ const hostRequire = createRequire(path.join(process.cwd(), 'package.json'));
23
+
24
+ for (const base of searchPaths) {
25
+ try {
26
+ const rxjsPath = hostRequire.resolve('rxjs', { paths: [base] });
27
+ cachedRxjs = { rxjs: hostRequire(rxjsPath) };
28
+ return cachedRxjs;
29
+ } catch {
30
+ // try next base
31
+ }
32
+ }
33
+
34
+ try {
35
+ cachedRxjs = { rxjs: require('rxjs') };
36
+ return cachedRxjs;
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Wrap a Nest/RxJS observable and record when the stream completes.
44
+ *
45
+ * @param {unknown} source
46
+ * @param {(exitReason?: string) => void} onFinish
47
+ * @param {import('../core/context').TraceContext} [alsContext]
48
+ * @returns {unknown}
49
+ */
50
+ function traceObservable(source, onFinish, alsContext) {
51
+ const finish = (reason) => {
52
+ if (alsContext) {
53
+ runWithContext(alsContext, () => onFinish(reason));
54
+ return;
55
+ }
56
+ onFinish(reason);
57
+ };
58
+
59
+ if (!source || typeof source.subscribe !== 'function') {
60
+ finish();
61
+ return source;
62
+ }
63
+
64
+ const loaded = loadRxjs();
65
+
66
+ if (!loaded?.rxjs?.Observable) {
67
+ return source;
68
+ }
69
+
70
+ const { Observable } = loaded.rxjs;
71
+
72
+ return new Observable((subscriber) => {
73
+ let innerSub;
74
+
75
+ try {
76
+ innerSub = source.subscribe({
77
+ next: (value) => subscriber.next(value),
78
+ error: (err) => {
79
+ finish(`error: ${err.message}`);
80
+ subscriber.error(err);
81
+ },
82
+ complete: () => {
83
+ finish();
84
+ subscriber.complete();
85
+ },
86
+ });
87
+ } catch (err) {
88
+ finish(`error: ${err.message}`);
89
+ subscriber.error(err);
90
+ }
91
+
92
+ return () => {
93
+ if (innerSub && typeof innerSub.unsubscribe === 'function') {
94
+ innerSub.unsubscribe();
95
+ }
96
+ };
97
+ });
98
+ }
99
+
100
+ module.exports = {
101
+ traceObservable,
102
+ loadRxjs,
103
+ };