trickle-observe 0.2.128 → 0.2.130
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/dist/register.d.ts +2 -2
- package/dist/register.js +134 -14
- package/observe-esm-hooks.mjs +131 -5
- package/package.json +1 -1
- package/src/register.ts +130 -15
package/dist/register.d.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
*
|
|
10
10
|
* TRICKLE_BACKEND_URL=http://localhost:4888 node -r trickle/register app.js
|
|
11
11
|
*
|
|
12
|
-
* This module patches Node's module loader to intercept
|
|
13
|
-
* and automatically instrument any
|
|
12
|
+
* This module patches Node's module loader to intercept framework requires
|
|
13
|
+
* (Express, Fastify, Koa, Hono) and automatically instrument any app created.
|
|
14
14
|
*
|
|
15
15
|
* Supported environment variables:
|
|
16
16
|
* TRICKLE_BACKEND_URL — Backend URL (default: http://localhost:4888)
|
package/dist/register.js
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
*
|
|
11
11
|
* TRICKLE_BACKEND_URL=http://localhost:4888 node -r trickle/register app.js
|
|
12
12
|
*
|
|
13
|
-
* This module patches Node's module loader to intercept
|
|
14
|
-
* and automatically instrument any
|
|
13
|
+
* This module patches Node's module loader to intercept framework requires
|
|
14
|
+
* (Express, Fastify, Koa, Hono) and automatically instrument any app created.
|
|
15
15
|
*
|
|
16
16
|
* Supported environment variables:
|
|
17
17
|
* TRICKLE_BACKEND_URL — Backend URL (default: http://localhost:4888)
|
|
@@ -26,6 +26,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
26
26
|
const module_1 = __importDefault(require("module"));
|
|
27
27
|
const transport_1 = require("./transport");
|
|
28
28
|
const express_1 = require("./express");
|
|
29
|
+
const fastify_1 = require("./fastify");
|
|
30
|
+
const koa_1 = require("./koa");
|
|
31
|
+
const hono_1 = require("./hono");
|
|
29
32
|
const env_detect_1 = require("./env-detect");
|
|
30
33
|
const M = module_1.default;
|
|
31
34
|
const originalLoad = M._load;
|
|
@@ -35,6 +38,7 @@ const backendUrl = process.env.TRICKLE_BACKEND_URL || 'http://localhost:4888';
|
|
|
35
38
|
const enabled = process.env.TRICKLE_ENABLED !== '0' && process.env.TRICKLE_ENABLED !== 'false';
|
|
36
39
|
const debug = process.env.TRICKLE_DEBUG === '1' || process.env.TRICKLE_DEBUG === 'true';
|
|
37
40
|
const envOverride = process.env.TRICKLE_ENV || undefined;
|
|
41
|
+
const environment = envOverride || (0, env_detect_1.detectEnvironment)();
|
|
38
42
|
if (enabled) {
|
|
39
43
|
// Configure the transport
|
|
40
44
|
(0, transport_1.configure)({
|
|
@@ -42,7 +46,7 @@ if (enabled) {
|
|
|
42
46
|
batchIntervalMs: 2000,
|
|
43
47
|
debug,
|
|
44
48
|
enabled: true,
|
|
45
|
-
environment
|
|
49
|
+
environment,
|
|
46
50
|
});
|
|
47
51
|
if (debug) {
|
|
48
52
|
console.log(`[trickle] Auto-instrumentation enabled (backend: ${backendUrl})`);
|
|
@@ -55,6 +59,21 @@ if (enabled) {
|
|
|
55
59
|
patched.add('express');
|
|
56
60
|
return patchExpress(exports, request, parent);
|
|
57
61
|
}
|
|
62
|
+
// Intercept require('fastify')
|
|
63
|
+
if (request === 'fastify' && !patched.has('fastify')) {
|
|
64
|
+
patched.add('fastify');
|
|
65
|
+
return patchFastify(exports, request, parent);
|
|
66
|
+
}
|
|
67
|
+
// Intercept require('koa')
|
|
68
|
+
if (request === 'koa' && !patched.has('koa')) {
|
|
69
|
+
patched.add('koa');
|
|
70
|
+
return patchKoa(exports, request, parent);
|
|
71
|
+
}
|
|
72
|
+
// Intercept require('hono')
|
|
73
|
+
if (request === 'hono' && !patched.has('hono')) {
|
|
74
|
+
patched.add('hono');
|
|
75
|
+
return patchHono(exports, request, parent);
|
|
76
|
+
}
|
|
58
77
|
return exports;
|
|
59
78
|
};
|
|
60
79
|
}
|
|
@@ -63,15 +82,12 @@ else if (debug) {
|
|
|
63
82
|
}
|
|
64
83
|
/**
|
|
65
84
|
* Wrap the Express factory function so every app created is auto-instrumented.
|
|
66
|
-
* Preserves all static properties (express.json, express.static, etc.).
|
|
67
85
|
*/
|
|
68
86
|
function patchExpress(originalExpress, request, parent) {
|
|
69
87
|
function wrappedExpress(...args) {
|
|
70
88
|
const app = originalExpress.apply(this, args);
|
|
71
89
|
try {
|
|
72
|
-
(0, express_1.instrumentExpress)(app, {
|
|
73
|
-
environment: envOverride || (0, env_detect_1.detectEnvironment)(),
|
|
74
|
-
});
|
|
90
|
+
(0, express_1.instrumentExpress)(app, { environment });
|
|
75
91
|
if (debug) {
|
|
76
92
|
console.log('[trickle] Auto-instrumented Express app');
|
|
77
93
|
}
|
|
@@ -84,22 +100,126 @@ function patchExpress(originalExpress, request, parent) {
|
|
|
84
100
|
}
|
|
85
101
|
return app;
|
|
86
102
|
}
|
|
87
|
-
// Copy all static properties (express.json, express.static, express.Router, etc.)
|
|
88
103
|
for (const key of Object.keys(originalExpress)) {
|
|
89
104
|
wrappedExpress[key] = originalExpress[key];
|
|
90
105
|
}
|
|
91
|
-
// Preserve prototype chain
|
|
92
106
|
Object.setPrototypeOf(wrappedExpress, Object.getPrototypeOf(originalExpress));
|
|
93
|
-
// Update require cache so subsequent require('express') returns the patched version
|
|
94
107
|
try {
|
|
95
108
|
const resolvedPath = M._resolveFilename(request, parent);
|
|
96
109
|
if (require.cache[resolvedPath]) {
|
|
97
110
|
require.cache[resolvedPath].exports = wrappedExpress;
|
|
98
111
|
}
|
|
99
112
|
}
|
|
100
|
-
catch {
|
|
101
|
-
// Cache update failed — first require still returns patched, but subsequent
|
|
102
|
-
// requires from other modules may get the original. This is rare.
|
|
103
|
-
}
|
|
113
|
+
catch { }
|
|
104
114
|
return wrappedExpress;
|
|
105
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Wrap the Fastify factory function.
|
|
118
|
+
*/
|
|
119
|
+
function patchFastify(origExports, request, parent) {
|
|
120
|
+
const factory = typeof origExports === 'function' ? origExports : origExports.default || origExports.fastify;
|
|
121
|
+
if (typeof factory !== 'function')
|
|
122
|
+
return origExports;
|
|
123
|
+
const wrappedFactory = function (...args) {
|
|
124
|
+
const app = factory.apply(this, args);
|
|
125
|
+
try {
|
|
126
|
+
(0, fastify_1.instrumentFastify)(app, { environment });
|
|
127
|
+
if (debug)
|
|
128
|
+
console.log('[trickle] Auto-instrumented Fastify app');
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
if (debug)
|
|
132
|
+
console.warn(`[trickle] Failed to auto-instrument Fastify: ${err.message}`);
|
|
133
|
+
}
|
|
134
|
+
return app;
|
|
135
|
+
};
|
|
136
|
+
for (const key of Object.keys(factory)) {
|
|
137
|
+
wrappedFactory[key] = factory[key];
|
|
138
|
+
}
|
|
139
|
+
if (typeof origExports === 'function') {
|
|
140
|
+
try {
|
|
141
|
+
const resolvedPath = M._resolveFilename(request, parent);
|
|
142
|
+
if (require.cache[resolvedPath])
|
|
143
|
+
require.cache[resolvedPath].exports = wrappedFactory;
|
|
144
|
+
}
|
|
145
|
+
catch { }
|
|
146
|
+
return wrappedFactory;
|
|
147
|
+
}
|
|
148
|
+
const wrapped = { ...origExports };
|
|
149
|
+
if (origExports.default)
|
|
150
|
+
wrapped.default = wrappedFactory;
|
|
151
|
+
if (origExports.fastify)
|
|
152
|
+
wrapped.fastify = wrappedFactory;
|
|
153
|
+
return wrapped;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Wrap the Koa constructor.
|
|
157
|
+
*/
|
|
158
|
+
function patchKoa(origExports, request, parent) {
|
|
159
|
+
const KoaClass = typeof origExports === 'function' ? origExports : origExports.default;
|
|
160
|
+
if (typeof KoaClass !== 'function')
|
|
161
|
+
return origExports;
|
|
162
|
+
const WrappedKoa = function (...args) {
|
|
163
|
+
const app = new KoaClass(...args);
|
|
164
|
+
try {
|
|
165
|
+
(0, koa_1.instrumentKoa)(app, { environment });
|
|
166
|
+
if (debug)
|
|
167
|
+
console.log('[trickle] Auto-instrumented Koa app');
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
if (debug)
|
|
171
|
+
console.warn(`[trickle] Failed to auto-instrument Koa: ${err.message}`);
|
|
172
|
+
}
|
|
173
|
+
return app;
|
|
174
|
+
};
|
|
175
|
+
WrappedKoa.prototype = KoaClass.prototype;
|
|
176
|
+
for (const key of Object.keys(KoaClass)) {
|
|
177
|
+
WrappedKoa[key] = KoaClass[key];
|
|
178
|
+
}
|
|
179
|
+
if (typeof origExports === 'function') {
|
|
180
|
+
try {
|
|
181
|
+
const resolvedPath = M._resolveFilename(request, parent);
|
|
182
|
+
if (require.cache[resolvedPath])
|
|
183
|
+
require.cache[resolvedPath].exports = WrappedKoa;
|
|
184
|
+
}
|
|
185
|
+
catch { }
|
|
186
|
+
return WrappedKoa;
|
|
187
|
+
}
|
|
188
|
+
return { ...origExports, default: WrappedKoa };
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Wrap the Hono constructor.
|
|
192
|
+
*/
|
|
193
|
+
function patchHono(origExports, request, parent) {
|
|
194
|
+
const HonoClass = origExports.Hono || (origExports.default && origExports.default.Hono);
|
|
195
|
+
if (typeof HonoClass !== 'function')
|
|
196
|
+
return origExports;
|
|
197
|
+
const WrappedHono = function (...args) {
|
|
198
|
+
const app = new HonoClass(...args);
|
|
199
|
+
try {
|
|
200
|
+
(0, hono_1.instrumentHono)(app, { environment });
|
|
201
|
+
if (debug)
|
|
202
|
+
console.log('[trickle] Auto-instrumented Hono app');
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
if (debug)
|
|
206
|
+
console.warn(`[trickle] Failed to auto-instrument Hono: ${err.message}`);
|
|
207
|
+
}
|
|
208
|
+
return app;
|
|
209
|
+
};
|
|
210
|
+
WrappedHono.prototype = HonoClass.prototype;
|
|
211
|
+
for (const key of Object.keys(HonoClass)) {
|
|
212
|
+
WrappedHono[key] = HonoClass[key];
|
|
213
|
+
}
|
|
214
|
+
const result = { ...origExports, Hono: WrappedHono };
|
|
215
|
+
try {
|
|
216
|
+
const resolvedPath = M._resolveFilename(request, parent);
|
|
217
|
+
if (require.cache[resolvedPath]) {
|
|
218
|
+
const cached = require.cache[resolvedPath].exports;
|
|
219
|
+
if (cached.Hono)
|
|
220
|
+
cached.Hono = WrappedHono;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch { }
|
|
224
|
+
return result;
|
|
225
|
+
}
|
package/observe-esm-hooks.mjs
CHANGED
|
@@ -648,24 +648,106 @@ function transformSource(source, url, originalSource) {
|
|
|
648
648
|
}
|
|
649
649
|
}
|
|
650
650
|
|
|
651
|
-
//
|
|
652
|
-
|
|
651
|
+
// ── Detect framework imports and inject instrumentation ──
|
|
652
|
+
// Scan for: import express from 'express' / import { Hono } from 'hono' etc.
|
|
653
|
+
const frameworkInjects = [];
|
|
654
|
+
const expressImportMatch = source.match(/import\s+(\w+)\s+from\s+['"]express['"]/);
|
|
655
|
+
const fastifyImportMatch = source.match(/import\s+(\w+)\s+from\s+['"]fastify['"]/);
|
|
656
|
+
const koaImportMatch = source.match(/import\s+(\w+)\s+from\s+['"]koa['"]/);
|
|
657
|
+
const honoImportMatch = source.match(/import\s+\{\s*Hono\s*\}\s+from\s+['"]hono['"]/);
|
|
658
|
+
|
|
659
|
+
if (expressImportMatch) {
|
|
660
|
+
// Express: imported as factory. Detect `const app = express()` and inject instrument(app)
|
|
661
|
+
const factoryName = expressImportMatch[1];
|
|
662
|
+
const appMatch = source.match(new RegExp(`(?:const|let|var)\\s+(\\w+)\\s*=\\s*${factoryName}\\s*\\(`));
|
|
663
|
+
if (appMatch) {
|
|
664
|
+
frameworkInjects.push({ framework: 'express', appVar: appMatch[1], importPath: config.wrapperPath.replace('/wrap.js', '/express.js') });
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (fastifyImportMatch) {
|
|
668
|
+
const factoryName = fastifyImportMatch[1];
|
|
669
|
+
const appMatch = source.match(new RegExp(`(?:const|let|var)\\s+(\\w+)\\s*=\\s*${factoryName}\\s*\\(`));
|
|
670
|
+
if (appMatch) {
|
|
671
|
+
frameworkInjects.push({ framework: 'fastify', appVar: appMatch[1], importPath: config.wrapperPath.replace('/wrap.js', '/fastify.js') });
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (koaImportMatch) {
|
|
675
|
+
const className = koaImportMatch[1];
|
|
676
|
+
const appMatch = source.match(new RegExp(`(?:const|let|var)\\s+(\\w+)\\s*=\\s*new\\s+${className}\\s*\\(`));
|
|
677
|
+
if (appMatch) {
|
|
678
|
+
frameworkInjects.push({ framework: 'koa', appVar: appMatch[1], importPath: config.wrapperPath.replace('/wrap.js', '/koa.js') });
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if (honoImportMatch) {
|
|
682
|
+
const appMatch = source.match(/(?:const|let|var)\s+(\w+)\s*=\s*new\s+Hono\s*\(/);
|
|
683
|
+
if (appMatch) {
|
|
684
|
+
frameworkInjects.push({ framework: 'hono', appVar: appMatch[1], importPath: config.wrapperPath.replace('/wrap.js', '/hono.js') });
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// If nothing to wrap or trace, but we have framework injections, continue
|
|
689
|
+
if (exportedFunctions.length === 0 && exportedDefaults.length === 0 && namedExports.length === 0 && !hasVarTracing && frameworkInjects.length === 0) {
|
|
653
690
|
return source;
|
|
654
691
|
}
|
|
655
692
|
|
|
693
|
+
// If ONLY framework injections (no function wrapping needed), inject directly
|
|
694
|
+
if (exportedFunctions.length === 0 && exportedDefaults.length === 0 && namedExports.length === 0 && !hasVarTracing && frameworkInjects.length > 0) {
|
|
695
|
+
const lines = source.split('\n');
|
|
696
|
+
const injections = [];
|
|
697
|
+
for (const fi of frameworkInjects) {
|
|
698
|
+
const instrumentFn = fi.framework === 'express' ? 'instrumentExpress'
|
|
699
|
+
: fi.framework === 'fastify' ? 'instrumentFastify'
|
|
700
|
+
: fi.framework === 'koa' ? 'instrumentKoa'
|
|
701
|
+
: 'instrumentHono';
|
|
702
|
+
const fiPath = fi.importPath.replace(/\\/g, '\\\\');
|
|
703
|
+
injections.push(`import { createRequire as __cr_fw } from 'node:module';`);
|
|
704
|
+
injections.push(`const __req_fw = __cr_fw(import.meta.url);`);
|
|
705
|
+
injections.push(`try { const __fwMod = __req_fw('${fiPath}'); __fwMod.${instrumentFn}(${fi.appVar}, { environment: process.env.TRICKLE_ENV || 'development' }); } catch(__e) { if (${config.debug}) console.error('[trickle/esm] Framework instrumentation failed:', __e.message); }`);
|
|
706
|
+
}
|
|
707
|
+
// Find the line after the app creation and insert
|
|
708
|
+
for (const fi of frameworkInjects) {
|
|
709
|
+
const appPattern = new RegExp(`(?:const|let|var)\\s+${fi.appVar}\\s*=`);
|
|
710
|
+
for (let i = 0; i < lines.length; i++) {
|
|
711
|
+
if (appPattern.test(lines[i])) {
|
|
712
|
+
// Find end of statement
|
|
713
|
+
let endLine = i;
|
|
714
|
+
let depth = 0;
|
|
715
|
+
for (let j = i; j < lines.length; j++) {
|
|
716
|
+
for (const ch of lines[j]) {
|
|
717
|
+
if (ch === '(' || ch === '{' || ch === '[') depth++;
|
|
718
|
+
if (ch === ')' || ch === '}' || ch === ']') depth--;
|
|
719
|
+
}
|
|
720
|
+
if (lines[j].includes(';') && depth <= 0) { endLine = j; break; }
|
|
721
|
+
if (j > i && depth <= 0) { endLine = j - 1; break; }
|
|
722
|
+
}
|
|
723
|
+
lines.splice(endLine + 1, 0, ...injections);
|
|
724
|
+
break;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (config.debug) {
|
|
729
|
+
console.log(`[trickle/esm] Injected ${frameworkInjects.map(f => f.framework).join(', ')} instrumentation into ${moduleName}`);
|
|
730
|
+
}
|
|
731
|
+
return lines.join('\n');
|
|
732
|
+
}
|
|
733
|
+
|
|
656
734
|
// Add wrapper import and wrapping code
|
|
657
735
|
const wrapperPathEscaped = config.wrapperPath.replace(/\\/g, '\\\\');
|
|
658
736
|
|
|
659
737
|
// Insert wrapper setup at the top (after imports, before user code)
|
|
660
738
|
// Using import + createRequire to load CJS wrapper from ESM context
|
|
739
|
+
// Always create __require if we have exports OR framework injections
|
|
740
|
+
const needsWrapper = exportedFunctions.length > 0 || exportedDefaults.length > 0 || namedExports.length > 0 || frameworkInjects.length > 0;
|
|
661
741
|
const wrapSetup = [
|
|
662
742
|
'',
|
|
663
743
|
'// [trickle] Auto-observation wrappers',
|
|
664
744
|
`import { createRequire as __cr } from 'node:module';`,
|
|
665
745
|
`const __require = __cr(import.meta.url);`,
|
|
666
|
-
`const { wrapFunction: __tw } = __require('${wrapperPathEscaped}');`,
|
|
667
|
-
`const __twOpts = (name, paramNames) => { const o = { functionName: name, module: '${moduleName}', trackArgs: true, trackReturn: true, sampleRate: 1, maxDepth: 5, environment: process.env.TRICKLE_ENV || 'development', enabled: true }; if (paramNames && paramNames.length) o.paramNames = paramNames; return o; };`,
|
|
668
746
|
];
|
|
747
|
+
if (exportedFunctions.length > 0 || exportedDefaults.length > 0 || namedExports.length > 0) {
|
|
748
|
+
wrapSetup.push(`const { wrapFunction: __tw } = __require('${wrapperPathEscaped}');`);
|
|
749
|
+
wrapSetup.push(`const __twOpts = (name, paramNames) => { const o = { functionName: name, module: '${moduleName}', trackArgs: true, trackReturn: true, sampleRate: 1, maxDepth: 5, environment: process.env.TRICKLE_ENV || 'development', enabled: true }; if (paramNames && paramNames.length) o.paramNames = paramNames; return o; };`);
|
|
750
|
+
}
|
|
669
751
|
|
|
670
752
|
// For in-place wrapping: insert wrapper calls right after each function declaration
|
|
671
753
|
// This ensures functions are wrapped BEFORE any top-level code calls them
|
|
@@ -726,17 +808,61 @@ function transformSource(source, url, originalSource) {
|
|
|
726
808
|
result.push(`export default __trickle_default_wrapped;`);
|
|
727
809
|
}
|
|
728
810
|
|
|
811
|
+
// Inject framework instrumentation if detected
|
|
812
|
+
if (frameworkInjects.length > 0) {
|
|
813
|
+
for (const fi of frameworkInjects) {
|
|
814
|
+
const instrumentFn = fi.framework === 'express' ? 'instrumentExpress'
|
|
815
|
+
: fi.framework === 'fastify' ? 'instrumentFastify'
|
|
816
|
+
: fi.framework === 'koa' ? 'instrumentKoa'
|
|
817
|
+
: 'instrumentHono';
|
|
818
|
+
const fiPath = fi.importPath.replace(/\\/g, '\\\\');
|
|
819
|
+
// Find where the app is created and inject after
|
|
820
|
+
const appPattern = new RegExp(`(?:const|let|var)\\s+${fi.appVar}\\s*=`);
|
|
821
|
+
for (let ri = 0; ri < result.length; ri++) {
|
|
822
|
+
if (appPattern.test(result[ri])) {
|
|
823
|
+
let endLine = ri;
|
|
824
|
+
let depth = 0;
|
|
825
|
+
for (let j = ri; j < result.length; j++) {
|
|
826
|
+
for (const ch of result[j]) {
|
|
827
|
+
if (ch === '(' || ch === '{' || ch === '[') depth++;
|
|
828
|
+
if (ch === ')' || ch === '}' || ch === ']') depth--;
|
|
829
|
+
}
|
|
830
|
+
if (result[j].includes(';') && depth <= 0) { endLine = j; break; }
|
|
831
|
+
if (j > ri && depth <= 0) { endLine = j - 1; break; }
|
|
832
|
+
}
|
|
833
|
+
// We need to inject BEFORE routes are defined. Since __require from wrapSetup
|
|
834
|
+
// may not be initialized yet, we add a separate import + require pair.
|
|
835
|
+
// ESM import declarations are hoisted, so this import is available immediately.
|
|
836
|
+
// We use __cr_fw (unique name) to avoid conflicts with other createRequire imports.
|
|
837
|
+
const crImportLine = `import { createRequire as __cr_fw_${fi.framework} } from 'node:module';`;
|
|
838
|
+
// Insert the import at the very beginning of result
|
|
839
|
+
result.unshift(crImportLine);
|
|
840
|
+
// The endLine index shifted by 1 due to unshift
|
|
841
|
+
result.splice(endLine + 2, 0,
|
|
842
|
+
`try { const __rq_fw = __cr_fw_${fi.framework}(import.meta.url); const __fw = __rq_fw('${fiPath}'); __fw.${instrumentFn}(${fi.appVar}, { environment: process.env.TRICKLE_ENV || 'development' }); } catch(__e) { if (${config.debug}) console.error('[trickle/esm] Framework injection error:', __e.message); }`
|
|
843
|
+
);
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
729
850
|
const transformed = result.join('\n');
|
|
730
851
|
|
|
731
852
|
if (config.debug) {
|
|
732
853
|
const fnCount = exportedFunctions.length + exportedDefaults.length + namedExports.length;
|
|
733
854
|
const varCount = varDecls.length + destructDecls.length;
|
|
734
|
-
|
|
855
|
+
const fwCount = frameworkInjects.length;
|
|
856
|
+
console.log(`[trickle/esm] Transformed ${fnCount} exports, ${varCount} vars, ${fwCount} frameworks from ${moduleName}`);
|
|
735
857
|
}
|
|
736
858
|
|
|
737
859
|
return transformed;
|
|
738
860
|
}
|
|
739
861
|
|
|
862
|
+
// ── Framework auto-instrumentation for ESM ──
|
|
863
|
+
// ESM imports bypass Module._load, so CJS hooks don't fire.
|
|
864
|
+
// We detect framework imports in user source code and inject instrumentation calls.
|
|
865
|
+
|
|
740
866
|
/**
|
|
741
867
|
* ESM load hook — intercepts module loading to transform user modules.
|
|
742
868
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trickle-observe",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.130",
|
|
4
4
|
"description": "Zero-code runtime observability for JavaScript/TypeScript. Auto-instruments Express, Fastify, Koa, Hono, OpenAI, Anthropic, Gemini, MCP. Captures functions, variables, LLM calls, agent workflows.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
package/src/register.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
*
|
|
10
10
|
* TRICKLE_BACKEND_URL=http://localhost:4888 node -r trickle/register app.js
|
|
11
11
|
*
|
|
12
|
-
* This module patches Node's module loader to intercept
|
|
13
|
-
* and automatically instrument any
|
|
12
|
+
* This module patches Node's module loader to intercept framework requires
|
|
13
|
+
* (Express, Fastify, Koa, Hono) and automatically instrument any app created.
|
|
14
14
|
*
|
|
15
15
|
* Supported environment variables:
|
|
16
16
|
* TRICKLE_BACKEND_URL — Backend URL (default: http://localhost:4888)
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
import Module from 'module';
|
|
23
23
|
import { configure } from './transport';
|
|
24
24
|
import { instrumentExpress } from './express';
|
|
25
|
+
import { instrumentFastify } from './fastify';
|
|
26
|
+
import { instrumentKoa } from './koa';
|
|
27
|
+
import { instrumentHono } from './hono';
|
|
25
28
|
import { detectEnvironment } from './env-detect';
|
|
26
29
|
|
|
27
30
|
const M = Module as any;
|
|
@@ -33,6 +36,7 @@ const backendUrl = process.env.TRICKLE_BACKEND_URL || 'http://localhost:4888';
|
|
|
33
36
|
const enabled = process.env.TRICKLE_ENABLED !== '0' && process.env.TRICKLE_ENABLED !== 'false';
|
|
34
37
|
const debug = process.env.TRICKLE_DEBUG === '1' || process.env.TRICKLE_DEBUG === 'true';
|
|
35
38
|
const envOverride = process.env.TRICKLE_ENV || undefined;
|
|
39
|
+
const environment = envOverride || detectEnvironment();
|
|
36
40
|
|
|
37
41
|
if (enabled) {
|
|
38
42
|
// Configure the transport
|
|
@@ -41,7 +45,7 @@ if (enabled) {
|
|
|
41
45
|
batchIntervalMs: 2000,
|
|
42
46
|
debug,
|
|
43
47
|
enabled: true,
|
|
44
|
-
environment
|
|
48
|
+
environment,
|
|
45
49
|
});
|
|
46
50
|
|
|
47
51
|
if (debug) {
|
|
@@ -58,6 +62,24 @@ if (enabled) {
|
|
|
58
62
|
return patchExpress(exports, request, parent);
|
|
59
63
|
}
|
|
60
64
|
|
|
65
|
+
// Intercept require('fastify')
|
|
66
|
+
if (request === 'fastify' && !patched.has('fastify')) {
|
|
67
|
+
patched.add('fastify');
|
|
68
|
+
return patchFastify(exports, request, parent);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Intercept require('koa')
|
|
72
|
+
if (request === 'koa' && !patched.has('koa')) {
|
|
73
|
+
patched.add('koa');
|
|
74
|
+
return patchKoa(exports, request, parent);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Intercept require('hono')
|
|
78
|
+
if (request === 'hono' && !patched.has('hono')) {
|
|
79
|
+
patched.add('hono');
|
|
80
|
+
return patchHono(exports, request, parent);
|
|
81
|
+
}
|
|
82
|
+
|
|
61
83
|
return exports;
|
|
62
84
|
};
|
|
63
85
|
} else if (debug) {
|
|
@@ -66,15 +88,12 @@ if (enabled) {
|
|
|
66
88
|
|
|
67
89
|
/**
|
|
68
90
|
* Wrap the Express factory function so every app created is auto-instrumented.
|
|
69
|
-
* Preserves all static properties (express.json, express.static, etc.).
|
|
70
91
|
*/
|
|
71
92
|
function patchExpress(originalExpress: any, request: string, parent: any): any {
|
|
72
93
|
function wrappedExpress(this: any, ...args: any[]): any {
|
|
73
94
|
const app = originalExpress.apply(this, args);
|
|
74
95
|
try {
|
|
75
|
-
instrumentExpress(app, {
|
|
76
|
-
environment: envOverride || detectEnvironment(),
|
|
77
|
-
});
|
|
96
|
+
instrumentExpress(app, { environment });
|
|
78
97
|
if (debug) {
|
|
79
98
|
console.log('[trickle] Auto-instrumented Express app');
|
|
80
99
|
}
|
|
@@ -87,24 +106,120 @@ function patchExpress(originalExpress: any, request: string, parent: any): any {
|
|
|
87
106
|
return app;
|
|
88
107
|
}
|
|
89
108
|
|
|
90
|
-
// Copy all static properties (express.json, express.static, express.Router, etc.)
|
|
91
109
|
for (const key of Object.keys(originalExpress)) {
|
|
92
110
|
(wrappedExpress as any)[key] = originalExpress[key];
|
|
93
111
|
}
|
|
94
|
-
|
|
95
|
-
// Preserve prototype chain
|
|
96
112
|
Object.setPrototypeOf(wrappedExpress, Object.getPrototypeOf(originalExpress));
|
|
97
113
|
|
|
98
|
-
// Update require cache so subsequent require('express') returns the patched version
|
|
99
114
|
try {
|
|
100
115
|
const resolvedPath = M._resolveFilename(request, parent);
|
|
101
116
|
if (require.cache[resolvedPath]) {
|
|
102
117
|
require.cache[resolvedPath]!.exports = wrappedExpress;
|
|
103
118
|
}
|
|
104
|
-
} catch {
|
|
105
|
-
// Cache update failed — first require still returns patched, but subsequent
|
|
106
|
-
// requires from other modules may get the original. This is rare.
|
|
107
|
-
}
|
|
119
|
+
} catch {}
|
|
108
120
|
|
|
109
121
|
return wrappedExpress;
|
|
110
122
|
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Wrap the Fastify factory function.
|
|
126
|
+
*/
|
|
127
|
+
function patchFastify(origExports: any, request: string, parent: any): any {
|
|
128
|
+
const factory = typeof origExports === 'function' ? origExports : origExports.default || origExports.fastify;
|
|
129
|
+
if (typeof factory !== 'function') return origExports;
|
|
130
|
+
|
|
131
|
+
const wrappedFactory = function (this: any, ...args: any[]): any {
|
|
132
|
+
const app = factory.apply(this, args);
|
|
133
|
+
try {
|
|
134
|
+
instrumentFastify(app, { environment });
|
|
135
|
+
if (debug) console.log('[trickle] Auto-instrumented Fastify app');
|
|
136
|
+
} catch (err: unknown) {
|
|
137
|
+
if (debug) console.warn(`[trickle] Failed to auto-instrument Fastify: ${(err as Error).message}`);
|
|
138
|
+
}
|
|
139
|
+
return app;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
for (const key of Object.keys(factory)) {
|
|
143
|
+
(wrappedFactory as any)[key] = (factory as any)[key];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (typeof origExports === 'function') {
|
|
147
|
+
try {
|
|
148
|
+
const resolvedPath = M._resolveFilename(request, parent);
|
|
149
|
+
if (require.cache[resolvedPath]) require.cache[resolvedPath]!.exports = wrappedFactory;
|
|
150
|
+
} catch {}
|
|
151
|
+
return wrappedFactory;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const wrapped = { ...origExports };
|
|
155
|
+
if (origExports.default) wrapped.default = wrappedFactory;
|
|
156
|
+
if (origExports.fastify) wrapped.fastify = wrappedFactory;
|
|
157
|
+
return wrapped;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Wrap the Koa constructor.
|
|
162
|
+
*/
|
|
163
|
+
function patchKoa(origExports: any, request: string, parent: any): any {
|
|
164
|
+
const KoaClass = typeof origExports === 'function' ? origExports : origExports.default;
|
|
165
|
+
if (typeof KoaClass !== 'function') return origExports;
|
|
166
|
+
|
|
167
|
+
const WrappedKoa = function (this: any, ...args: any[]): any {
|
|
168
|
+
const app = new KoaClass(...args);
|
|
169
|
+
try {
|
|
170
|
+
instrumentKoa(app, { environment });
|
|
171
|
+
if (debug) console.log('[trickle] Auto-instrumented Koa app');
|
|
172
|
+
} catch (err: unknown) {
|
|
173
|
+
if (debug) console.warn(`[trickle] Failed to auto-instrument Koa: ${(err as Error).message}`);
|
|
174
|
+
}
|
|
175
|
+
return app;
|
|
176
|
+
};
|
|
177
|
+
WrappedKoa.prototype = KoaClass.prototype;
|
|
178
|
+
for (const key of Object.keys(KoaClass)) {
|
|
179
|
+
(WrappedKoa as any)[key] = (KoaClass as any)[key];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (typeof origExports === 'function') {
|
|
183
|
+
try {
|
|
184
|
+
const resolvedPath = M._resolveFilename(request, parent);
|
|
185
|
+
if (require.cache[resolvedPath]) require.cache[resolvedPath]!.exports = WrappedKoa;
|
|
186
|
+
} catch {}
|
|
187
|
+
return WrappedKoa;
|
|
188
|
+
}
|
|
189
|
+
return { ...origExports, default: WrappedKoa };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Wrap the Hono constructor.
|
|
194
|
+
*/
|
|
195
|
+
function patchHono(origExports: any, request: string, parent: any): any {
|
|
196
|
+
const HonoClass = origExports.Hono || (origExports.default && origExports.default.Hono);
|
|
197
|
+
if (typeof HonoClass !== 'function') return origExports;
|
|
198
|
+
|
|
199
|
+
const WrappedHono = function (this: any, ...args: any[]): any {
|
|
200
|
+
const app = new HonoClass(...args);
|
|
201
|
+
try {
|
|
202
|
+
instrumentHono(app, { environment });
|
|
203
|
+
if (debug) console.log('[trickle] Auto-instrumented Hono app');
|
|
204
|
+
} catch (err: unknown) {
|
|
205
|
+
if (debug) console.warn(`[trickle] Failed to auto-instrument Hono: ${(err as Error).message}`);
|
|
206
|
+
}
|
|
207
|
+
return app;
|
|
208
|
+
};
|
|
209
|
+
WrappedHono.prototype = HonoClass.prototype;
|
|
210
|
+
for (const key of Object.keys(HonoClass)) {
|
|
211
|
+
(WrappedHono as any)[key] = (HonoClass as any)[key];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const result = { ...origExports, Hono: WrappedHono };
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const resolvedPath = M._resolveFilename(request, parent);
|
|
218
|
+
if (require.cache[resolvedPath]) {
|
|
219
|
+
const cached = require.cache[resolvedPath]!.exports;
|
|
220
|
+
if (cached.Hono) cached.Hono = WrappedHono;
|
|
221
|
+
}
|
|
222
|
+
} catch {}
|
|
223
|
+
|
|
224
|
+
return result;
|
|
225
|
+
}
|