trickle-observe 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.
Files changed (57) hide show
  1. package/auto-env.js +13 -0
  2. package/auto-esm.mjs +128 -0
  3. package/auto.js +3 -0
  4. package/dist/auto-codegen.d.ts +29 -0
  5. package/dist/auto-codegen.js +999 -0
  6. package/dist/auto-register.d.ts +16 -0
  7. package/dist/auto-register.js +99 -0
  8. package/dist/cache.d.ts +27 -0
  9. package/dist/cache.js +52 -0
  10. package/dist/env-detect.d.ts +5 -0
  11. package/dist/env-detect.js +35 -0
  12. package/dist/express.d.ts +44 -0
  13. package/dist/express.js +342 -0
  14. package/dist/fetch-observer.d.ts +24 -0
  15. package/dist/fetch-observer.js +217 -0
  16. package/dist/index.d.ts +64 -0
  17. package/dist/index.js +172 -0
  18. package/dist/observe-register.d.ts +29 -0
  19. package/dist/observe-register.js +455 -0
  20. package/dist/observe.d.ts +44 -0
  21. package/dist/observe.js +109 -0
  22. package/dist/proxy-tracker.d.ts +15 -0
  23. package/dist/proxy-tracker.js +172 -0
  24. package/dist/register.d.ts +21 -0
  25. package/dist/register.js +105 -0
  26. package/dist/transport.d.ts +22 -0
  27. package/dist/transport.js +228 -0
  28. package/dist/type-hash.d.ts +5 -0
  29. package/dist/type-hash.js +60 -0
  30. package/dist/type-inference.d.ts +14 -0
  31. package/dist/type-inference.js +259 -0
  32. package/dist/types.d.ts +78 -0
  33. package/dist/types.js +2 -0
  34. package/dist/wrap.d.ts +10 -0
  35. package/dist/wrap.js +247 -0
  36. package/observe-esm-hooks.mjs +367 -0
  37. package/observe-esm.mjs +40 -0
  38. package/observe.js +2 -0
  39. package/package.json +26 -0
  40. package/register.js +2 -0
  41. package/src/auto-codegen.ts +1058 -0
  42. package/src/auto-register.ts +102 -0
  43. package/src/cache.ts +53 -0
  44. package/src/env-detect.ts +22 -0
  45. package/src/express.ts +386 -0
  46. package/src/fetch-observer.ts +226 -0
  47. package/src/index.ts +199 -0
  48. package/src/observe-register.ts +453 -0
  49. package/src/observe.ts +127 -0
  50. package/src/proxy-tracker.ts +208 -0
  51. package/src/register.ts +110 -0
  52. package/src/transport.ts +207 -0
  53. package/src/type-hash.ts +71 -0
  54. package/src/type-inference.ts +285 -0
  55. package/src/types.ts +61 -0
  56. package/src/wrap.ts +289 -0
  57. package/tsconfig.json +8 -0
@@ -0,0 +1,455 @@
1
+ "use strict";
2
+ /**
3
+ * Auto-observation register — patches Node's module loader to automatically
4
+ * wrap ALL functions in user code (not just exports).
5
+ *
6
+ * Uses two complementary hooks:
7
+ * 1. Module._compile: Transforms source code to wrap function declarations
8
+ * IMMEDIATELY after each function body. This catches:
9
+ * - Entry file functions (previously invisible)
10
+ * - Non-exported helper functions inside modules
11
+ * - All function declarations in user code
12
+ * 2. Module._load: Wraps exported functions on the exports object
13
+ * (covers module.exports patterns like { foo, bar })
14
+ *
15
+ * A double-wrap guard in wrapFunction prevents the same function being
16
+ * wrapped by both hooks.
17
+ *
18
+ * Usage:
19
+ *
20
+ * node -r trickle/observe app.js
21
+ *
22
+ * Environment variables:
23
+ * TRICKLE_BACKEND_URL — Backend URL (default: http://localhost:4888)
24
+ * TRICKLE_ENABLED — Set to "0" or "false" to disable
25
+ * TRICKLE_DEBUG — Set to "1" for debug logging
26
+ * TRICKLE_ENV — Override detected environment
27
+ * TRICKLE_OBSERVE_INCLUDE — Comma-separated substrings to include (default: all user code)
28
+ * TRICKLE_OBSERVE_EXCLUDE — Comma-separated substrings to exclude (default: none)
29
+ */
30
+ var __importDefault = (this && this.__importDefault) || function (mod) {
31
+ return (mod && mod.__esModule) ? mod : { "default": mod };
32
+ };
33
+ Object.defineProperty(exports, "__esModule", { value: true });
34
+ const module_1 = __importDefault(require("module"));
35
+ const path_1 = __importDefault(require("path"));
36
+ const transport_1 = require("./transport");
37
+ const env_detect_1 = require("./env-detect");
38
+ const wrap_1 = require("./wrap");
39
+ const fetch_observer_1 = require("./fetch-observer");
40
+ const M = module_1.default;
41
+ const originalLoad = M._load;
42
+ const originalCompile = M.prototype._compile;
43
+ // Read config from environment
44
+ const backendUrl = process.env.TRICKLE_BACKEND_URL || 'http://localhost:4888';
45
+ const enabled = process.env.TRICKLE_ENABLED !== '0' && process.env.TRICKLE_ENABLED !== 'false';
46
+ const debug = process.env.TRICKLE_DEBUG === '1' || process.env.TRICKLE_DEBUG === 'true';
47
+ const envOverride = process.env.TRICKLE_ENV || undefined;
48
+ const includePatterns = process.env.TRICKLE_OBSERVE_INCLUDE
49
+ ? process.env.TRICKLE_OBSERVE_INCLUDE.split(',').map(s => s.trim())
50
+ : [];
51
+ const excludePatterns = process.env.TRICKLE_OBSERVE_EXCLUDE
52
+ ? process.env.TRICKLE_OBSERVE_EXCLUDE.split(',').map(s => s.trim())
53
+ : [];
54
+ const wrapped = new Set();
55
+ /**
56
+ * Check if a file path should be observed (user code, not node_modules/trickle internals).
57
+ */
58
+ function shouldObserve(filename) {
59
+ if (!filename || !filename.startsWith('/'))
60
+ return false;
61
+ if (filename.includes('node_modules'))
62
+ return false;
63
+ // Don't transform trickle's own code
64
+ if (filename.includes('client-js/') || filename.includes('trickle-client/') || filename.includes('trickle/dist/'))
65
+ return false;
66
+ // Apply include/exclude filters
67
+ if (includePatterns.length > 0) {
68
+ if (!includePatterns.some(p => filename.includes(p)))
69
+ return false;
70
+ }
71
+ if (excludePatterns.length > 0) {
72
+ if (excludePatterns.some(p => filename.includes(p)))
73
+ return false;
74
+ }
75
+ // Only transform JS/TS files
76
+ const ext = path_1.default.extname(filename).toLowerCase();
77
+ if (!['.js', '.jsx', '.ts', '.tsx', '.cjs', '.mjs'].includes(ext))
78
+ return false;
79
+ return true;
80
+ }
81
+ /**
82
+ * Find the closing brace position for a function body starting at `openBrace`.
83
+ * Handles nested braces, strings, template literals, and comments.
84
+ */
85
+ function findClosingBrace(source, openBrace) {
86
+ let depth = 1;
87
+ let pos = openBrace + 1;
88
+ while (pos < source.length && depth > 0) {
89
+ const ch = source[pos];
90
+ if (ch === '{') {
91
+ depth++;
92
+ }
93
+ else if (ch === '}') {
94
+ depth--;
95
+ if (depth === 0)
96
+ return pos;
97
+ }
98
+ else if (ch === '"' || ch === "'" || ch === '`') {
99
+ // Skip string/template literal
100
+ const quote = ch;
101
+ pos++;
102
+ while (pos < source.length) {
103
+ if (source[pos] === '\\') {
104
+ pos++;
105
+ } // skip escaped char
106
+ else if (source[pos] === quote)
107
+ break;
108
+ else if (quote === '`' && source[pos] === '$' && pos + 1 < source.length && source[pos + 1] === '{') {
109
+ // Template literal expression — skip nested content
110
+ pos += 2;
111
+ let tDepth = 1;
112
+ while (pos < source.length && tDepth > 0) {
113
+ if (source[pos] === '{')
114
+ tDepth++;
115
+ else if (source[pos] === '}')
116
+ tDepth--;
117
+ else if (source[pos] === '"' || source[pos] === "'" || source[pos] === '`') {
118
+ const q = source[pos];
119
+ pos++;
120
+ while (pos < source.length && source[pos] !== q) {
121
+ if (source[pos] === '\\')
122
+ pos++;
123
+ pos++;
124
+ }
125
+ }
126
+ pos++;
127
+ }
128
+ continue;
129
+ }
130
+ pos++;
131
+ }
132
+ }
133
+ else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '/') {
134
+ // Line comment — skip to end of line
135
+ while (pos < source.length && source[pos] !== '\n')
136
+ pos++;
137
+ }
138
+ else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '*') {
139
+ // Block comment — skip to */
140
+ pos += 2;
141
+ while (pos < source.length - 1 && !(source[pos] === '*' && source[pos + 1] === '/'))
142
+ pos++;
143
+ pos++; // skip past /
144
+ }
145
+ pos++;
146
+ }
147
+ return -1; // not found
148
+ }
149
+ /**
150
+ * Transform CJS source code to wrap function declarations with trickle observation.
151
+ *
152
+ * For each `function foo(...) { ... }` found, inserts a wrapper call
153
+ * IMMEDIATELY AFTER the function body closes:
154
+ *
155
+ * function foo(a) { return a; }
156
+ * foo = __trickle_wrap(foo, 'foo');
157
+ *
158
+ * This ensures functions are wrapped before subsequent code calls them,
159
+ * which is critical for entry files where functions are defined and used
160
+ * in the same top-level scope.
161
+ */
162
+ function transformCjsSource(source, filename, moduleName, env) {
163
+ const funcRegex = /^[ \t]*(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/gm;
164
+ const insertions = [];
165
+ let match;
166
+ while ((match = funcRegex.exec(source)) !== null) {
167
+ const name = match[1];
168
+ // Skip common false positives
169
+ if (name === 'require' || name === 'exports' || name === 'module')
170
+ continue;
171
+ // Find the opening brace of the function body
172
+ const afterMatch = match.index + match[0].length;
173
+ const openBrace = source.indexOf('{', afterMatch);
174
+ if (openBrace === -1)
175
+ continue;
176
+ // Extract parameter names from the source between ( and {
177
+ const paramStr = source.slice(afterMatch, openBrace).replace(/[()]/g, '').trim();
178
+ const paramNames = paramStr
179
+ ? paramStr.split(',').map(p => {
180
+ // Handle default values: "x = 5" → "x", destructuring: "{a, b}" → skip
181
+ const trimmed = p.trim().split('=')[0].trim().split(':')[0].trim();
182
+ // Skip destructuring patterns and rest params
183
+ if (trimmed.startsWith('{') || trimmed.startsWith('[') || trimmed.startsWith('...'))
184
+ return '';
185
+ return trimmed;
186
+ }).filter(Boolean)
187
+ : [];
188
+ // Find the matching closing brace
189
+ const closeBrace = findClosingBrace(source, openBrace);
190
+ if (closeBrace === -1)
191
+ continue;
192
+ insertions.push({ position: closeBrace + 1, name, paramNames });
193
+ }
194
+ if (insertions.length === 0)
195
+ return source;
196
+ // Resolve the path to the wrap helper (compiled JS)
197
+ const wrapHelperPath = path_1.default.join(__dirname, 'wrap.js');
198
+ // Prepend: load the wrapper and create the wrap helper
199
+ const prefix = [
200
+ `var __trickle_mod = require(${JSON.stringify(wrapHelperPath)});`,
201
+ `var __trickle_wrap = function(fn, name, paramNames) {`,
202
+ ` var opts = {`,
203
+ ` functionName: name,`,
204
+ ` module: ${JSON.stringify(moduleName)},`,
205
+ ` trackArgs: true,`,
206
+ ` trackReturn: true,`,
207
+ ` sampleRate: 1,`,
208
+ ` maxDepth: 5,`,
209
+ ` environment: ${JSON.stringify(env)},`,
210
+ ` enabled: true,`,
211
+ ` };`,
212
+ ` if (paramNames && paramNames.length) opts.paramNames = paramNames;`,
213
+ ` return __trickle_mod.wrapFunction(fn, opts);`,
214
+ `};`,
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];
221
+ 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);
224
+ }
225
+ return prefix + result;
226
+ }
227
+ /**
228
+ * Extract parameter names from a function using fn.toString().
229
+ */
230
+ function extractParamNames(fn) {
231
+ try {
232
+ const src = fn.toString();
233
+ const parenMatch = src.match(/^(?:async\s+)?(?:function\s*\w*|\w+)\s*\(([^)]*)\)/);
234
+ if (!parenMatch)
235
+ return [];
236
+ const paramStr = parenMatch[1].trim();
237
+ if (!paramStr)
238
+ return [];
239
+ return paramStr.split(',').map(p => {
240
+ const trimmed = p.trim().split('=')[0].trim().split(':')[0].trim();
241
+ if (trimmed.startsWith('{') || trimmed.startsWith('[') || trimmed.startsWith('...'))
242
+ return '';
243
+ return trimmed;
244
+ }).filter(Boolean);
245
+ }
246
+ catch {
247
+ return [];
248
+ }
249
+ }
250
+ if (enabled) {
251
+ const environment = envOverride || (0, env_detect_1.detectEnvironment)();
252
+ (0, transport_1.configure)({
253
+ backendUrl,
254
+ batchIntervalMs: 2000,
255
+ debug,
256
+ enabled: true,
257
+ environment,
258
+ });
259
+ if (debug) {
260
+ console.log(`[trickle/observe] Auto-observation enabled (backend: ${backendUrl})`);
261
+ }
262
+ // ── Hook 0: Patch global.fetch to capture HTTP response types ──
263
+ (0, fetch_observer_1.patchFetch)(environment, debug);
264
+ // ── Hook 1: Module._compile — transform source to wrap function declarations ──
265
+ // This catches ALL functions including entry file and non-exported helpers.
266
+ M.prototype._compile = function hookedCompile(content, filename) {
267
+ if (shouldObserve(filename)) {
268
+ const moduleName = path_1.default.basename(filename).replace(/\.[jt]sx?$/, '');
269
+ try {
270
+ const transformed = transformCjsSource(content, filename, moduleName, environment);
271
+ if (transformed !== content) {
272
+ // Count how many functions were wrapped (from insertions)
273
+ const funcRegex = /^[ \t]*(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/gm;
274
+ let count = 0;
275
+ let m;
276
+ while ((m = funcRegex.exec(content)) !== null) {
277
+ if (m[1] !== 'require' && m[1] !== 'exports' && m[1] !== 'module')
278
+ count++;
279
+ }
280
+ if (debug && count > 0) {
281
+ console.log(`[trickle/observe] Deep-wrapped ${count} functions in ${moduleName} (${filename})`);
282
+ }
283
+ return originalCompile.call(this, transformed, filename);
284
+ }
285
+ }
286
+ catch (err) {
287
+ // If transform fails, fall through to original compilation
288
+ if (debug) {
289
+ console.log(`[trickle/observe] Transform failed for ${filename}: ${err}`);
290
+ }
291
+ }
292
+ }
293
+ return originalCompile.call(this, content, filename);
294
+ };
295
+ // ── Hook 2: Module._load — wrap exports object (catches module.exports patterns) ──
296
+ // Still useful for wrapping exports that use inline function expressions
297
+ // (e.g. module.exports = { foo: function() {} }).
298
+ // The double-wrap guard in wrapFunction prevents redundant wrapping.
299
+ M._load = function hookedLoad(request, parent, isMain) {
300
+ const exports = originalLoad.apply(this, arguments);
301
+ // Only process user modules (relative paths)
302
+ if (!request.startsWith('.') && !request.startsWith('/')) {
303
+ return exports;
304
+ }
305
+ // Resolve to absolute path for dedup
306
+ let resolvedPath;
307
+ try {
308
+ resolvedPath = M._resolveFilename(request, parent);
309
+ }
310
+ catch {
311
+ return exports;
312
+ }
313
+ // Skip node_modules and trickle internals
314
+ if (resolvedPath.includes('node_modules'))
315
+ return exports;
316
+ if (resolvedPath.includes('client-js/') || resolvedPath.includes('trickle-client/') || resolvedPath.includes('trickle/dist/'))
317
+ return exports;
318
+ // Skip already-wrapped modules
319
+ if (wrapped.has(resolvedPath))
320
+ return exports;
321
+ // Apply include/exclude filters
322
+ if (includePatterns.length > 0) {
323
+ const matches = includePatterns.some(p => resolvedPath.includes(p));
324
+ if (!matches)
325
+ return exports;
326
+ }
327
+ if (excludePatterns.length > 0) {
328
+ const excluded = excludePatterns.some(p => resolvedPath.includes(p));
329
+ if (excluded)
330
+ return exports;
331
+ }
332
+ wrapped.add(resolvedPath);
333
+ // Derive module name from file path
334
+ const moduleName = path_1.default.basename(resolvedPath).replace(/\.[jt]sx?$/, '');
335
+ // Wrap exported functions
336
+ if (exports && typeof exports === 'object') {
337
+ let count = 0;
338
+ for (const key of Object.keys(exports)) {
339
+ if (typeof exports[key] === 'function' && key !== 'default') {
340
+ 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');
344
+ if (isClass)
345
+ continue;
346
+ const paramNames = extractParamNames(fn);
347
+ const wrapOpts = {
348
+ functionName: key,
349
+ module: moduleName,
350
+ trackArgs: true,
351
+ trackReturn: true,
352
+ sampleRate: 1,
353
+ maxDepth: 5,
354
+ environment,
355
+ enabled: true,
356
+ paramNames: paramNames.length > 0 ? paramNames : undefined,
357
+ };
358
+ exports[key] = (0, wrap_1.wrapFunction)(fn, wrapOpts);
359
+ count++;
360
+ }
361
+ }
362
+ // Handle default export if it's a function
363
+ if (typeof exports.default === 'function') {
364
+ 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++;
379
+ }
380
+ // Wrap class prototype methods for exported classes
381
+ for (const key of Object.keys(exports)) {
382
+ const val = exports[key];
383
+ 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');
386
+ for (const method of protoNames) {
387
+ if (method.startsWith('_'))
388
+ continue;
389
+ try {
390
+ const origMethod = val.prototype[method];
391
+ if (origMethod[Symbol.for('__trickle_wrapped')])
392
+ continue;
393
+ const methodParamNames = extractParamNames(origMethod);
394
+ const methodOpts = {
395
+ functionName: `${key}.${method}`,
396
+ module: moduleName,
397
+ trackArgs: true,
398
+ trackReturn: true,
399
+ sampleRate: 1,
400
+ maxDepth: 5,
401
+ environment,
402
+ enabled: true,
403
+ paramNames: methodParamNames.length > 0 ? methodParamNames : undefined,
404
+ };
405
+ val.prototype[method] = (0, wrap_1.wrapFunction)(origMethod, methodOpts);
406
+ count++;
407
+ }
408
+ catch { /* skip methods that can't be wrapped */ }
409
+ }
410
+ }
411
+ }
412
+ if (debug && count > 0) {
413
+ console.log(`[trickle/observe] Wrapped ${count} exports from ${moduleName} (${resolvedPath})`);
414
+ }
415
+ }
416
+ else if (typeof exports === 'function') {
417
+ // Module exports a single function (e.g. module.exports = fn)
418
+ const fn = exports;
419
+ const fnParamNames = extractParamNames(fn);
420
+ const wrapOpts = {
421
+ functionName: fn.name || moduleName,
422
+ module: moduleName,
423
+ trackArgs: true,
424
+ trackReturn: true,
425
+ sampleRate: 1,
426
+ maxDepth: 5,
427
+ environment,
428
+ enabled: true,
429
+ paramNames: fnParamNames.length > 0 ? fnParamNames : undefined,
430
+ };
431
+ const wrappedFn = (0, wrap_1.wrapFunction)(fn, wrapOpts);
432
+ // Copy static properties
433
+ for (const key of Object.keys(fn)) {
434
+ wrappedFn[key] = fn[key];
435
+ }
436
+ // Update require cache
437
+ try {
438
+ if (require.cache[resolvedPath]) {
439
+ require.cache[resolvedPath].exports = wrappedFn;
440
+ }
441
+ }
442
+ catch {
443
+ // Cache update failed — non-critical
444
+ }
445
+ if (debug) {
446
+ console.log(`[trickle/observe] Wrapped default export from ${moduleName}`);
447
+ }
448
+ return wrappedFn;
449
+ }
450
+ return exports;
451
+ };
452
+ }
453
+ else if (debug) {
454
+ console.log('[trickle/observe] Auto-observation disabled (TRICKLE_ENABLED=false)');
455
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Universal function observation — wrap any module's exports to capture
3
+ * runtime types and sample data for every function call.
4
+ *
5
+ * Usage:
6
+ *
7
+ * import { observe } from 'trickle';
8
+ * import * as helpers from './myHelpers';
9
+ *
10
+ * const { fetchUser, createOrder } = observe(helpers, { module: 'my-helpers' });
11
+ * // Every call to fetchUser / createOrder now captures types + samples
12
+ *
13
+ * Works with any object whose values are functions — module exports, plain
14
+ * objects, class instances, test helpers, SDK clients, etc.
15
+ */
16
+ export interface ObserveOpts {
17
+ /** Module name shown in `trickle functions` output. Defaults to 'observed'. */
18
+ module?: string;
19
+ /** Environment label. Auto-detected if omitted. */
20
+ environment?: string;
21
+ /** Fraction of calls to capture (0–1). Defaults to 1 (all calls). */
22
+ sampleRate?: number;
23
+ /** Max depth for type inference. Defaults to 5. */
24
+ maxDepth?: number;
25
+ /** Set to false to disable observation (passthrough). Defaults to true. */
26
+ enabled?: boolean;
27
+ }
28
+ /**
29
+ * Wrap every function property on `obj` so calls are observed by trickle.
30
+ * Non-function properties are copied through unchanged.
31
+ *
32
+ * Returns a new object with the same shape — the original is never mutated.
33
+ */
34
+ export declare function observe<T extends Record<string, any>>(obj: T, opts?: ObserveOpts): T;
35
+ /**
36
+ * Wrap a single function for observation.
37
+ * Convenience for when you don't have a module object to pass to observe().
38
+ *
39
+ * import { observeFn } from 'trickle';
40
+ * const tracedFetch = observeFn(fetchUser, { module: 'api', name: 'fetchUser' });
41
+ */
42
+ export declare function observeFn<T extends (...args: any[]) => any>(fn: T, opts?: ObserveOpts & {
43
+ name?: string;
44
+ }): T;
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ /**
3
+ * Universal function observation — wrap any module's exports to capture
4
+ * runtime types and sample data for every function call.
5
+ *
6
+ * Usage:
7
+ *
8
+ * import { observe } from 'trickle';
9
+ * import * as helpers from './myHelpers';
10
+ *
11
+ * const { fetchUser, createOrder } = observe(helpers, { module: 'my-helpers' });
12
+ * // Every call to fetchUser / createOrder now captures types + samples
13
+ *
14
+ * Works with any object whose values are functions — module exports, plain
15
+ * objects, class instances, test helpers, SDK clients, etc.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.observe = observe;
19
+ exports.observeFn = observeFn;
20
+ const wrap_1 = require("./wrap");
21
+ const env_detect_1 = require("./env-detect");
22
+ /**
23
+ * Wrap every function property on `obj` so calls are observed by trickle.
24
+ * Non-function properties are copied through unchanged.
25
+ *
26
+ * Returns a new object with the same shape — the original is never mutated.
27
+ */
28
+ function observe(obj, opts) {
29
+ if (!obj || typeof obj !== 'object')
30
+ return obj;
31
+ const moduleName = opts?.module || inferModuleName();
32
+ const environment = opts?.environment || (0, env_detect_1.detectEnvironment)();
33
+ const enabled = opts?.enabled !== false;
34
+ const sampleRate = opts?.sampleRate ?? 1;
35
+ const maxDepth = opts?.maxDepth ?? 5;
36
+ const result = {};
37
+ for (const key of Object.keys(obj)) {
38
+ const val = obj[key];
39
+ if (typeof val === 'function') {
40
+ const wrapOpts = {
41
+ functionName: key,
42
+ module: moduleName,
43
+ trackArgs: true,
44
+ trackReturn: true,
45
+ sampleRate,
46
+ maxDepth,
47
+ environment,
48
+ enabled,
49
+ };
50
+ result[key] = (0, wrap_1.wrapFunction)(val, wrapOpts);
51
+ }
52
+ else {
53
+ result[key] = val;
54
+ }
55
+ }
56
+ return result;
57
+ }
58
+ /**
59
+ * Wrap a single function for observation.
60
+ * Convenience for when you don't have a module object to pass to observe().
61
+ *
62
+ * import { observeFn } from 'trickle';
63
+ * const tracedFetch = observeFn(fetchUser, { module: 'api', name: 'fetchUser' });
64
+ */
65
+ function observeFn(fn, opts) {
66
+ const wrapOpts = {
67
+ functionName: opts?.name || fn.name || 'anonymous',
68
+ module: opts?.module || inferModuleName(),
69
+ trackArgs: true,
70
+ trackReturn: true,
71
+ sampleRate: opts?.sampleRate ?? 1,
72
+ maxDepth: opts?.maxDepth ?? 5,
73
+ environment: opts?.environment || (0, env_detect_1.detectEnvironment)(),
74
+ enabled: opts?.enabled !== false,
75
+ };
76
+ return (0, wrap_1.wrapFunction)(fn, wrapOpts);
77
+ }
78
+ /**
79
+ * Attempt to infer a module name from the call stack.
80
+ */
81
+ function inferModuleName() {
82
+ try {
83
+ const stack = new Error().stack;
84
+ if (!stack)
85
+ return 'observed';
86
+ const lines = stack.split('\n');
87
+ // Skip internal frames (Error, observe internals)
88
+ for (let i = 3; i < lines.length; i++) {
89
+ const line = lines[i].trim();
90
+ const match = line.match(/(?:at\s+)?(?:.*?\s+\()?(.+?)(?::\d+:\d+)?\)?$/);
91
+ if (match) {
92
+ const filePath = match[1];
93
+ if (filePath.includes('node_modules'))
94
+ continue;
95
+ if (filePath.includes('trickle'))
96
+ continue;
97
+ const parts = filePath.split('/');
98
+ const filename = parts[parts.length - 1];
99
+ if (filename && !filename.startsWith('<')) {
100
+ return filename.replace(/\.[jt]sx?$/, '');
101
+ }
102
+ }
103
+ }
104
+ }
105
+ catch {
106
+ // Don't crash on stack inspection failure
107
+ }
108
+ return 'observed';
109
+ }
@@ -0,0 +1,15 @@
1
+ import { TypeNode } from './types';
2
+ /**
3
+ * Creates a deep Proxy around a value to track property accesses.
4
+ * Returns the proxy and a function to retrieve all accessed paths with their inferred types.
5
+ *
6
+ * The proxy is designed to be fully transparent:
7
+ * - Array.isArray() returns true for proxied arrays
8
+ * - JSON.stringify works correctly
9
+ * - typeof, ===, iteration, spread, Object.keys all work identically
10
+ * - Symbol.toPrimitive, Symbol.iterator, Symbol.toStringTag are forwarded
11
+ */
12
+ export declare function createTracker(value: unknown): {
13
+ proxy: unknown;
14
+ getAccessedPaths: () => Map<string, TypeNode>;
15
+ };