trickle-observe 0.2.129 → 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.
@@ -648,24 +648,106 @@ function transformSource(source, url, originalSource) {
648
648
  }
649
649
  }
650
650
 
651
- // If nothing to wrap or trace, return original
652
- if (exportedFunctions.length === 0 && exportedDefaults.length === 0 && namedExports.length === 0 && !hasVarTracing) {
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
- console.log(`[trickle/esm] Transformed ${fnCount} exports, ${varCount} vars from ${moduleName}`);
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.129",
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",