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.
- package/LICENSE +21 -0
- package/README.md +382 -0
- package/opencons.d.ts +55 -0
- package/package.json +73 -0
- package/scripts/vendor-d3.js +22 -0
- package/src/core/context.js +44 -0
- package/src/core/index.js +198 -0
- package/src/core/tracer.js +252 -0
- package/src/drivers/db-language.js +207 -0
- package/src/drivers/detect.js +62 -0
- package/src/drivers/drizzle.js +87 -0
- package/src/drivers/index.js +43 -0
- package/src/drivers/mongoose.js +89 -0
- package/src/drivers/mysql2.js +116 -0
- package/src/drivers/pg.js +130 -0
- package/src/drivers/prisma.js +109 -0
- package/src/drivers/record.js +158 -0
- package/src/index.js +28 -0
- package/src/integrations/nest-lifecycle.js +357 -0
- package/src/integrations/nest.js +89 -0
- package/src/interceptors/express.js +270 -0
- package/src/interceptors/require-hook.js +109 -0
- package/src/lib/config.js +139 -0
- package/src/lib/errors.js +54 -0
- package/src/lib/http-response.js +37 -0
- package/src/lib/logger.js +69 -0
- package/src/lib/serialize.js +22 -0
- package/src/server/static.js +165 -0
- package/src/server/ws.js +62 -0
- package/src/store/source-cache.js +120 -0
- package/src/store/trace-store.js +117 -0
- package/src/transform/ast.js +255 -0
- package/src/transform/natural-language.js +146 -0
- package/src/transform/probe.js +161 -0
- package/src/transform/register.js +44 -0
- package/src/utils/label.js +26 -0
- package/src/utils/observable.js +103 -0
- package/widget/app.js +356 -0
- package/widget/db-language.js +90 -0
- package/widget/graph.js +1167 -0
- package/widget/index.html +132 -0
- package/widget/styles.css +773 -0
- package/widget/timeline.js +57 -0
- 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
|
+
};
|