trickle-observe 0.2.2 → 0.2.3

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.
@@ -37,6 +37,8 @@ const transport_1 = require("./transport");
37
37
  const env_detect_1 = require("./env-detect");
38
38
  const wrap_1 = require("./wrap");
39
39
  const fetch_observer_1 = require("./fetch-observer");
40
+ const express_1 = require("./express");
41
+ const trace_var_1 = require("./trace-var");
40
42
  const M = module_1.default;
41
43
  const originalLoad = M._load;
42
44
  const originalCompile = M.prototype._compile;
@@ -159,6 +161,228 @@ function findClosingBrace(source, openBrace) {
159
161
  * which is critical for entry files where functions are defined and used
160
162
  * in the same top-level scope.
161
163
  */
164
+ /**
165
+ * Find variable declarations in source and return insertions for tracing.
166
+ * Handles: const x = ...; let x = ...; var x = ...;
167
+ * Skips: destructuring, for-loop vars, require() calls, imports.
168
+ */
169
+ function findVarDeclarations(source) {
170
+ const varInsertions = [];
171
+ // Match: const/let/var <identifier> = <something>
172
+ // We look for lines containing a simple variable declaration with an assignment
173
+ const varRegex = /^([ \t]*)(const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=[^=]/gm;
174
+ let vmatch;
175
+ while ((vmatch = varRegex.exec(source)) !== null) {
176
+ const varName = vmatch[3];
177
+ // Skip common noise
178
+ if (varName === '__trickle_mod' || varName === '__trickle_wrap' || varName === '__trickle_tv')
179
+ continue;
180
+ if (varName.startsWith('__trickle'))
181
+ continue;
182
+ if (varName === '_a' || varName === '_b' || varName === '_c')
183
+ continue; // TS compiled vars
184
+ // Check if this is a require() call — skip those (they're imports, not interesting values)
185
+ const restOfLine = source.slice(vmatch.index + vmatch[0].length - 1, vmatch.index + vmatch[0].length + 200);
186
+ if (/^\s*require\s*\(/.test(restOfLine))
187
+ continue;
188
+ // Calculate line number (count newlines before this position)
189
+ let lineNo = 1;
190
+ for (let i = 0; i < vmatch.index; i++) {
191
+ if (source[i] === '\n')
192
+ lineNo++;
193
+ }
194
+ // Find the end of this statement — look for the semicolon at depth 0
195
+ // or the end of the line for semicolon-free code
196
+ const startPos = vmatch.index + vmatch[0].length - 1; // position of the '='
197
+ let pos = startPos;
198
+ let depth = 0;
199
+ let foundEnd = -1;
200
+ while (pos < source.length) {
201
+ const ch = source[pos];
202
+ if (ch === '(' || ch === '[' || ch === '{') {
203
+ depth++;
204
+ }
205
+ else if (ch === ')' || ch === ']' || ch === '}') {
206
+ depth--;
207
+ if (depth < 0)
208
+ break; // we've gone past our scope
209
+ }
210
+ else if (ch === ';' && depth === 0) {
211
+ foundEnd = pos;
212
+ break;
213
+ }
214
+ else if (ch === '\n' && depth === 0) {
215
+ // For semicolon-free code, the newline is the end
216
+ // But only if the next non-whitespace isn't a continuation (., +, etc.)
217
+ const nextNonWs = source.slice(pos + 1).match(/^\s*(\S)/);
218
+ if (nextNonWs && !'.+=-|&?:,'.includes(nextNonWs[1])) {
219
+ foundEnd = pos;
220
+ break;
221
+ }
222
+ }
223
+ else if (ch === '"' || ch === "'" || ch === '`') {
224
+ // Skip strings
225
+ const quote = ch;
226
+ pos++;
227
+ while (pos < source.length) {
228
+ if (source[pos] === '\\') {
229
+ pos++;
230
+ }
231
+ else if (source[pos] === quote)
232
+ break;
233
+ pos++;
234
+ }
235
+ }
236
+ else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '/') {
237
+ // Skip line comment
238
+ while (pos < source.length && source[pos] !== '\n')
239
+ pos++;
240
+ continue;
241
+ }
242
+ else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '*') {
243
+ // Skip block comment
244
+ pos += 2;
245
+ while (pos < source.length - 1 && !(source[pos] === '*' && source[pos + 1] === '/'))
246
+ pos++;
247
+ pos++;
248
+ }
249
+ pos++;
250
+ }
251
+ if (foundEnd === -1)
252
+ continue; // couldn't find statement end
253
+ varInsertions.push({ lineEnd: foundEnd + 1, varName, lineNo });
254
+ }
255
+ return varInsertions;
256
+ }
257
+ /**
258
+ * Find destructured variable declarations: const { a, b } = ... and const [a, b] = ...
259
+ */
260
+ function findDestructuredDeclarations(source) {
261
+ const results = [];
262
+ const destructRegex = /^[ \t]*(?:export\s+)?(?:const|let|var)\s+(\{[^}]*\}|\[[^\]]*\])\s*(?::\s*[^=]+?)?\s*=[^=]/gm;
263
+ let match;
264
+ while ((match = destructRegex.exec(source)) !== null) {
265
+ const pattern = match[1];
266
+ const varNames = extractDestructuredNames(pattern);
267
+ if (varNames.length === 0)
268
+ continue;
269
+ const restOfLine = source.slice(match.index + match[0].length - 1, match.index + match[0].length + 200);
270
+ if (/^\s*require\s*\(/.test(restOfLine))
271
+ continue;
272
+ let lineNo = 1;
273
+ for (let i = 0; i < match.index; i++) {
274
+ if (source[i] === '\n')
275
+ lineNo++;
276
+ }
277
+ const startPos = match.index + match[0].length - 1;
278
+ let pos = startPos;
279
+ let depth = 0;
280
+ let foundEnd = -1;
281
+ while (pos < source.length) {
282
+ const ch = source[pos];
283
+ if (ch === '(' || ch === '[' || ch === '{') {
284
+ depth++;
285
+ }
286
+ else if (ch === ')' || ch === ']' || ch === '}') {
287
+ depth--;
288
+ if (depth < 0)
289
+ break;
290
+ }
291
+ else if (ch === ';' && depth === 0) {
292
+ foundEnd = pos;
293
+ break;
294
+ }
295
+ else if (ch === '\n' && depth === 0) {
296
+ const nextNonWs = source.slice(pos + 1).match(/^\s*(\S)/);
297
+ if (nextNonWs && !'.+=-|&?:,'.includes(nextNonWs[1])) {
298
+ foundEnd = pos;
299
+ break;
300
+ }
301
+ }
302
+ else if (ch === '"' || ch === "'" || ch === '`') {
303
+ const quote = ch;
304
+ pos++;
305
+ while (pos < source.length) {
306
+ if (source[pos] === '\\') {
307
+ pos++;
308
+ }
309
+ else if (source[pos] === quote)
310
+ break;
311
+ pos++;
312
+ }
313
+ }
314
+ else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '/') {
315
+ while (pos < source.length && source[pos] !== '\n')
316
+ pos++;
317
+ continue;
318
+ }
319
+ else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '*') {
320
+ pos += 2;
321
+ while (pos < source.length - 1 && !(source[pos] === '*' && source[pos + 1] === '/'))
322
+ pos++;
323
+ pos++;
324
+ }
325
+ pos++;
326
+ }
327
+ if (foundEnd === -1)
328
+ continue;
329
+ results.push({ lineEnd: foundEnd + 1, varNames, lineNo });
330
+ }
331
+ return results;
332
+ }
333
+ /**
334
+ * Extract variable names from a destructuring pattern.
335
+ * { a, b, c: d } → ['a', 'b', 'd'], [a, b, ...rest] → ['a', 'b', 'rest']
336
+ */
337
+ function extractDestructuredNames(pattern) {
338
+ const names = [];
339
+ const inner = pattern.slice(1, -1).trim();
340
+ if (!inner)
341
+ return names;
342
+ const parts = [];
343
+ let depth = 0;
344
+ let current = '';
345
+ for (const ch of inner) {
346
+ if (ch === '{' || ch === '[')
347
+ depth++;
348
+ else if (ch === '}' || ch === ']')
349
+ depth--;
350
+ else if (ch === ',' && depth === 0) {
351
+ parts.push(current.trim());
352
+ current = '';
353
+ continue;
354
+ }
355
+ current += ch;
356
+ }
357
+ if (current.trim())
358
+ parts.push(current.trim());
359
+ for (const part of parts) {
360
+ if (part.startsWith('...')) {
361
+ const restName = part.slice(3).trim().split(/[\s:]/)[0];
362
+ if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(restName))
363
+ names.push(restName);
364
+ continue;
365
+ }
366
+ const colonIdx = part.indexOf(':');
367
+ if (colonIdx !== -1) {
368
+ const afterColon = part.slice(colonIdx + 1).trim();
369
+ if (afterColon.startsWith('{') || afterColon.startsWith('[')) {
370
+ names.push(...extractDestructuredNames(afterColon));
371
+ }
372
+ else {
373
+ const localName = afterColon.split(/[\s=]/)[0].trim();
374
+ if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(localName))
375
+ names.push(localName);
376
+ }
377
+ }
378
+ else {
379
+ const name = part.split(/[\s=]/)[0].trim();
380
+ if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name))
381
+ names.push(name);
382
+ }
383
+ }
384
+ return names;
385
+ }
162
386
  function transformCjsSource(source, filename, moduleName, env) {
163
387
  const funcRegex = /^[ \t]*(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/gm;
164
388
  const insertions = [];
@@ -191,12 +415,16 @@ function transformCjsSource(source, filename, moduleName, env) {
191
415
  continue;
192
416
  insertions.push({ position: closeBrace + 1, name, paramNames });
193
417
  }
194
- if (insertions.length === 0)
418
+ // Also find variable declarations for tracing
419
+ const varTraceEnabled = process.env.TRICKLE_TRACE_VARS !== '0';
420
+ const varInsertions = varTraceEnabled ? findVarDeclarations(source) : [];
421
+ const destructInsertions = varTraceEnabled ? findDestructuredDeclarations(source) : [];
422
+ if (insertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0)
195
423
  return source;
196
424
  // Resolve the path to the wrap helper (compiled JS)
197
425
  const wrapHelperPath = path_1.default.join(__dirname, 'wrap.js');
198
426
  // Prepend: load the wrapper and create the wrap helper
199
- const prefix = [
427
+ const prefixLines = [
200
428
  `var __trickle_mod = require(${JSON.stringify(wrapHelperPath)});`,
201
429
  `var __trickle_wrap = function(fn, name, paramNames) {`,
202
430
  ` var opts = {`,
@@ -205,22 +433,47 @@ function transformCjsSource(source, filename, moduleName, env) {
205
433
  ` trackArgs: true,`,
206
434
  ` trackReturn: true,`,
207
435
  ` sampleRate: 1,`,
208
- ` maxDepth: 5,`,
436
+ ` maxDepth: 3,`,
209
437
  ` environment: ${JSON.stringify(env)},`,
210
438
  ` enabled: true,`,
211
439
  ` };`,
212
440
  ` if (paramNames && paramNames.length) opts.paramNames = paramNames;`,
213
441
  ` return __trickle_mod.wrapFunction(fn, opts);`,
214
442
  `};`,
215
- '',
216
- ].join('\n');
217
- // Insert wrapper calls immediately after each function body (reverse order to preserve positions)
218
- let result = source;
219
- for (let i = insertions.length - 1; i >= 0; i--) {
220
- const { position, name, paramNames } = insertions[i];
443
+ ];
444
+ // Add variable tracing helper if we have var insertions
445
+ if (varInsertions.length > 0 || destructInsertions.length > 0) {
446
+ const traceVarPath = path_1.default.join(__dirname, 'trace-var.js');
447
+ prefixLines.push(`var __trickle_tv_mod = require(${JSON.stringify(traceVarPath)});`, `var __trickle_tv = function(v, n, l) { try { __trickle_tv_mod.traceVar(v, n, l, ${JSON.stringify(moduleName)}, ${JSON.stringify(filename)}); } catch(e){} };`);
448
+ }
449
+ prefixLines.push('');
450
+ const prefix = prefixLines.join('\n');
451
+ const allInsertions = [];
452
+ for (const { position, name, paramNames } of insertions) {
221
453
  const paramNamesArg = paramNames.length > 0 ? JSON.stringify(paramNames) : 'null';
222
- const wrapperCall = `\ntry{${name}=__trickle_wrap(${name},'${name}',${paramNamesArg})}catch(__e){}\n`;
223
- result = result.slice(0, position) + wrapperCall + result.slice(position);
454
+ allInsertions.push({
455
+ position,
456
+ code: `\ntry{${name}=__trickle_wrap(${name},'${name}',${paramNamesArg})}catch(__e){}\n`,
457
+ });
458
+ }
459
+ for (const { lineEnd, varName, lineNo } of varInsertions) {
460
+ allInsertions.push({
461
+ position: lineEnd,
462
+ code: `\ntry{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo})}catch(__e){}\n`,
463
+ });
464
+ }
465
+ for (const { lineEnd, varNames, lineNo } of destructInsertions) {
466
+ const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
467
+ allInsertions.push({
468
+ position: lineEnd,
469
+ code: `\n;try{${calls}}catch(__e){}\n`,
470
+ });
471
+ }
472
+ // Sort by position descending (insert from end to preserve earlier positions)
473
+ allInsertions.sort((a, b) => b.position - a.position);
474
+ let result = source;
475
+ for (const { position, code } of allInsertions) {
476
+ result = result.slice(0, position) + code + result.slice(position);
224
477
  }
225
478
  return prefix + result;
226
479
  }
@@ -259,8 +512,12 @@ if (enabled) {
259
512
  if (debug) {
260
513
  console.log(`[trickle/observe] Auto-observation enabled (backend: ${backendUrl})`);
261
514
  }
262
- // ── Hook 0: Patch global.fetch to capture HTTP response types ──
515
+ // ── Hook 0a: Patch global.fetch to capture HTTP response types ──
263
516
  (0, fetch_observer_1.patchFetch)(environment, debug);
517
+ // ── Hook 0b: Initialize variable tracer ──
518
+ if (process.env.TRICKLE_TRACE_VARS !== '0') {
519
+ (0, trace_var_1.initVarTracer)({ debug });
520
+ }
264
521
  // ── Hook 1: Module._compile — transform source to wrap function declarations ──
265
522
  // This catches ALL functions including entry file and non-exported helpers.
266
523
  M.prototype._compile = function hookedCompile(content, filename) {
@@ -296,13 +553,79 @@ if (enabled) {
296
553
  // Still useful for wrapping exports that use inline function expressions
297
554
  // (e.g. module.exports = { foo: function() {} }).
298
555
  // The double-wrap guard in wrapFunction prevents redundant wrapping.
556
+ // Track whether we've already patched Express to avoid double-patching
557
+ const expressPatched = new Set();
299
558
  M._load = function hookedLoad(request, parent, isMain) {
300
559
  const exports = originalLoad.apply(this, arguments);
301
- // Only process user modules (relative paths)
302
- if (!request.startsWith('.') && !request.startsWith('/')) {
303
- return exports;
560
+ // ── Express auto-detection: wrap Express factory to capture route types ──
561
+ // When someone requires 'express', wrap the factory so every app.get/post/etc
562
+ // automatically captures { body, params, query } → res.json() type data.
563
+ if (request === 'express' && !expressPatched.has('express')) {
564
+ expressPatched.add('express');
565
+ try {
566
+ const origExpress = exports;
567
+ const wrappedExpress = function (...args) {
568
+ const app = origExpress.apply(this, args);
569
+ // Tag the app so we can verify the middleware was injected
570
+ app.__trickle_instrumented = true;
571
+ try {
572
+ // Wrap route methods for future route definitions
573
+ (0, express_1.instrumentExpress)(app, { environment });
574
+ // Also inject middleware to capture routes defined BEFORE instrumentation
575
+ // (common in DI/class-based architectures where routes are defined in constructors)
576
+ if (typeof app.use === 'function') {
577
+ app.use((0, express_1.trickleMiddleware)({ environment }));
578
+ if (debug) {
579
+ console.log('[trickle/observe] Injected trickleMiddleware into Express app');
580
+ }
581
+ }
582
+ if (debug) {
583
+ console.log('[trickle/observe] Auto-instrumented Express app (route types will be captured)');
584
+ }
585
+ }
586
+ catch (e) {
587
+ if (debug) {
588
+ console.log('[trickle/observe] Express instrumentation error:', e.message);
589
+ }
590
+ }
591
+ return app;
592
+ };
593
+ // Copy all static properties (express.json, express.static, express.Router, etc.)
594
+ for (const key of Object.keys(origExpress)) {
595
+ wrappedExpress[key] = origExpress[key];
596
+ }
597
+ Object.setPrototypeOf(wrappedExpress, Object.getPrototypeOf(origExpress));
598
+ // Also wrap express.Router() to instrument route handlers on Router instances.
599
+ // Most real Express apps define routes on Routers, not directly on the app.
600
+ if (typeof origExpress.Router === 'function') {
601
+ const origRouter = origExpress.Router;
602
+ wrappedExpress.Router = function (...rArgs) {
603
+ const router = origRouter.apply(this, rArgs);
604
+ try {
605
+ (0, express_1.instrumentExpress)(router, { environment });
606
+ if (debug) {
607
+ console.log('[trickle/observe] Auto-instrumented Express Router');
608
+ }
609
+ }
610
+ catch { /* don't crash */ }
611
+ return router;
612
+ };
613
+ }
614
+ // Update require cache
615
+ try {
616
+ const resolvedPath = M._resolveFilename(request, parent);
617
+ if (require.cache[resolvedPath]) {
618
+ require.cache[resolvedPath].exports = wrappedExpress;
619
+ }
620
+ }
621
+ catch { /* non-critical */ }
622
+ return wrappedExpress;
623
+ }
624
+ catch { /* fall through to normal processing */ }
304
625
  }
305
- // Resolve to absolute path for dedup
626
+ // Resolve to absolute path for dedup — do this FIRST since bundlers like
627
+ // tsx/esbuild may use path aliases (e.g., @config/env) that don't start
628
+ // with './' or '/'. We need the resolved path to decide if it's user code.
306
629
  let resolvedPath;
307
630
  try {
308
631
  resolvedPath = M._resolveFilename(request, parent);
@@ -310,6 +633,9 @@ if (enabled) {
310
633
  catch {
311
634
  return exports;
312
635
  }
636
+ // Skip built-in modules (they resolve to names like 'fs', 'path', not absolute paths)
637
+ if (!resolvedPath.startsWith('/'))
638
+ return exports;
313
639
  // Skip node_modules and trickle internals
314
640
  if (resolvedPath.includes('node_modules'))
315
641
  return exports;
@@ -338,9 +664,26 @@ if (enabled) {
338
664
  for (const key of Object.keys(exports)) {
339
665
  if (typeof exports[key] === 'function' && key !== 'default') {
340
666
  const fn = exports[key];
341
- // Skip classes — their prototype methods are wrapped separately below
342
- const isClass = fn.prototype && fn.prototype.constructor === fn &&
343
- Object.getOwnPropertyNames(fn.prototype).some(m => m !== 'constructor' && typeof fn.prototype[m] === 'function');
667
+ // Skip classes — wrapping them breaks DI containers (tsyringe, inversify, etc.)
668
+ // and decorator metadata. Detect via toString() for ES2015+ classes,
669
+ // and via prototype check for classes with prototype methods.
670
+ // Wrapped in try-catch because some prototype property access can throw
671
+ // (e.g., Node.js stream classes with getter-only properties).
672
+ let isClass = false;
673
+ try {
674
+ const fnStr = Function.prototype.toString.call(fn);
675
+ isClass = fnStr.startsWith('class ') ||
676
+ (fn.prototype && fn.prototype.constructor === fn &&
677
+ Object.getOwnPropertyNames(fn.prototype).some(m => {
678
+ try {
679
+ return m !== 'constructor' && typeof fn.prototype[m] === 'function';
680
+ }
681
+ catch {
682
+ return false;
683
+ }
684
+ }));
685
+ }
686
+ catch { /* assume not a class */ }
344
687
  if (isClass)
345
688
  continue;
346
689
  const paramNames = extractParamNames(fn);
@@ -350,39 +693,98 @@ if (enabled) {
350
693
  trackArgs: true,
351
694
  trackReturn: true,
352
695
  sampleRate: 1,
353
- maxDepth: 5,
696
+ maxDepth: 3,
354
697
  environment,
355
698
  enabled: true,
356
699
  paramNames: paramNames.length > 0 ? paramNames : undefined,
357
700
  };
358
- exports[key] = (0, wrap_1.wrapFunction)(fn, wrapOpts);
701
+ const wrapped = (0, wrap_1.wrapFunction)(fn, wrapOpts);
702
+ try {
703
+ exports[key] = wrapped;
704
+ }
705
+ catch {
706
+ // Property might be getter-only (tsx/esbuild uses getter exports).
707
+ // Redefine with Object.defineProperty.
708
+ try {
709
+ Object.defineProperty(exports, key, {
710
+ value: wrapped,
711
+ enumerable: true,
712
+ configurable: true,
713
+ writable: true,
714
+ });
715
+ }
716
+ catch {
717
+ continue; /* truly read-only, skip */
718
+ }
719
+ }
359
720
  count++;
360
721
  }
361
722
  }
362
- // Handle default export if it's a function
723
+ // Handle default export if it's a function (but not a class)
363
724
  if (typeof exports.default === 'function') {
364
725
  const fn = exports.default;
365
- const paramNames = extractParamNames(fn);
366
- const wrapOpts = {
367
- functionName: fn.name || 'default',
368
- module: moduleName,
369
- trackArgs: true,
370
- trackReturn: true,
371
- sampleRate: 1,
372
- maxDepth: 5,
373
- environment,
374
- enabled: true,
375
- paramNames: paramNames.length > 0 ? paramNames : undefined,
376
- };
377
- exports.default = (0, wrap_1.wrapFunction)(fn, wrapOpts);
378
- count++;
726
+ let defaultIsClass = false;
727
+ try {
728
+ const defaultFnStr = Function.prototype.toString.call(fn);
729
+ defaultIsClass = defaultFnStr.startsWith('class ') ||
730
+ (fn.prototype && fn.prototype.constructor === fn &&
731
+ Object.getOwnPropertyNames(fn.prototype).some(m => {
732
+ try {
733
+ return m !== 'constructor' && typeof fn.prototype[m] === 'function';
734
+ }
735
+ catch {
736
+ return false;
737
+ }
738
+ }));
739
+ }
740
+ catch { /* assume not a class */ }
741
+ if (!defaultIsClass) {
742
+ const paramNames = extractParamNames(fn);
743
+ const wrapOpts = {
744
+ functionName: fn.name || 'default',
745
+ module: moduleName,
746
+ trackArgs: true,
747
+ trackReturn: true,
748
+ sampleRate: 1,
749
+ maxDepth: 3,
750
+ environment,
751
+ enabled: true,
752
+ paramNames: paramNames.length > 0 ? paramNames : undefined,
753
+ };
754
+ const wrapped = (0, wrap_1.wrapFunction)(fn, wrapOpts);
755
+ try {
756
+ exports.default = wrapped;
757
+ }
758
+ catch {
759
+ try {
760
+ Object.defineProperty(exports, 'default', {
761
+ value: wrapped, enumerable: true, configurable: true, writable: true,
762
+ });
763
+ }
764
+ catch { /* skip */ }
765
+ }
766
+ count++;
767
+ }
379
768
  }
380
769
  // Wrap class prototype methods for exported classes
381
770
  for (const key of Object.keys(exports)) {
382
771
  const val = exports[key];
383
772
  if (typeof val === 'function' && val.prototype && val.prototype.constructor === val) {
384
- const protoNames = Object.getOwnPropertyNames(val.prototype)
385
- .filter(m => m !== 'constructor' && typeof val.prototype[m] === 'function');
773
+ let protoNames;
774
+ try {
775
+ protoNames = Object.getOwnPropertyNames(val.prototype)
776
+ .filter(m => { try {
777
+ return m !== 'constructor' && typeof val.prototype[m] === 'function';
778
+ }
779
+ catch {
780
+ return false;
781
+ } });
782
+ }
783
+ catch {
784
+ continue;
785
+ }
786
+ // Use the class's actual name, not the export key (avoids "default.method")
787
+ const className = val.name || key;
386
788
  for (const method of protoNames) {
387
789
  if (method.startsWith('_'))
388
790
  continue;
@@ -392,12 +794,12 @@ if (enabled) {
392
794
  continue;
393
795
  const methodParamNames = extractParamNames(origMethod);
394
796
  const methodOpts = {
395
- functionName: `${key}.${method}`,
797
+ functionName: `${className}.${method}`,
396
798
  module: moduleName,
397
799
  trackArgs: true,
398
800
  trackReturn: true,
399
801
  sampleRate: 1,
400
- maxDepth: 5,
802
+ maxDepth: 3,
401
803
  environment,
402
804
  enabled: true,
403
805
  paramNames: methodParamNames.length > 0 ? methodParamNames : undefined,
@@ -415,7 +817,14 @@ if (enabled) {
415
817
  }
416
818
  else if (typeof exports === 'function') {
417
819
  // Module exports a single function (e.g. module.exports = fn)
820
+ // But skip classes — wrapping them breaks DI, decorators, and instanceof
418
821
  const fn = exports;
822
+ const singleFnStr = Function.prototype.toString.call(fn);
823
+ const singleIsClass = singleFnStr.startsWith('class ') ||
824
+ (fn.prototype && fn.prototype.constructor === fn &&
825
+ Object.getOwnPropertyNames(fn.prototype).some(m => m !== 'constructor' && typeof fn.prototype[m] === 'function'));
826
+ if (singleIsClass)
827
+ return exports;
419
828
  const fnParamNames = extractParamNames(fn);
420
829
  const wrapOpts = {
421
830
  functionName: fn.name || moduleName,
@@ -423,7 +832,7 @@ if (enabled) {
423
832
  trackArgs: true,
424
833
  trackReturn: true,
425
834
  sampleRate: 1,
426
- maxDepth: 5,
835
+ maxDepth: 3,
427
836
  environment,
428
837
  enabled: true,
429
838
  paramNames: fnParamNames.length > 0 ? fnParamNames : undefined,
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Variable-level tracing — captures the runtime type and sample value
3
+ * of variable assignments within function bodies.
4
+ *
5
+ * This is injected by the Module._compile source transform. After each
6
+ * `const/let/var x = expr;` statement, the transform inserts:
7
+ *
8
+ * __trickle_tv(x, 'x', 42, 'my-module', '/path/to/file.ts');
9
+ *
10
+ * The traceVar function:
11
+ * 1. Infers the TypeNode from the runtime value
12
+ * 2. Captures a sanitized sample value
13
+ * 3. Appends to .trickle/variables.jsonl
14
+ * 4. Caches by (file:line:varName + typeHash) to avoid duplicates
15
+ */
16
+ import { TypeNode } from './types';
17
+ export interface VariableObservation {
18
+ kind: 'variable';
19
+ varName: string;
20
+ line: number;
21
+ module: string;
22
+ file: string;
23
+ type: TypeNode;
24
+ typeHash: string;
25
+ sample: unknown;
26
+ }
27
+ /**
28
+ * Initialize the variable tracer.
29
+ * Called once during observe-register setup.
30
+ */
31
+ export declare function initVarTracer(opts?: {
32
+ debug?: boolean;
33
+ }): void;
34
+ /**
35
+ * Trace a variable's runtime value.
36
+ * Called by injected code after each variable declaration.
37
+ *
38
+ * @param value - The variable's current value (already computed)
39
+ * @param varName - The variable name in source
40
+ * @param line - Line number in source file
41
+ * @param moduleName - Module name (derived from filename)
42
+ * @param filePath - Absolute path to source file
43
+ */
44
+ export declare function traceVar(value: unknown, varName: string, line: number, moduleName: string, filePath: string): void;