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,217 @@
1
+ "use strict";
2
+ /**
3
+ * Fetch observer — patches global.fetch to automatically capture
4
+ * request/response types from HTTP calls made by user code.
5
+ *
6
+ * When your app does:
7
+ * const data = await fetch('https://api.example.com/users').then(r => r.json());
8
+ *
9
+ * Trickle captures:
10
+ * - Function name: "GET /users" (method + path)
11
+ * - Module: "api.example.com" (hostname)
12
+ * - Input type: request body (for POST/PUT/PATCH)
13
+ * - Return type: inferred from JSON response
14
+ * - Sample data: actual response payload
15
+ *
16
+ * The observer only intercepts when .json() is called, so:
17
+ * - Non-JSON responses (HTML, binary) are ignored
18
+ * - The original response is never modified
19
+ * - No extra network requests are made
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.patchFetch = patchFetch;
23
+ const type_inference_1 = require("./type-inference");
24
+ const type_hash_1 = require("./type-hash");
25
+ const transport_1 = require("./transport");
26
+ // Track which type hashes we've already sent to avoid duplicates
27
+ const sentHashes = new Set();
28
+ /**
29
+ * Patch global.fetch to observe JSON responses.
30
+ * Safe to call multiple times (idempotent).
31
+ */
32
+ function patchFetch(environment, debugMode) {
33
+ if (typeof globalThis.fetch !== 'function')
34
+ return;
35
+ const originalFetch = globalThis.fetch;
36
+ // Guard against double-patching
37
+ if (originalFetch.__trickle_patched)
38
+ return;
39
+ globalThis.fetch = async function trickleObservedFetch(input, init) {
40
+ // Extract URL and method before the request
41
+ let url;
42
+ let method;
43
+ if (typeof input === 'string') {
44
+ url = input;
45
+ method = init?.method?.toUpperCase() || 'GET';
46
+ }
47
+ else if (input instanceof URL) {
48
+ url = input.href;
49
+ method = init?.method?.toUpperCase() || 'GET';
50
+ }
51
+ else if (typeof input === 'object' && input !== null && 'url' in input) {
52
+ // Request object
53
+ url = input.url;
54
+ method = (init?.method || input.method || 'GET').toUpperCase();
55
+ }
56
+ else {
57
+ url = String(input);
58
+ method = init?.method?.toUpperCase() || 'GET';
59
+ }
60
+ // Skip trickle's own backend calls
61
+ if (url.includes('/api/ingest') || url.includes('/api/functions') || url.includes('/api/health')) {
62
+ return originalFetch.call(globalThis, input, init);
63
+ }
64
+ // Make the actual request
65
+ const response = await originalFetch.call(globalThis, input, init);
66
+ // Only observe JSON responses
67
+ const contentType = response.headers.get('content-type') || '';
68
+ if (!contentType.includes('json')) {
69
+ return response;
70
+ }
71
+ // Clone and intercept: read the clone's JSON in background
72
+ try {
73
+ const cloned = response.clone();
74
+ cloned.json().then((data) => {
75
+ try {
76
+ captureHttpResponse(method, url, init?.body, data, environment, debugMode);
77
+ }
78
+ catch {
79
+ // Never interfere
80
+ }
81
+ }).catch(() => { });
82
+ }
83
+ catch {
84
+ // Clone/read failed — ignore
85
+ }
86
+ return response;
87
+ };
88
+ // Mark as patched
89
+ globalThis.fetch.__trickle_patched = true;
90
+ }
91
+ /**
92
+ * Parse a URL into a clean function name and module name.
93
+ * "https://api.example.com/v1/users?limit=10"
94
+ * → functionName: "GET /v1/users", module: "api.example.com"
95
+ */
96
+ function parseUrl(method, rawUrl) {
97
+ try {
98
+ const parsed = new URL(rawUrl);
99
+ const pathname = parsed.pathname || '/';
100
+ return {
101
+ functionName: `${method} ${pathname}`,
102
+ module: parsed.hostname || 'http',
103
+ };
104
+ }
105
+ catch {
106
+ // Relative URL or invalid — use as-is
107
+ return {
108
+ functionName: `${method} ${rawUrl}`,
109
+ module: 'http',
110
+ };
111
+ }
112
+ }
113
+ /**
114
+ * Capture the HTTP response type and enqueue it to the backend.
115
+ */
116
+ function captureHttpResponse(method, url, requestBody, responseData, environment, debugMode) {
117
+ const { functionName, module: moduleName } = parseUrl(method, url);
118
+ // Infer types
119
+ const returnType = (0, type_inference_1.inferType)(responseData, 5);
120
+ // Infer request body type (for POST/PUT/PATCH)
121
+ let argsType;
122
+ if (requestBody && typeof requestBody === 'string') {
123
+ try {
124
+ const parsed = JSON.parse(requestBody);
125
+ argsType = { kind: 'tuple', elements: [(0, type_inference_1.inferType)(parsed, 5)] };
126
+ }
127
+ catch {
128
+ argsType = { kind: 'tuple', elements: [] };
129
+ }
130
+ }
131
+ else {
132
+ argsType = { kind: 'tuple', elements: [] };
133
+ }
134
+ const hash = (0, type_hash_1.hashType)(argsType, returnType);
135
+ // Dedup — only send each unique type shape once
136
+ const key = `${functionName}::${hash}`;
137
+ if (sentHashes.has(key))
138
+ return;
139
+ sentHashes.add(key);
140
+ // Build sample input from request body
141
+ let sampleInput = undefined;
142
+ if (requestBody && typeof requestBody === 'string') {
143
+ try {
144
+ sampleInput = JSON.parse(requestBody);
145
+ }
146
+ catch {
147
+ sampleInput = undefined;
148
+ }
149
+ }
150
+ const payload = {
151
+ functionName,
152
+ module: moduleName,
153
+ language: 'js',
154
+ environment,
155
+ typeHash: hash,
156
+ argsType,
157
+ returnType,
158
+ sampleInput: sampleInput ? [sampleInput] : undefined,
159
+ sampleOutput: sanitizeSample(responseData),
160
+ };
161
+ (0, transport_1.enqueue)(payload);
162
+ if (debugMode) {
163
+ console.log(`[trickle/fetch] Captured ${functionName} → ${describeType(returnType)}`);
164
+ }
165
+ }
166
+ /**
167
+ * Brief description of a type for debug logging.
168
+ */
169
+ function describeType(type) {
170
+ if (type.kind === 'object') {
171
+ const props = Object.keys(type.properties || {});
172
+ if (props.length <= 4)
173
+ return `{ ${props.join(', ')} }`;
174
+ return `{ ${props.slice(0, 3).join(', ')}, ... } (${props.length} props)`;
175
+ }
176
+ if (type.kind === 'array')
177
+ return `${describeType(type.element)}[]`;
178
+ if (type.kind === 'primitive')
179
+ return type.name;
180
+ return type.kind;
181
+ }
182
+ /**
183
+ * Sanitize sample data for storage (truncate large values).
184
+ */
185
+ function sanitizeSample(value, depth = 3) {
186
+ if (depth <= 0)
187
+ return '[truncated]';
188
+ if (value === null || value === undefined)
189
+ return value;
190
+ const t = typeof value;
191
+ if (t === 'string') {
192
+ const s = value;
193
+ return s.length > 200 ? s.substring(0, 200) + '...' : s;
194
+ }
195
+ if (t === 'number' || t === 'boolean')
196
+ return value;
197
+ if (t === 'function')
198
+ return '[Function]';
199
+ if (Array.isArray(value)) {
200
+ return value.slice(0, 5).map(item => sanitizeSample(item, depth - 1));
201
+ }
202
+ if (t === 'object') {
203
+ const obj = value;
204
+ const result = {};
205
+ const keys = Object.keys(obj).slice(0, 20);
206
+ for (const key of keys) {
207
+ try {
208
+ result[key] = sanitizeSample(obj[key], depth - 1);
209
+ }
210
+ catch {
211
+ result[key] = '[unreadable]';
212
+ }
213
+ }
214
+ return result;
215
+ }
216
+ return String(value);
217
+ }
@@ -0,0 +1,64 @@
1
+ import { GlobalOpts, TrickleOpts } from './types';
2
+ /**
3
+ * Configure trickle global options.
4
+ * Call this before wrapping any functions if you need non-default settings.
5
+ */
6
+ export declare function configure(opts: Partial<GlobalOpts>): void;
7
+ /**
8
+ * Wrap a function to capture runtime type information.
9
+ *
10
+ * Usage:
11
+ * const wrapped = trickle(myFunction);
12
+ * const wrapped = trickle(myFunction, { name: 'myFn', module: 'api' });
13
+ * const wrapped = trickle('myFunction', myFunction);
14
+ * const wrapped = trickle('myFunction', myFunction, { module: 'api' });
15
+ */
16
+ export declare function trickle<T extends (...args: any[]) => any>(fn: T, opts?: TrickleOpts): T;
17
+ export declare function trickle<T extends (...args: any[]) => any>(name: string, fn: T, opts?: TrickleOpts): T;
18
+ /**
19
+ * Wrap a Lambda handler function.
20
+ * Same as trickle() but automatically flushes the transport after each invocation,
21
+ * since Lambda may freeze the process between invocations.
22
+ */
23
+ export declare function trickleHandler<T extends (...args: any[]) => any>(handler: T, opts?: TrickleOpts): T;
24
+ /**
25
+ * Instrument an Express app by monkey-patching route methods to capture types.
26
+ *
27
+ * Must be called BEFORE defining routes:
28
+ *
29
+ * const app = express();
30
+ * trickleExpress(app);
31
+ * app.get('/api/users', (req, res) => { ... });
32
+ *
33
+ * Each handler is wrapped to capture:
34
+ * - Input: `{ body, params, query }` from the request
35
+ * - Output: data passed to `res.json()` or `res.send()`
36
+ * - Errors: thrown exceptions or `next(err)` calls
37
+ */
38
+ export declare function trickleExpress(app: any, opts?: {
39
+ enabled?: boolean;
40
+ environment?: string;
41
+ sampleRate?: number;
42
+ maxDepth?: number;
43
+ }): void;
44
+ /**
45
+ * Auto-instrument a framework app. Currently supports Express.
46
+ *
47
+ * Usage:
48
+ * import { instrument } from 'trickle';
49
+ * const app = express();
50
+ * instrument(app);
51
+ *
52
+ * Detects Express by checking for `app.listen` and `app.get` (function) on the object.
53
+ */
54
+ export declare function instrument(app: any, opts?: {
55
+ enabled?: boolean;
56
+ environment?: string;
57
+ sampleRate?: number;
58
+ maxDepth?: number;
59
+ }): void;
60
+ export type { TypeNode, GlobalOpts, TrickleOpts, IngestPayload } from './types';
61
+ export { flush } from './transport';
62
+ export { instrumentExpress, trickleMiddleware } from './express';
63
+ export { observe, observeFn } from './observe';
64
+ export type { ObserveOpts } from './observe';
package/dist/index.js ADDED
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.observeFn = exports.observe = exports.trickleMiddleware = exports.instrumentExpress = exports.flush = void 0;
4
+ exports.configure = configure;
5
+ exports.trickle = trickle;
6
+ exports.trickleHandler = trickleHandler;
7
+ exports.trickleExpress = trickleExpress;
8
+ exports.instrument = instrument;
9
+ const transport_1 = require("./transport");
10
+ const wrap_1 = require("./wrap");
11
+ const env_detect_1 = require("./env-detect");
12
+ const express_1 = require("./express");
13
+ let globalOpts = {
14
+ backendUrl: 'http://localhost:4888',
15
+ batchIntervalMs: 2000,
16
+ enabled: true,
17
+ environment: undefined,
18
+ };
19
+ /**
20
+ * Configure trickle global options.
21
+ * Call this before wrapping any functions if you need non-default settings.
22
+ */
23
+ function configure(opts) {
24
+ Object.assign(globalOpts, opts);
25
+ (0, transport_1.configure)(globalOpts);
26
+ }
27
+ function trickle(...args) {
28
+ let fn;
29
+ let opts = {};
30
+ let explicitName;
31
+ if (typeof args[0] === 'string') {
32
+ explicitName = args[0];
33
+ fn = args[1];
34
+ opts = args[2] || {};
35
+ }
36
+ else {
37
+ fn = args[0];
38
+ opts = args[1] || {};
39
+ }
40
+ if (typeof fn !== 'function') {
41
+ throw new TypeError('trickle: expected a function argument');
42
+ }
43
+ const functionName = explicitName || opts.name || fn.name || 'anonymous';
44
+ const module = opts.module || inferModule();
45
+ const environment = globalOpts.environment || (0, env_detect_1.detectEnvironment)();
46
+ const wrapOpts = {
47
+ functionName,
48
+ module,
49
+ trackArgs: opts.trackArgs !== false,
50
+ trackReturn: opts.trackReturn !== false,
51
+ sampleRate: opts.sampleRate ?? 1,
52
+ maxDepth: opts.maxDepth ?? 5,
53
+ environment,
54
+ enabled: globalOpts.enabled,
55
+ };
56
+ return (0, wrap_1.wrapFunction)(fn, wrapOpts);
57
+ }
58
+ /**
59
+ * Wrap a Lambda handler function.
60
+ * Same as trickle() but automatically flushes the transport after each invocation,
61
+ * since Lambda may freeze the process between invocations.
62
+ */
63
+ function trickleHandler(handler, opts) {
64
+ const wrapped = trickle(handler, {
65
+ ...opts,
66
+ name: opts?.name || handler.name || 'handler',
67
+ });
68
+ const flushing = function (...args) {
69
+ const result = wrapped.apply(this, args);
70
+ // If the handler returns a promise, flush after it resolves
71
+ if (result !== null && result !== undefined && typeof result === 'object' && typeof result.then === 'function') {
72
+ return result.then(async (resolved) => {
73
+ await (0, transport_1.flush)().catch(() => { });
74
+ return resolved;
75
+ }, async (err) => {
76
+ await (0, transport_1.flush)().catch(() => { });
77
+ throw err;
78
+ });
79
+ }
80
+ // Synchronous handler — flush and return
81
+ (0, transport_1.flush)().catch(() => { });
82
+ return result;
83
+ };
84
+ Object.defineProperty(flushing, 'name', { value: handler.name || 'handler', configurable: true });
85
+ Object.defineProperty(flushing, 'length', { value: handler.length, configurable: true });
86
+ return flushing;
87
+ }
88
+ /**
89
+ * Instrument an Express app by monkey-patching route methods to capture types.
90
+ *
91
+ * Must be called BEFORE defining routes:
92
+ *
93
+ * const app = express();
94
+ * trickleExpress(app);
95
+ * app.get('/api/users', (req, res) => { ... });
96
+ *
97
+ * Each handler is wrapped to capture:
98
+ * - Input: `{ body, params, query }` from the request
99
+ * - Output: data passed to `res.json()` or `res.send()`
100
+ * - Errors: thrown exceptions or `next(err)` calls
101
+ */
102
+ function trickleExpress(app, opts) {
103
+ (0, express_1.instrumentExpress)(app, {
104
+ enabled: opts?.enabled ?? globalOpts.enabled,
105
+ environment: opts?.environment ?? globalOpts.environment ?? (0, env_detect_1.detectEnvironment)(),
106
+ sampleRate: opts?.sampleRate ?? 1,
107
+ maxDepth: opts?.maxDepth ?? 5,
108
+ });
109
+ }
110
+ /**
111
+ * Auto-instrument a framework app. Currently supports Express.
112
+ *
113
+ * Usage:
114
+ * import { instrument } from 'trickle';
115
+ * const app = express();
116
+ * instrument(app);
117
+ *
118
+ * Detects Express by checking for `app.listen` and `app.get` (function) on the object.
119
+ */
120
+ function instrument(app, opts) {
121
+ // Detect Express-like app
122
+ if (app && typeof app.listen === 'function' && typeof app.get === 'function' && typeof app.use === 'function') {
123
+ trickleExpress(app, opts);
124
+ return;
125
+ }
126
+ // Future: detect other frameworks here (Koa, Fastify, etc.)
127
+ if (typeof console !== 'undefined' && console.warn) {
128
+ console.warn('[trickle] instrument(): could not detect a supported framework on the provided object');
129
+ }
130
+ }
131
+ /**
132
+ * Attempt to infer the module name from the call stack.
133
+ * Falls back to 'unknown' if we can't determine it.
134
+ */
135
+ function inferModule() {
136
+ try {
137
+ const stack = new Error().stack;
138
+ if (!stack)
139
+ return 'unknown';
140
+ const lines = stack.split('\n');
141
+ // Skip first 3 lines: "Error", trickle internals
142
+ for (let i = 3; i < lines.length; i++) {
143
+ const line = lines[i].trim();
144
+ // Look for a file path
145
+ const match = line.match(/(?:at\s+)?(?:.*?\s+\()?(.+?)(?::\d+:\d+)?\)?$/);
146
+ if (match) {
147
+ let filePath = match[1];
148
+ // Strip node_modules paths
149
+ if (filePath.includes('node_modules'))
150
+ continue;
151
+ // Extract just the filename or relative path
152
+ const parts = filePath.split('/');
153
+ const filename = parts[parts.length - 1];
154
+ if (filename && !filename.startsWith('<')) {
155
+ return filename.replace(/\.[jt]sx?$/, '');
156
+ }
157
+ }
158
+ }
159
+ }
160
+ catch {
161
+ // Don't crash on stack inspection failure
162
+ }
163
+ return 'unknown';
164
+ }
165
+ var transport_2 = require("./transport");
166
+ Object.defineProperty(exports, "flush", { enumerable: true, get: function () { return transport_2.flush; } });
167
+ var express_2 = require("./express");
168
+ Object.defineProperty(exports, "instrumentExpress", { enumerable: true, get: function () { return express_2.instrumentExpress; } });
169
+ Object.defineProperty(exports, "trickleMiddleware", { enumerable: true, get: function () { return express_2.trickleMiddleware; } });
170
+ var observe_1 = require("./observe");
171
+ Object.defineProperty(exports, "observe", { enumerable: true, get: function () { return observe_1.observe; } });
172
+ Object.defineProperty(exports, "observeFn", { enumerable: true, get: function () { return observe_1.observeFn; } });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Auto-observation register — patches Node's module loader to automatically
3
+ * wrap ALL functions in user code (not just exports).
4
+ *
5
+ * Uses two complementary hooks:
6
+ * 1. Module._compile: Transforms source code to wrap function declarations
7
+ * IMMEDIATELY after each function body. This catches:
8
+ * - Entry file functions (previously invisible)
9
+ * - Non-exported helper functions inside modules
10
+ * - All function declarations in user code
11
+ * 2. Module._load: Wraps exported functions on the exports object
12
+ * (covers module.exports patterns like { foo, bar })
13
+ *
14
+ * A double-wrap guard in wrapFunction prevents the same function being
15
+ * wrapped by both hooks.
16
+ *
17
+ * Usage:
18
+ *
19
+ * node -r trickle/observe app.js
20
+ *
21
+ * Environment variables:
22
+ * TRICKLE_BACKEND_URL — Backend URL (default: http://localhost:4888)
23
+ * TRICKLE_ENABLED — Set to "0" or "false" to disable
24
+ * TRICKLE_DEBUG — Set to "1" for debug logging
25
+ * TRICKLE_ENV — Override detected environment
26
+ * TRICKLE_OBSERVE_INCLUDE — Comma-separated substrings to include (default: all user code)
27
+ * TRICKLE_OBSERVE_EXCLUDE — Comma-separated substrings to exclude (default: none)
28
+ */
29
+ export {};