@reproapp/node-sdk 0.0.1 → 0.0.2
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/docs/tracing.md +158 -0
- package/package.json +2 -13
- package/src/index.ts +2851 -0
- package/src/integrations/sendgrid.ts +184 -0
- package/tracer/cjs-hook.js +281 -0
- package/tracer/dep-hook.js +142 -0
- package/tracer/esm-loader.mjs +46 -0
- package/tracer/index.js +68 -0
- package/tracer/register.js +194 -0
- package/tracer/runtime.js +963 -0
- package/tracer/server.js +65 -0
- package/tracer/wrap-plugin.js +608 -0
- package/tsconfig.json +12 -0
package/tracer/index.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// index.js
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
trace,
|
|
5
|
+
patchHttp,
|
|
6
|
+
patchArrayIterators,
|
|
7
|
+
patchPromise,
|
|
8
|
+
startV8,
|
|
9
|
+
printV8,
|
|
10
|
+
patchConsole,
|
|
11
|
+
getCurrentTraceId,
|
|
12
|
+
getCurrentSpanContext,
|
|
13
|
+
setFunctionLogsEnabled
|
|
14
|
+
} = require('./runtime');
|
|
15
|
+
const { installCJS } = require('./cjs-hook');
|
|
16
|
+
|
|
17
|
+
let INIT = false;
|
|
18
|
+
|
|
19
|
+
function init(opts = {}) {
|
|
20
|
+
if (INIT) return api();
|
|
21
|
+
INIT = true;
|
|
22
|
+
|
|
23
|
+
const {
|
|
24
|
+
instrument = true,
|
|
25
|
+
include, // e.g. [ new RegExp('^'+process.cwd().replace(/\\/g,'/')+'/') ]
|
|
26
|
+
exclude = [ /node_modules[\\/]/ ],
|
|
27
|
+
parserPlugins = [
|
|
28
|
+
'jsx',
|
|
29
|
+
['decorators', { version: 'legacy' }], // pick '2023-05' mode if you use stage-3
|
|
30
|
+
'classProperties','classPrivateProperties','classPrivateMethods',
|
|
31
|
+
'dynamicImport','topLevelAwait','typescript',
|
|
32
|
+
],
|
|
33
|
+
mode = process.env.TRACE_MODE || 'trace',
|
|
34
|
+
samplingMs = 10,
|
|
35
|
+
functionLogs,
|
|
36
|
+
} = opts;
|
|
37
|
+
|
|
38
|
+
// install http ALS context so Express/Nest/Fastify get traceIds without extra code
|
|
39
|
+
patchHttp();
|
|
40
|
+
patchConsole();
|
|
41
|
+
patchArrayIterators();
|
|
42
|
+
patchPromise();
|
|
43
|
+
|
|
44
|
+
if (typeof functionLogs === 'boolean') setFunctionLogsEnabled(functionLogs);
|
|
45
|
+
|
|
46
|
+
if (instrument) {
|
|
47
|
+
// CJS require hook (for require())
|
|
48
|
+
installCJS({ include, exclude: [...exclude, /node_modules[\\/]@babel[\\/].*/], parserPlugins });
|
|
49
|
+
// ESM users: run with --loader ./omnitrace/esm-loader.mjs (cannot be installed from here)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (String(mode).toLowerCase() === 'v8') startV8(samplingMs);
|
|
53
|
+
process.once('SIGINT', async () => { await printV8(); process.exit(0); });
|
|
54
|
+
|
|
55
|
+
return api();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function api(){
|
|
59
|
+
return {
|
|
60
|
+
init,
|
|
61
|
+
tracer: trace,
|
|
62
|
+
withTrace: trace.withTrace,
|
|
63
|
+
getCurrentTraceId,
|
|
64
|
+
getCurrentSpanContext,
|
|
65
|
+
setFunctionLogsEnabled,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
module.exports = api();
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// register.js
|
|
2
|
+
const escapeRx = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const cwd = process.cwd().replace(/\\/g, '/');
|
|
5
|
+
const hook = require('require-in-the-middle');
|
|
6
|
+
const { instrumentExports } = require('./dep-hook');
|
|
7
|
+
const { SYM_BODY_TRACED } = require('./runtime');
|
|
8
|
+
|
|
9
|
+
hook([], (exports, name, basedir) => {
|
|
10
|
+
// require-in-the-middle passes resolved filename on exports.__filename in some versions
|
|
11
|
+
// but we can safely compute via Module._resolveFilename if needed.
|
|
12
|
+
try {
|
|
13
|
+
// name: module id (e.g., 'express', './local.js')
|
|
14
|
+
// basedir: path of the requiring module
|
|
15
|
+
// We can’t always get the absolute filename reliably here; rely on name when needed.
|
|
16
|
+
const filename = name || '';
|
|
17
|
+
return instrumentExports(exports, filename, name);
|
|
18
|
+
} catch {
|
|
19
|
+
return exports;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const { addHook } = require('import-in-the-middle');
|
|
25
|
+
const { instrumentExports } = require('./dep-hook');
|
|
26
|
+
|
|
27
|
+
addHook((moduleExports, specifier, context) => {
|
|
28
|
+
// specifier: what was imported (e.g., 'kafkajs', 'node:fs', './x.mjs')
|
|
29
|
+
// context.url: the importing module URL (can be 'file:///...')
|
|
30
|
+
// Return the (optionally) modified moduleExports.
|
|
31
|
+
return instrumentExports(moduleExports, specifier || '', specifier || '');
|
|
32
|
+
});
|
|
33
|
+
} catch {}
|
|
34
|
+
|
|
35
|
+
// include: your project (excluding its node_modules), plus the specific third-party files we care about
|
|
36
|
+
const projectNoNodeModules = new RegExp('^' + escapeRx(cwd) + '/(?!node_modules/)');
|
|
37
|
+
// also include the SDK’s own source inside node_modules so its internal calls are traced
|
|
38
|
+
const sdkRoot = path.resolve(__dirname, '..').replace(/\\/g, '/');
|
|
39
|
+
const sdkPath = new RegExp('^' + escapeRx(sdkRoot) + '/');
|
|
40
|
+
// match these files via the hook’s per-file logic; include broad paths so the hook sees them:
|
|
41
|
+
const expressPath = /node_modules[\\/]express[\\/]/;
|
|
42
|
+
const mongoosePath = /node_modules[\\/]mongoose[\\/]/;
|
|
43
|
+
const includeMatchers = [ projectNoNodeModules, sdkPath, expressPath, mongoosePath ];
|
|
44
|
+
const excludeMatchers = [
|
|
45
|
+
/[\\/]omnitrace[\\/].*/, // don't instrument the tracer
|
|
46
|
+
/node_modules[\\/]repro-nest[\\/]tracer[\\/].*/, // avoid instrumenting tracer internals
|
|
47
|
+
/[\\/]tracer[\\/].*/, // skip local tracer sources
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
function shouldHandleCacheFile(file) {
|
|
51
|
+
const f = String(file || '').replace(/\\/g, '/');
|
|
52
|
+
if (!f) return false;
|
|
53
|
+
if (excludeMatchers.some(rx => rx.test(f))) return false;
|
|
54
|
+
return includeMatchers.some(rx => rx.test(f));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function hasBodyTracing(value, seen = new WeakSet(), depth = 0) {
|
|
58
|
+
if (!value || depth > 4) return false;
|
|
59
|
+
const ty = typeof value;
|
|
60
|
+
if (ty !== 'object' && ty !== 'function') return false;
|
|
61
|
+
if (seen.has(value)) return false;
|
|
62
|
+
seen.add(value);
|
|
63
|
+
|
|
64
|
+
if (ty === 'function') {
|
|
65
|
+
try {
|
|
66
|
+
if (value.__repro_instrumented === true) return true;
|
|
67
|
+
if (value[SYM_BODY_TRACED] === true) return true;
|
|
68
|
+
if (value.__repro_body_traced === true) return true;
|
|
69
|
+
} catch {}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
for (const k of Object.getOwnPropertyNames(value)) {
|
|
74
|
+
const d = Object.getOwnPropertyDescriptor(value, k);
|
|
75
|
+
if (!d) continue;
|
|
76
|
+
if ('value' in d) {
|
|
77
|
+
if (hasBodyTracing(d.value, seen, depth + 1)) return true;
|
|
78
|
+
}
|
|
79
|
+
if (typeof d.get === 'function' && hasBodyTracing(d.get, seen, depth + 1)) return true;
|
|
80
|
+
if (typeof d.set === 'function' && hasBodyTracing(d.set, seen, depth + 1)) return true;
|
|
81
|
+
}
|
|
82
|
+
} catch {}
|
|
83
|
+
|
|
84
|
+
const proto = Object.getPrototypeOf(value);
|
|
85
|
+
if (proto && proto !== Object.prototype) {
|
|
86
|
+
if (hasBodyTracing(proto, seen, depth + 1)) return true;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function reloadUninstrumentedAppModules() {
|
|
92
|
+
const reloaded = new Set();
|
|
93
|
+
const mainFile = (() => {
|
|
94
|
+
try { return require.main && require.main.filename; } catch { return null; }
|
|
95
|
+
})() || process.argv[1] || null;
|
|
96
|
+
Object.keys(require.cache || {}).forEach((filename) => {
|
|
97
|
+
try {
|
|
98
|
+
if (!shouldHandleCacheFile(filename)) return;
|
|
99
|
+
// Never reload the main entrypoint; doing so can re-run bootstrap code (e.g., Nest listen())
|
|
100
|
+
// and cause duplicate servers / EADDRINUSE.
|
|
101
|
+
if (mainFile && filename === mainFile) return;
|
|
102
|
+
if (String(filename).replace(/\\/g,'/').includes('/tracer/')) return;
|
|
103
|
+
// Only reload typical app output locations to avoid double-transforming ad-hoc scripts/tests.
|
|
104
|
+
if (!/[\\/]((dist|build|out)[\\/]|src[\\/])/i.test(filename.replace(/\\/g,'/'))) return;
|
|
105
|
+
const cached = require.cache[filename];
|
|
106
|
+
if (!cached || !cached.exports) return;
|
|
107
|
+
if (reloaded.has(filename)) return;
|
|
108
|
+
if (cached.__repro_wrapped) return;
|
|
109
|
+
// If exports already show body-level instrumentation, skip reload.
|
|
110
|
+
if (hasBodyTracing(cached.exports)) return;
|
|
111
|
+
|
|
112
|
+
// Reload the module so it passes through the Babel wrap hook now installed.
|
|
113
|
+
delete require.cache[filename];
|
|
114
|
+
try { require(filename); reloaded.add(filename); } catch {}
|
|
115
|
+
} catch {}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Force-wrap live instances/prototypes for late-loaded classes/objects.
|
|
120
|
+
function forceWrapLiveTargets() {
|
|
121
|
+
try {
|
|
122
|
+
if (typeof instrumentExports !== 'function') return;
|
|
123
|
+
|
|
124
|
+
const seen = new WeakSet();
|
|
125
|
+
const maxDepth = 3;
|
|
126
|
+
const deepInstrument = (val, filename, depth = 0) => {
|
|
127
|
+
if (!val || depth > maxDepth) return;
|
|
128
|
+
const ty = typeof val;
|
|
129
|
+
if (ty !== 'object' && ty !== 'function') return;
|
|
130
|
+
if (seen.has(val)) return;
|
|
131
|
+
seen.add(val);
|
|
132
|
+
try { instrumentExports(val, filename, path.basename(filename)); } catch {}
|
|
133
|
+
try {
|
|
134
|
+
if (ty === 'object') {
|
|
135
|
+
for (const k of Object.keys(val)) {
|
|
136
|
+
deepInstrument(val[k], filename, depth + 1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch {}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
Object.keys(require.cache || {}).forEach((filename) => {
|
|
143
|
+
try {
|
|
144
|
+
if (!shouldHandleCacheFile(filename)) return;
|
|
145
|
+
const cached = require.cache[filename];
|
|
146
|
+
if (!cached || !cached.exports) return;
|
|
147
|
+
const exp = cached.exports;
|
|
148
|
+
|
|
149
|
+
// Wrap exports normally
|
|
150
|
+
instrumentExports(exp, filename, path.basename(filename));
|
|
151
|
+
|
|
152
|
+
// If a class/prototype is exported, wrap its prototype too
|
|
153
|
+
if (typeof exp === 'function' && exp.prototype && typeof exp.prototype === 'object') {
|
|
154
|
+
instrumentExports(exp.prototype, filename + '#prototype', path.basename(filename));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// If default export is an object instance, wrap its own methods
|
|
158
|
+
if (exp && typeof exp === 'object') {
|
|
159
|
+
instrumentExports(exp, filename + '#instance', path.basename(filename));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Deep instrument nested values to catch pre-created instances.
|
|
163
|
+
deepInstrument(exp, filename, 0);
|
|
164
|
+
} catch {}
|
|
165
|
+
});
|
|
166
|
+
} catch {}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
require('./index').init({
|
|
170
|
+
instrument: true,
|
|
171
|
+
mode: process.env.TRACE_MODE || 'trace',
|
|
172
|
+
include: includeMatchers,
|
|
173
|
+
exclude: excludeMatchers,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Opportunistically instrument modules that were loaded before the hook was installed.
|
|
177
|
+
// This helps when the SDK register is required after some app modules are already in require.cache.
|
|
178
|
+
try {
|
|
179
|
+
if (typeof instrumentExports === 'function') {
|
|
180
|
+
Object.keys(require.cache || {}).forEach((filename) => {
|
|
181
|
+
try {
|
|
182
|
+
if (!shouldHandleCacheFile(filename)) return;
|
|
183
|
+
const cached = require.cache[filename];
|
|
184
|
+
if (!cached || !cached.exports) return;
|
|
185
|
+
instrumentExports(cached.exports, filename, path.basename(filename));
|
|
186
|
+
} catch {}
|
|
187
|
+
});
|
|
188
|
+
// Also force-wrap live instances/prototypes after cache pass
|
|
189
|
+
forceWrapLiveTargets();
|
|
190
|
+
}
|
|
191
|
+
} catch {}
|
|
192
|
+
|
|
193
|
+
// Reload already-required app modules that were loaded before the hook so their callsites are wrapped.
|
|
194
|
+
try { reloadUninstrumentedAppModules(); } catch {}
|