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,270 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getCurrentTracer } = require('../core/context');
|
|
4
|
+
|
|
5
|
+
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'all'];
|
|
6
|
+
|
|
7
|
+
let globalPatchApplied = false;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Patch Express Application and Router prototypes so every handler
|
|
11
|
+
* registered after Opencons initialises is automatically wrapped.
|
|
12
|
+
*/
|
|
13
|
+
function patchExpressGlobally() {
|
|
14
|
+
if (globalPatchApplied) return;
|
|
15
|
+
globalPatchApplied = true;
|
|
16
|
+
|
|
17
|
+
const express = require('express');
|
|
18
|
+
|
|
19
|
+
patchLayer(express.application);
|
|
20
|
+
patchRouterPrototype(express.Router.prototype);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @deprecated Use patchExpressGlobally — kept for explicit per-app attachment.
|
|
25
|
+
* @param {import('express').Application} app
|
|
26
|
+
*/
|
|
27
|
+
function patchExpressApp(app) {
|
|
28
|
+
patchExpressGlobally();
|
|
29
|
+
patchLayer(app);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {import('express').Application | import('express').Router} target
|
|
34
|
+
*/
|
|
35
|
+
function patchLayer(target) {
|
|
36
|
+
if (target.__openconsLayerPatched) return;
|
|
37
|
+
target.__openconsLayerPatched = true;
|
|
38
|
+
|
|
39
|
+
const originalUse = target.use;
|
|
40
|
+
target.use = function patchedUse(...args) {
|
|
41
|
+
const wrapped = wrapHandlers(args);
|
|
42
|
+
|
|
43
|
+
for (const fn of wrapped) {
|
|
44
|
+
if (fn && fn.__openconsEntry) {
|
|
45
|
+
patchLayer(this);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return originalUse.apply(this, wrapped);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
for (const method of HTTP_METHODS) {
|
|
53
|
+
if (typeof target[method] !== 'function') continue;
|
|
54
|
+
|
|
55
|
+
const original = target[method];
|
|
56
|
+
target[method] = function patchedMethod(...args) {
|
|
57
|
+
const wrapped = wrapHandlers(args);
|
|
58
|
+
return original.apply(this, wrapped);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {Function} Router
|
|
65
|
+
*/
|
|
66
|
+
function patchRouterPrototype(Router) {
|
|
67
|
+
if (Router.__openconsPatched) return;
|
|
68
|
+
Router.__openconsPatched = true;
|
|
69
|
+
|
|
70
|
+
const originalUse = Router.use;
|
|
71
|
+
Router.use = function patchedRouterUse(...args) {
|
|
72
|
+
const wrapped = wrapHandlers(args);
|
|
73
|
+
return originalUse.apply(this, wrapped);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
for (const method of HTTP_METHODS) {
|
|
77
|
+
if (typeof Router[method] !== 'function') continue;
|
|
78
|
+
|
|
79
|
+
const original = Router[method];
|
|
80
|
+
Router[method] = function patchedRouterMethod(...args) {
|
|
81
|
+
const wrapped = wrapHandlers(args);
|
|
82
|
+
return original.apply(this, wrapped);
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {unknown[]} args
|
|
89
|
+
* @returns {unknown[]}
|
|
90
|
+
*/
|
|
91
|
+
function wrapHandlers(args) {
|
|
92
|
+
const result = [...args];
|
|
93
|
+
const startIndex = typeof result[0] === 'string' || result[0] instanceof RegExp ? 1 : 0;
|
|
94
|
+
|
|
95
|
+
for (let i = startIndex; i < result.length; i += 1) {
|
|
96
|
+
const handler = result[i];
|
|
97
|
+
|
|
98
|
+
if (typeof handler === 'function') {
|
|
99
|
+
result[i] = wrapHandler(handler);
|
|
100
|
+
} else if (Array.isArray(handler)) {
|
|
101
|
+
result[i] = handler.map((fn) =>
|
|
102
|
+
typeof fn === 'function' ? wrapHandler(fn) : fn
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {Function} handler
|
|
112
|
+
* @returns {Function}
|
|
113
|
+
*/
|
|
114
|
+
function wrapHandler(handler) {
|
|
115
|
+
if (handler.__openconsWrapped) return handler;
|
|
116
|
+
|
|
117
|
+
const name = resolveHandlerName(handler);
|
|
118
|
+
|
|
119
|
+
function wrapped(req, res, next) {
|
|
120
|
+
const tracer = getCurrentTracer();
|
|
121
|
+
|
|
122
|
+
if (!tracer) {
|
|
123
|
+
return handler(req, res, next);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const entered = performance.now();
|
|
127
|
+
let calledNext = false;
|
|
128
|
+
let exitReason = null;
|
|
129
|
+
let exited = false;
|
|
130
|
+
|
|
131
|
+
const wrappedNext = (...nextArgs) => {
|
|
132
|
+
calledNext = true;
|
|
133
|
+
|
|
134
|
+
if (!exited) {
|
|
135
|
+
exited = true;
|
|
136
|
+
recordMiddlewareExit();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return next(...nextArgs);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const recordMiddlewareExit = () => {
|
|
143
|
+
const duration_ms = Math.round((performance.now() - entered) * 10) / 10;
|
|
144
|
+
|
|
145
|
+
tracer.addNode({
|
|
146
|
+
type: 'middleware',
|
|
147
|
+
label: name,
|
|
148
|
+
duration_ms,
|
|
149
|
+
called_next: calledNext,
|
|
150
|
+
exit_reason: exitReason || undefined,
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const originalStatus = res.status.bind(res);
|
|
155
|
+
res.status = function patchedStatus(code) {
|
|
156
|
+
if (!calledNext && !exited) {
|
|
157
|
+
exitReason = `res.status(${code})`;
|
|
158
|
+
}
|
|
159
|
+
return originalStatus(code);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const originalJson = res.json.bind(res);
|
|
163
|
+
res.json = function patchedJson(body) {
|
|
164
|
+
if (!calledNext && !exited) {
|
|
165
|
+
exitReason = 'res.json(...)';
|
|
166
|
+
}
|
|
167
|
+
return originalJson(body);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const originalSend = res.send.bind(res);
|
|
171
|
+
res.send = function patchedSend(body) {
|
|
172
|
+
if (!calledNext && !exited) {
|
|
173
|
+
exitReason = 'res.send(...)';
|
|
174
|
+
}
|
|
175
|
+
return originalSend(body);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const originalEnd = res.end.bind(res);
|
|
179
|
+
res.end = function patchedEnd(...endArgs) {
|
|
180
|
+
if (!calledNext && !exited) {
|
|
181
|
+
exitReason = 'res.end(...)';
|
|
182
|
+
exited = true;
|
|
183
|
+
recordMiddlewareExit();
|
|
184
|
+
}
|
|
185
|
+
return originalEnd(...endArgs);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const result = handler(req, res, wrappedNext);
|
|
190
|
+
|
|
191
|
+
if (result && typeof result.then === 'function') {
|
|
192
|
+
result
|
|
193
|
+
.catch((err) => {
|
|
194
|
+
if (!exited) {
|
|
195
|
+
exitReason = `error: ${err.message}`;
|
|
196
|
+
exited = true;
|
|
197
|
+
recordMiddlewareExit();
|
|
198
|
+
}
|
|
199
|
+
if (!calledNext) next(err);
|
|
200
|
+
})
|
|
201
|
+
.finally(() => {
|
|
202
|
+
if (!exited) {
|
|
203
|
+
exited = true;
|
|
204
|
+
recordMiddlewareExit();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
} else if (!exited && !res.headersSent && !calledNext) {
|
|
208
|
+
// Synchronous handler that did not call next or send a response yet.
|
|
209
|
+
// Defer exit recording until response is sent or next is called.
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return result;
|
|
213
|
+
} catch (err) {
|
|
214
|
+
if (!exited) {
|
|
215
|
+
exitReason = `error: ${err.message}`;
|
|
216
|
+
exited = true;
|
|
217
|
+
recordMiddlewareExit();
|
|
218
|
+
}
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
wrapped.__openconsWrapped = true;
|
|
224
|
+
wrapped.__openconsName = name;
|
|
225
|
+
|
|
226
|
+
return wrapped;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* @param {Function} handler
|
|
231
|
+
*/
|
|
232
|
+
function resolveHandlerName(handler) {
|
|
233
|
+
if (handler.__openconsName) return handler.__openconsName;
|
|
234
|
+
|
|
235
|
+
const ctor = handler.constructor?.name;
|
|
236
|
+
let method = handler.name;
|
|
237
|
+
|
|
238
|
+
if (method && method.startsWith('bound ')) {
|
|
239
|
+
method = method.slice('bound '.length);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (ctor && ctor !== 'Function' && ctor !== 'Object') {
|
|
243
|
+
if (method && method !== 'constructor' && method !== 'anonymous') {
|
|
244
|
+
if (method === 'use' || method === 'handle' || method === 'handleRequest') {
|
|
245
|
+
return ctor;
|
|
246
|
+
}
|
|
247
|
+
return `${ctor}.${method}`;
|
|
248
|
+
}
|
|
249
|
+
return ctor;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (method && method !== 'anonymous') {
|
|
253
|
+
return method;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const str = Function.prototype.toString.call(handler);
|
|
257
|
+
const match =
|
|
258
|
+
str.match(/^async\s+function\s+(\w+)/) ||
|
|
259
|
+
str.match(/^function\s+(\w+)/) ||
|
|
260
|
+
str.match(/(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?(?:function|\()/);
|
|
261
|
+
if (match) return match[1];
|
|
262
|
+
|
|
263
|
+
return 'anonymous';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
module.exports = {
|
|
267
|
+
patchExpressGlobally,
|
|
268
|
+
patchExpressApp,
|
|
269
|
+
wrapHandler,
|
|
270
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const Module = require('module');
|
|
6
|
+
const { transformSource } = require('../transform/ast');
|
|
7
|
+
const sourceCache = require('../store/source-cache');
|
|
8
|
+
|
|
9
|
+
const OPENCONS_PKG = path.normalize(path.join(__dirname, '..', '..'));
|
|
10
|
+
|
|
11
|
+
/** @type {boolean} */
|
|
12
|
+
let hookInstalled = false;
|
|
13
|
+
|
|
14
|
+
/** @type {((filename: string) => boolean) | null} */
|
|
15
|
+
let shouldTransformFile = null;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {object} options
|
|
19
|
+
* @param {string[]} [options.exclude]
|
|
20
|
+
* @param {string} [options.projectRoot]
|
|
21
|
+
*/
|
|
22
|
+
function installRequireHook(options = {}) {
|
|
23
|
+
if (hookInstalled) return;
|
|
24
|
+
hookInstalled = true;
|
|
25
|
+
|
|
26
|
+
const projectRoot = options.projectRoot || process.cwd();
|
|
27
|
+
sourceCache.setProjectRoot(projectRoot);
|
|
28
|
+
|
|
29
|
+
shouldTransformFile = createFilter(projectRoot, options.exclude || []);
|
|
30
|
+
|
|
31
|
+
const originalJsHandler = Module._extensions['.js'];
|
|
32
|
+
|
|
33
|
+
Module._extensions['.js'] = function OpenconsJsExtension(module, filename) {
|
|
34
|
+
if (!shouldTransformFile(filename)) {
|
|
35
|
+
return originalJsHandler(module, filename);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let source;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
source = fs.readFileSync(filename, 'utf8');
|
|
42
|
+
} catch (err) {
|
|
43
|
+
return originalJsHandler(module, filename);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
sourceCache.storeOriginal(filename);
|
|
47
|
+
|
|
48
|
+
const result = transformSource(source, filename, { projectRoot });
|
|
49
|
+
|
|
50
|
+
if (result.skipped) {
|
|
51
|
+
sourceCache.store(filename, source, null);
|
|
52
|
+
return module._compile(source, filename);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
sourceCache.store(filename, source, result.map);
|
|
56
|
+
|
|
57
|
+
return module._compile(result.code, filename);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const { logger } = require('../lib/logger');
|
|
61
|
+
logger.info('Source transform hook installed (CommonJS .js)');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {string} projectRoot
|
|
66
|
+
* @param {string[]} excludePatterns
|
|
67
|
+
*/
|
|
68
|
+
function createFilter(projectRoot, excludePatterns) {
|
|
69
|
+
const normalizedRoot = path.normalize(projectRoot);
|
|
70
|
+
|
|
71
|
+
return function filter(filename) {
|
|
72
|
+
const normalized = path.normalize(filename);
|
|
73
|
+
|
|
74
|
+
if (normalized.includes(`${path.sep}node_modules${path.sep}`)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (normalized.startsWith(OPENCONS_PKG)) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!normalized.startsWith(normalizedRoot)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!normalized.endsWith('.js')) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const relative = path.relative(normalizedRoot, normalized);
|
|
91
|
+
|
|
92
|
+
return !excludePatterns.some((pattern) => matchGlob(relative, pattern));
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {string} value
|
|
98
|
+
* @param {string} pattern
|
|
99
|
+
*/
|
|
100
|
+
function matchGlob(value, pattern) {
|
|
101
|
+
const regex = new RegExp(
|
|
102
|
+
`^${pattern.replace(/\*/g, '.*').replace(/\//g, '[\\\\/]').replace(/\\/g, '\\\\')}$`
|
|
103
|
+
);
|
|
104
|
+
return regex.test(value);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
installRequireHook,
|
|
109
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { ConfigurationError } = require('./errors');
|
|
4
|
+
|
|
5
|
+
/** @typedef {import('../core/tracer').TraceGraph} TraceGraph */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} OpenconsOptions
|
|
9
|
+
* @property {number} port
|
|
10
|
+
* @property {boolean | undefined} enabled
|
|
11
|
+
* @property {boolean} enableWidget
|
|
12
|
+
* @property {string[]} exclude
|
|
13
|
+
* @property {boolean} captureBody
|
|
14
|
+
* @property {boolean} captureResponse
|
|
15
|
+
* @property {number} maxTraces
|
|
16
|
+
* @property {Record<'mongoose' | 'drizzle' | 'pg' | 'prisma' | 'mysql2', boolean>} drivers
|
|
17
|
+
* @property {{ enabled: boolean, projectRoot: string | undefined, exclude: string[] }} transform
|
|
18
|
+
* @property {number} [widgetPort]
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const DEFAULT_OPTIONS = {
|
|
22
|
+
port: 7331,
|
|
23
|
+
enabled: undefined,
|
|
24
|
+
enableWidget: true,
|
|
25
|
+
exclude: [],
|
|
26
|
+
captureBody: false,
|
|
27
|
+
captureResponse: false,
|
|
28
|
+
maxTraces: 100,
|
|
29
|
+
drivers: {
|
|
30
|
+
mongoose: true,
|
|
31
|
+
drizzle: true,
|
|
32
|
+
pg: true,
|
|
33
|
+
prisma: true,
|
|
34
|
+
mysql2: true,
|
|
35
|
+
},
|
|
36
|
+
transform: {
|
|
37
|
+
enabled: false,
|
|
38
|
+
projectRoot: undefined,
|
|
39
|
+
exclude: [],
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {unknown} value
|
|
45
|
+
* @param {string} field
|
|
46
|
+
* @returns {number}
|
|
47
|
+
*/
|
|
48
|
+
function requirePositiveInt(value, field) {
|
|
49
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {
|
|
50
|
+
throw new ConfigurationError(`${field} must be a positive integer`);
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {unknown} value
|
|
57
|
+
* @param {string} field
|
|
58
|
+
* @returns {string[]}
|
|
59
|
+
*/
|
|
60
|
+
function requireStringArray(value, field) {
|
|
61
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
|
|
62
|
+
throw new ConfigurationError(`${field} must be an array of strings`);
|
|
63
|
+
}
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Merge user options with defaults and validate types.
|
|
69
|
+
*
|
|
70
|
+
* @param {Partial<OpenconsOptions>} [userOptions]
|
|
71
|
+
* @returns {OpenconsOptions}
|
|
72
|
+
*/
|
|
73
|
+
function resolveOptions(userOptions = {}) {
|
|
74
|
+
const options = {
|
|
75
|
+
...DEFAULT_OPTIONS,
|
|
76
|
+
...userOptions,
|
|
77
|
+
drivers: {
|
|
78
|
+
...DEFAULT_OPTIONS.drivers,
|
|
79
|
+
...(userOptions.drivers || {}),
|
|
80
|
+
},
|
|
81
|
+
transform: {
|
|
82
|
+
...DEFAULT_OPTIONS.transform,
|
|
83
|
+
...(userOptions.transform || {}),
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
options.port = requirePositiveInt(options.port, 'port');
|
|
88
|
+
options.maxTraces = requirePositiveInt(options.maxTraces, 'maxTraces');
|
|
89
|
+
options.exclude = requireStringArray(options.exclude, 'exclude');
|
|
90
|
+
options.transform.exclude = requireStringArray(options.transform.exclude, 'transform.exclude');
|
|
91
|
+
|
|
92
|
+
if (typeof options.enableWidget !== 'boolean') {
|
|
93
|
+
throw new ConfigurationError('enableWidget must be a boolean');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (typeof options.captureBody !== 'boolean') {
|
|
97
|
+
throw new ConfigurationError('captureBody must be a boolean');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (typeof options.captureResponse !== 'boolean') {
|
|
101
|
+
throw new ConfigurationError('captureResponse must be a boolean');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (options.enabled !== undefined && typeof options.enabled !== 'boolean') {
|
|
105
|
+
throw new ConfigurationError('enabled must be a boolean when provided');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const [driver, enabled] of Object.entries(options.drivers)) {
|
|
109
|
+
if (typeof enabled !== 'boolean') {
|
|
110
|
+
throw new ConfigurationError(`drivers.${driver} must be a boolean`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (typeof options.transform.enabled !== 'boolean') {
|
|
115
|
+
throw new ConfigurationError('transform.enabled must be a boolean');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (
|
|
119
|
+
options.transform.projectRoot !== undefined &&
|
|
120
|
+
typeof options.transform.projectRoot !== 'string'
|
|
121
|
+
) {
|
|
122
|
+
throw new ConfigurationError('transform.projectRoot must be a string when provided');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return options;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @returns {boolean}
|
|
130
|
+
*/
|
|
131
|
+
function isProductionDisabled(options) {
|
|
132
|
+
return process.env.NODE_ENV === 'production' && options.enabled !== true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = {
|
|
136
|
+
DEFAULT_OPTIONS,
|
|
137
|
+
resolveOptions,
|
|
138
|
+
isProductionDisabled,
|
|
139
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
class OpenconsError extends Error {
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} message
|
|
6
|
+
* @param {string} [code]
|
|
7
|
+
*/
|
|
8
|
+
constructor(message, code = 'OPENCONS_ERROR') {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'OpenconsError';
|
|
11
|
+
this.code = code;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class ConfigurationError extends OpenconsError {
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} message
|
|
18
|
+
*/
|
|
19
|
+
constructor(message) {
|
|
20
|
+
super(message, 'CONFIGURATION_ERROR');
|
|
21
|
+
this.name = 'ConfigurationError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class WidgetServerError extends OpenconsError {
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} message
|
|
28
|
+
* @param {NodeJS.ErrnoException | null} [cause]
|
|
29
|
+
*/
|
|
30
|
+
constructor(message, cause = null) {
|
|
31
|
+
super(message, 'WIDGET_SERVER_ERROR');
|
|
32
|
+
this.name = 'WidgetServerError';
|
|
33
|
+
if (cause) {
|
|
34
|
+
this.cause = cause;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class WebSocketError extends OpenconsError {
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} message
|
|
42
|
+
*/
|
|
43
|
+
constructor(message) {
|
|
44
|
+
super(message, 'WEBSOCKET_ERROR');
|
|
45
|
+
this.name = 'WebSocketError';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
OpenconsError,
|
|
51
|
+
ConfigurationError,
|
|
52
|
+
WidgetServerError,
|
|
53
|
+
WebSocketError,
|
|
54
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {import('http').ServerResponse} res
|
|
5
|
+
* @param {number} status
|
|
6
|
+
* @param {string} body
|
|
7
|
+
* @param {string} [contentType]
|
|
8
|
+
*/
|
|
9
|
+
function sendText(res, status, body, contentType = 'text/plain; charset=utf-8') {
|
|
10
|
+
res.writeHead(status, { 'Content-Type': contentType });
|
|
11
|
+
res.end(body);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {import('http').ServerResponse} res
|
|
16
|
+
* @param {number} status
|
|
17
|
+
* @param {{ error: string, code?: string }} payload
|
|
18
|
+
*/
|
|
19
|
+
function sendJsonError(res, status, payload) {
|
|
20
|
+
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
21
|
+
res.end(JSON.stringify(payload));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {import('http').ServerResponse} res
|
|
26
|
+
* @param {unknown} payload
|
|
27
|
+
*/
|
|
28
|
+
function sendJson(res, payload) {
|
|
29
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
30
|
+
res.end(JSON.stringify(payload));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
sendText,
|
|
35
|
+
sendJsonError,
|
|
36
|
+
sendJson,
|
|
37
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/** @typedef {'debug' | 'info' | 'warn' | 'error'} LogLevel */
|
|
4
|
+
|
|
5
|
+
const LEVELS = /** @type {const} */ ({ debug: 0, info: 1, warn: 2, error: 3 });
|
|
6
|
+
|
|
7
|
+
/** @type {LogLevel} */
|
|
8
|
+
const logLevel = process.env.OPENCONS_LOG_LEVEL || process.env.ROUTEGRAPHER_LOG_LEVEL;
|
|
9
|
+
let minLevel = logLevel === 'debug' ? 'debug' : 'info';
|
|
10
|
+
|
|
11
|
+
const PREFIX = '[Opencons]';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {LogLevel} level
|
|
15
|
+
* @param {string} message
|
|
16
|
+
* @param {unknown} [detail]
|
|
17
|
+
*/
|
|
18
|
+
function write(level, message, detail) {
|
|
19
|
+
if (LEVELS[level] < LEVELS[minLevel]) return;
|
|
20
|
+
|
|
21
|
+
const line = `${PREFIX} ${message}`;
|
|
22
|
+
const sink =
|
|
23
|
+
level === 'error' ? console.error : level === 'warn' ? console.warn : console.log;
|
|
24
|
+
|
|
25
|
+
if (detail !== undefined) {
|
|
26
|
+
sink(line, detail);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
sink(line);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const logger = {
|
|
34
|
+
/**
|
|
35
|
+
* @param {LogLevel} level
|
|
36
|
+
*/
|
|
37
|
+
setLevel(level) {
|
|
38
|
+
if (level in LEVELS) {
|
|
39
|
+
minLevel = level;
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
/** @returns {LogLevel} */
|
|
44
|
+
getLevel() {
|
|
45
|
+
return minLevel;
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
/** @param {string} message @param {unknown} [detail] */
|
|
49
|
+
debug(message, detail) {
|
|
50
|
+
write('debug', message, detail);
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
/** @param {string} message @param {unknown} [detail] */
|
|
54
|
+
info(message, detail) {
|
|
55
|
+
write('info', message, detail);
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
/** @param {string} message @param {unknown} [detail] */
|
|
59
|
+
warn(message, detail) {
|
|
60
|
+
write('warn', message, detail);
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
/** @param {string} message @param {unknown} [detail] */
|
|
64
|
+
error(message, detail) {
|
|
65
|
+
write('error', message, detail);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
module.exports = { logger };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deep-clone a value for trace snapshots. Falls back to a placeholder when
|
|
5
|
+
* JSON serialization fails (circular refs, BigInt, etc.).
|
|
6
|
+
*
|
|
7
|
+
* @param {unknown} value
|
|
8
|
+
* @returns {unknown}
|
|
9
|
+
*/
|
|
10
|
+
function safeClone(value) {
|
|
11
|
+
if (value === undefined || value === null) return value;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(JSON.stringify(value));
|
|
15
|
+
} catch {
|
|
16
|
+
return '[unserializable]';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
safeClone,
|
|
22
|
+
};
|