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.
- package/auto-env.js +13 -0
- package/auto-esm.mjs +128 -0
- package/auto.js +3 -0
- package/dist/auto-codegen.d.ts +29 -0
- package/dist/auto-codegen.js +999 -0
- package/dist/auto-register.d.ts +16 -0
- package/dist/auto-register.js +99 -0
- package/dist/cache.d.ts +27 -0
- package/dist/cache.js +52 -0
- package/dist/env-detect.d.ts +5 -0
- package/dist/env-detect.js +35 -0
- package/dist/express.d.ts +44 -0
- package/dist/express.js +342 -0
- package/dist/fetch-observer.d.ts +24 -0
- package/dist/fetch-observer.js +217 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +172 -0
- package/dist/observe-register.d.ts +29 -0
- package/dist/observe-register.js +455 -0
- package/dist/observe.d.ts +44 -0
- package/dist/observe.js +109 -0
- package/dist/proxy-tracker.d.ts +15 -0
- package/dist/proxy-tracker.js +172 -0
- package/dist/register.d.ts +21 -0
- package/dist/register.js +105 -0
- package/dist/transport.d.ts +22 -0
- package/dist/transport.js +228 -0
- package/dist/type-hash.d.ts +5 -0
- package/dist/type-hash.js +60 -0
- package/dist/type-inference.d.ts +14 -0
- package/dist/type-inference.js +259 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.js +2 -0
- package/dist/wrap.d.ts +10 -0
- package/dist/wrap.js +247 -0
- package/observe-esm-hooks.mjs +367 -0
- package/observe-esm.mjs +40 -0
- package/observe.js +2 -0
- package/package.json +26 -0
- package/register.js +2 -0
- package/src/auto-codegen.ts +1058 -0
- package/src/auto-register.ts +102 -0
- package/src/cache.ts +53 -0
- package/src/env-detect.ts +22 -0
- package/src/express.ts +386 -0
- package/src/fetch-observer.ts +226 -0
- package/src/index.ts +199 -0
- package/src/observe-register.ts +453 -0
- package/src/observe.ts +127 -0
- package/src/proxy-tracker.ts +208 -0
- package/src/register.ts +110 -0
- package/src/transport.ts +207 -0
- package/src/type-hash.ts +71 -0
- package/src/type-inference.ts +285 -0
- package/src/types.ts +61 -0
- package/src/wrap.ts +289 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { TypeNode } from './types';
|
|
2
|
+
|
|
3
|
+
const MAX_ARRAY_SAMPLE = 20;
|
|
4
|
+
const DEFAULT_MAX_DEPTH = 5;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Infer the TypeNode representation of a runtime JavaScript value.
|
|
8
|
+
* Uses a WeakSet to detect circular references.
|
|
9
|
+
* Samples at most the first 20 elements of arrays for performance.
|
|
10
|
+
*/
|
|
11
|
+
export function inferType(value: unknown, maxDepth: number = DEFAULT_MAX_DEPTH): TypeNode {
|
|
12
|
+
const seen = new WeakSet();
|
|
13
|
+
return infer(value, maxDepth, seen);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function infer(value: unknown, depth: number, seen: WeakSet<object>): TypeNode {
|
|
17
|
+
// Null
|
|
18
|
+
if (value === null) {
|
|
19
|
+
return { kind: 'primitive', name: 'null' };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Undefined
|
|
23
|
+
if (value === undefined) {
|
|
24
|
+
return { kind: 'primitive', name: 'undefined' };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const t = typeof value;
|
|
28
|
+
|
|
29
|
+
// Primitives
|
|
30
|
+
if (t === 'string') return { kind: 'primitive', name: 'string' };
|
|
31
|
+
if (t === 'number') return { kind: 'primitive', name: 'number' };
|
|
32
|
+
if (t === 'boolean') return { kind: 'primitive', name: 'boolean' };
|
|
33
|
+
if (t === 'bigint') return { kind: 'primitive', name: 'bigint' };
|
|
34
|
+
if (t === 'symbol') return { kind: 'primitive', name: 'symbol' };
|
|
35
|
+
|
|
36
|
+
// Functions
|
|
37
|
+
if (t === 'function') {
|
|
38
|
+
return {
|
|
39
|
+
kind: 'function',
|
|
40
|
+
params: new Array((value as Function).length).fill({ kind: 'unknown' } as TypeNode),
|
|
41
|
+
returnType: { kind: 'unknown' },
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Object types — check depth and circular refs
|
|
46
|
+
if (t === 'object') {
|
|
47
|
+
const obj = value as object;
|
|
48
|
+
|
|
49
|
+
// Circular reference detection
|
|
50
|
+
if (seen.has(obj)) {
|
|
51
|
+
return { kind: 'unknown' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (depth <= 0) {
|
|
55
|
+
return { kind: 'unknown' };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
seen.add(obj);
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
return inferObject(obj, depth, seen);
|
|
62
|
+
} finally {
|
|
63
|
+
// Don't remove from seen — keeps circular detection intact for the full traversal
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { kind: 'unknown' };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function inferObject(obj: object, depth: number, seen: WeakSet<object>): TypeNode {
|
|
71
|
+
// Promise
|
|
72
|
+
if (obj instanceof Promise) {
|
|
73
|
+
return { kind: 'promise', resolved: { kind: 'unknown' } };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Map
|
|
77
|
+
if (obj instanceof Map) {
|
|
78
|
+
let keyType: TypeNode = { kind: 'unknown' };
|
|
79
|
+
let valType: TypeNode = { kind: 'unknown' };
|
|
80
|
+
let count = 0;
|
|
81
|
+
const keyTypes: TypeNode[] = [];
|
|
82
|
+
const valTypes: TypeNode[] = [];
|
|
83
|
+
|
|
84
|
+
for (const [k, v] of obj) {
|
|
85
|
+
if (count >= MAX_ARRAY_SAMPLE) break;
|
|
86
|
+
keyTypes.push(infer(k, depth - 1, seen));
|
|
87
|
+
valTypes.push(infer(v, depth - 1, seen));
|
|
88
|
+
count++;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
keyType = unifyTypes(keyTypes);
|
|
92
|
+
valType = unifyTypes(valTypes);
|
|
93
|
+
|
|
94
|
+
return { kind: 'map', key: keyType, value: valType };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Set
|
|
98
|
+
if (obj instanceof Set) {
|
|
99
|
+
const elementTypes: TypeNode[] = [];
|
|
100
|
+
let count = 0;
|
|
101
|
+
for (const item of obj) {
|
|
102
|
+
if (count >= MAX_ARRAY_SAMPLE) break;
|
|
103
|
+
elementTypes.push(infer(item, depth - 1, seen));
|
|
104
|
+
count++;
|
|
105
|
+
}
|
|
106
|
+
return { kind: 'set', element: unifyTypes(elementTypes) };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Array (including TypedArrays)
|
|
110
|
+
if (Array.isArray(obj)) {
|
|
111
|
+
return inferArray(obj, depth, seen);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// TypedArrays (not real arrays)
|
|
115
|
+
if (ArrayBuffer.isView(obj) && !(obj instanceof DataView)) {
|
|
116
|
+
return {
|
|
117
|
+
kind: 'object',
|
|
118
|
+
properties: {
|
|
119
|
+
__typedArray: { kind: 'primitive', name: 'string' },
|
|
120
|
+
length: { kind: 'primitive', name: 'number' },
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Buffer (Node.js)
|
|
126
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(obj)) {
|
|
127
|
+
return {
|
|
128
|
+
kind: 'object',
|
|
129
|
+
properties: {
|
|
130
|
+
__buffer: { kind: 'primitive', name: 'string' },
|
|
131
|
+
length: { kind: 'primitive', name: 'number' },
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Date
|
|
137
|
+
if (obj instanceof Date) {
|
|
138
|
+
return {
|
|
139
|
+
kind: 'object',
|
|
140
|
+
properties: {
|
|
141
|
+
__date: { kind: 'primitive', name: 'string' },
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// RegExp
|
|
147
|
+
if (obj instanceof RegExp) {
|
|
148
|
+
return {
|
|
149
|
+
kind: 'object',
|
|
150
|
+
properties: {
|
|
151
|
+
__regexp: { kind: 'primitive', name: 'string' },
|
|
152
|
+
source: { kind: 'primitive', name: 'string' },
|
|
153
|
+
flags: { kind: 'primitive', name: 'string' },
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Error
|
|
159
|
+
if (obj instanceof Error) {
|
|
160
|
+
return {
|
|
161
|
+
kind: 'object',
|
|
162
|
+
properties: {
|
|
163
|
+
__error: { kind: 'primitive', name: 'string' },
|
|
164
|
+
name: { kind: 'primitive', name: 'string' },
|
|
165
|
+
message: { kind: 'primitive', name: 'string' },
|
|
166
|
+
stack: { kind: 'primitive', name: 'string' },
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Plain objects
|
|
172
|
+
return inferPlainObject(obj, depth, seen);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function inferArray(arr: unknown[], depth: number, seen: WeakSet<object>): TypeNode {
|
|
176
|
+
if (arr.length === 0) {
|
|
177
|
+
return { kind: 'array', element: { kind: 'unknown' } };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const sampleSize = Math.min(arr.length, MAX_ARRAY_SAMPLE);
|
|
181
|
+
const elementTypes: TypeNode[] = [];
|
|
182
|
+
|
|
183
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
184
|
+
elementTypes.push(infer(arr[i], depth - 1, seen));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return { kind: 'array', element: unifyTypes(elementTypes) };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function inferPlainObject(obj: object, depth: number, seen: WeakSet<object>): TypeNode {
|
|
191
|
+
const properties: Record<string, TypeNode> = {};
|
|
192
|
+
const keys = Object.keys(obj);
|
|
193
|
+
|
|
194
|
+
for (const key of keys) {
|
|
195
|
+
try {
|
|
196
|
+
properties[key] = infer((obj as Record<string, unknown>)[key], depth - 1, seen);
|
|
197
|
+
} catch {
|
|
198
|
+
properties[key] = { kind: 'unknown' };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { kind: 'object', properties };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Unify an array of TypeNodes into a single TypeNode.
|
|
207
|
+
* If all types are structurally identical, returns that type.
|
|
208
|
+
* Otherwise, returns a union of the distinct types.
|
|
209
|
+
*/
|
|
210
|
+
function unifyTypes(types: TypeNode[]): TypeNode {
|
|
211
|
+
if (types.length === 0) return { kind: 'unknown' };
|
|
212
|
+
if (types.length === 1) return types[0];
|
|
213
|
+
|
|
214
|
+
// Deduplicate by serialization
|
|
215
|
+
const uniqueMap = new Map<string, TypeNode>();
|
|
216
|
+
for (const t of types) {
|
|
217
|
+
const key = canonicalStringify(t);
|
|
218
|
+
if (!uniqueMap.has(key)) {
|
|
219
|
+
uniqueMap.set(key, t);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const unique = Array.from(uniqueMap.values());
|
|
224
|
+
if (unique.length === 1) return unique[0];
|
|
225
|
+
|
|
226
|
+
// Flatten nested unions
|
|
227
|
+
const members: TypeNode[] = [];
|
|
228
|
+
for (const u of unique) {
|
|
229
|
+
if (u.kind === 'union') {
|
|
230
|
+
members.push(...u.members);
|
|
231
|
+
} else {
|
|
232
|
+
members.push(u);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Deduplicate again after flattening
|
|
237
|
+
const finalMap = new Map<string, TypeNode>();
|
|
238
|
+
for (const m of members) {
|
|
239
|
+
const key = canonicalStringify(m);
|
|
240
|
+
if (!finalMap.has(key)) {
|
|
241
|
+
finalMap.set(key, m);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const finalMembers = Array.from(finalMap.values());
|
|
246
|
+
if (finalMembers.length === 1) return finalMembers[0];
|
|
247
|
+
|
|
248
|
+
return { kind: 'union', members: finalMembers };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function canonicalStringify(node: TypeNode): string {
|
|
252
|
+
if (node.kind === 'object') {
|
|
253
|
+
const sorted = Object.keys(node.properties).sort();
|
|
254
|
+
const entries = sorted.map(k => `${JSON.stringify(k)}:${canonicalStringify(node.properties[k])}`);
|
|
255
|
+
return `{object:{${entries.join(',')}}}`;
|
|
256
|
+
}
|
|
257
|
+
if (node.kind === 'union') {
|
|
258
|
+
const sorted = node.members.map(canonicalStringify).sort();
|
|
259
|
+
return `{union:[${sorted.join(',')}]}`;
|
|
260
|
+
}
|
|
261
|
+
if (node.kind === 'array') {
|
|
262
|
+
return `{array:${canonicalStringify(node.element)}}`;
|
|
263
|
+
}
|
|
264
|
+
if (node.kind === 'primitive') {
|
|
265
|
+
return `{prim:${node.name}}`;
|
|
266
|
+
}
|
|
267
|
+
if (node.kind === 'function') {
|
|
268
|
+
return `{fn:[${node.params.map(canonicalStringify).join(',')}]->${canonicalStringify(node.returnType)}}`;
|
|
269
|
+
}
|
|
270
|
+
if (node.kind === 'promise') {
|
|
271
|
+
return `{promise:${canonicalStringify(node.resolved)}}`;
|
|
272
|
+
}
|
|
273
|
+
if (node.kind === 'map') {
|
|
274
|
+
return `{map:${canonicalStringify(node.key)},${canonicalStringify(node.value)}}`;
|
|
275
|
+
}
|
|
276
|
+
if (node.kind === 'set') {
|
|
277
|
+
return `{set:${canonicalStringify(node.element)}}`;
|
|
278
|
+
}
|
|
279
|
+
if (node.kind === 'tuple') {
|
|
280
|
+
return `{tuple:[${node.elements.map(canonicalStringify).join(',')}]}`;
|
|
281
|
+
}
|
|
282
|
+
return '{unknown}';
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export { unifyTypes };
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type TypeNode =
|
|
2
|
+
| { kind: "primitive"; name: "string" | "number" | "boolean" | "null" | "undefined" | "bigint" | "symbol" }
|
|
3
|
+
| { kind: "array"; element: TypeNode }
|
|
4
|
+
| { kind: "object"; properties: Record<string, TypeNode> }
|
|
5
|
+
| { kind: "union"; members: TypeNode[] }
|
|
6
|
+
| { kind: "function"; params: TypeNode[]; returnType: TypeNode }
|
|
7
|
+
| { kind: "promise"; resolved: TypeNode }
|
|
8
|
+
| { kind: "map"; key: TypeNode; value: TypeNode }
|
|
9
|
+
| { kind: "set"; element: TypeNode }
|
|
10
|
+
| { kind: "tuple"; elements: TypeNode[] }
|
|
11
|
+
| { kind: "unknown" };
|
|
12
|
+
|
|
13
|
+
export interface IngestPayload {
|
|
14
|
+
functionName: string;
|
|
15
|
+
module: string;
|
|
16
|
+
language: "js" | "python";
|
|
17
|
+
environment: string;
|
|
18
|
+
typeHash: string;
|
|
19
|
+
argsType: TypeNode;
|
|
20
|
+
returnType: TypeNode;
|
|
21
|
+
isAsync?: boolean;
|
|
22
|
+
paramNames?: string[];
|
|
23
|
+
sampleInput?: unknown;
|
|
24
|
+
sampleOutput?: unknown;
|
|
25
|
+
error?: {
|
|
26
|
+
type: string;
|
|
27
|
+
message: string;
|
|
28
|
+
stackTrace?: string;
|
|
29
|
+
argsSnapshot?: unknown;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface GlobalOpts {
|
|
34
|
+
backendUrl: string;
|
|
35
|
+
batchIntervalMs: number;
|
|
36
|
+
enabled: boolean;
|
|
37
|
+
environment: string | undefined;
|
|
38
|
+
maxBatchSize?: number;
|
|
39
|
+
debug?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface TrickleOpts {
|
|
43
|
+
name?: string;
|
|
44
|
+
module?: string;
|
|
45
|
+
trackArgs?: boolean;
|
|
46
|
+
trackReturn?: boolean;
|
|
47
|
+
sampleRate?: number;
|
|
48
|
+
maxDepth?: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface WrapOptions {
|
|
52
|
+
functionName: string;
|
|
53
|
+
module: string;
|
|
54
|
+
trackArgs: boolean;
|
|
55
|
+
trackReturn: boolean;
|
|
56
|
+
sampleRate: number;
|
|
57
|
+
maxDepth: number;
|
|
58
|
+
environment: string;
|
|
59
|
+
enabled: boolean;
|
|
60
|
+
paramNames?: string[];
|
|
61
|
+
}
|
package/src/wrap.ts
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { TypeNode, IngestPayload, WrapOptions } from './types';
|
|
2
|
+
import { inferType } from './type-inference';
|
|
3
|
+
import { hashType } from './type-hash';
|
|
4
|
+
import { createTracker } from './proxy-tracker';
|
|
5
|
+
import { TypeCache } from './cache';
|
|
6
|
+
import { enqueue } from './transport';
|
|
7
|
+
|
|
8
|
+
const typeCache = new TypeCache();
|
|
9
|
+
|
|
10
|
+
/** Symbol to mark already-wrapped functions, preventing double-wrap. */
|
|
11
|
+
const TRICKLE_WRAPPED = Symbol.for('__trickle_wrapped');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Wrap a function to capture runtime type information on each call.
|
|
15
|
+
* The wrapper is completely transparent: same name, same length, same behavior.
|
|
16
|
+
* Errors are always re-thrown after capturing type context.
|
|
17
|
+
*/
|
|
18
|
+
export function wrapFunction<T extends (...args: any[]) => any>(fn: T, opts: WrapOptions): T {
|
|
19
|
+
if (!opts.enabled) return fn;
|
|
20
|
+
|
|
21
|
+
// Prevent double-wrapping (compile hook + load hook may both see the same function)
|
|
22
|
+
if ((fn as any)[TRICKLE_WRAPPED]) return fn;
|
|
23
|
+
|
|
24
|
+
const functionKey = `${opts.module}::${opts.functionName}`;
|
|
25
|
+
|
|
26
|
+
// Create wrapper with same length using a dynamic approach
|
|
27
|
+
const wrapper = function (this: any, ...args: any[]): any {
|
|
28
|
+
// Sample rate check
|
|
29
|
+
if (opts.sampleRate < 1 && Math.random() > opts.sampleRate) {
|
|
30
|
+
return fn.apply(this, args);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Set up arg tracking
|
|
34
|
+
const trackers: Array<{ proxy: unknown; getAccessedPaths: () => Map<string, TypeNode> }> = [];
|
|
35
|
+
const proxiedArgs = args.map((arg, i) => {
|
|
36
|
+
if (arg !== null && arg !== undefined && typeof arg === 'object') {
|
|
37
|
+
const tracker = createTracker(arg);
|
|
38
|
+
trackers.push(tracker);
|
|
39
|
+
return tracker.proxy;
|
|
40
|
+
}
|
|
41
|
+
return arg;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
let result: any;
|
|
45
|
+
let threwError = false;
|
|
46
|
+
let caughtError: unknown;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
result = fn.apply(this, proxiedArgs);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
threwError = true;
|
|
52
|
+
caughtError = err;
|
|
53
|
+
|
|
54
|
+
// Capture error context
|
|
55
|
+
try {
|
|
56
|
+
captureErrorPayload(functionKey, opts, args, trackers, err);
|
|
57
|
+
} catch {
|
|
58
|
+
// Never let our instrumentation interfere
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// CRITICAL: always re-throw
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Handle async functions (Promise return)
|
|
66
|
+
if (result !== null && result !== undefined && typeof result === 'object' && typeof result.then === 'function') {
|
|
67
|
+
return result.then(
|
|
68
|
+
(resolved: unknown) => {
|
|
69
|
+
try {
|
|
70
|
+
capturePayload(functionKey, opts, args, trackers, resolved, true);
|
|
71
|
+
} catch {
|
|
72
|
+
// Never let our instrumentation interfere
|
|
73
|
+
}
|
|
74
|
+
return resolved;
|
|
75
|
+
},
|
|
76
|
+
(err: unknown) => {
|
|
77
|
+
try {
|
|
78
|
+
captureErrorPayload(functionKey, opts, args, trackers, err);
|
|
79
|
+
} catch {
|
|
80
|
+
// Never let our instrumentation interfere
|
|
81
|
+
}
|
|
82
|
+
// Re-throw the original rejection
|
|
83
|
+
throw err;
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Synchronous return
|
|
89
|
+
try {
|
|
90
|
+
capturePayload(functionKey, opts, args, trackers, result);
|
|
91
|
+
} catch {
|
|
92
|
+
// Never let our instrumentation interfere
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return result;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Preserve function name and length
|
|
99
|
+
Object.defineProperty(wrapper, 'name', { value: fn.name || opts.functionName, configurable: true });
|
|
100
|
+
Object.defineProperty(wrapper, 'length', { value: fn.length, configurable: true });
|
|
101
|
+
|
|
102
|
+
// Mark as wrapped to prevent double-wrapping
|
|
103
|
+
(wrapper as any)[TRICKLE_WRAPPED] = true;
|
|
104
|
+
|
|
105
|
+
return wrapper as unknown as T;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Capture and enqueue a successful invocation's type data.
|
|
110
|
+
*/
|
|
111
|
+
function capturePayload(
|
|
112
|
+
functionKey: string,
|
|
113
|
+
opts: WrapOptions,
|
|
114
|
+
originalArgs: unknown[],
|
|
115
|
+
trackers: Array<{ proxy: unknown; getAccessedPaths: () => Map<string, TypeNode> }>,
|
|
116
|
+
returnValue: unknown,
|
|
117
|
+
isAsync: boolean = false,
|
|
118
|
+
): void {
|
|
119
|
+
// Build args type as a tuple
|
|
120
|
+
const argsType = buildArgsType(originalArgs, trackers, opts.maxDepth);
|
|
121
|
+
const returnType = inferType(returnValue, opts.maxDepth);
|
|
122
|
+
|
|
123
|
+
const hash = hashType(argsType, returnType);
|
|
124
|
+
|
|
125
|
+
// Check cache
|
|
126
|
+
if (!typeCache.shouldSend(functionKey, hash)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
typeCache.markSent(functionKey, hash);
|
|
131
|
+
|
|
132
|
+
const payload: IngestPayload = {
|
|
133
|
+
functionName: opts.functionName,
|
|
134
|
+
module: opts.module,
|
|
135
|
+
language: 'js',
|
|
136
|
+
environment: opts.environment,
|
|
137
|
+
typeHash: hash,
|
|
138
|
+
argsType,
|
|
139
|
+
returnType,
|
|
140
|
+
sampleInput: sanitizeSample(originalArgs),
|
|
141
|
+
sampleOutput: sanitizeSample(returnValue),
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
if (isAsync) {
|
|
145
|
+
payload.isAsync = true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (opts.paramNames && opts.paramNames.length > 0) {
|
|
149
|
+
payload.paramNames = opts.paramNames;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
enqueue(payload);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Capture type context for a failed invocation.
|
|
157
|
+
*/
|
|
158
|
+
function captureErrorPayload(
|
|
159
|
+
functionKey: string,
|
|
160
|
+
opts: WrapOptions,
|
|
161
|
+
originalArgs: unknown[],
|
|
162
|
+
trackers: Array<{ proxy: unknown; getAccessedPaths: () => Map<string, TypeNode> }>,
|
|
163
|
+
error: unknown,
|
|
164
|
+
): void {
|
|
165
|
+
const argsType = buildArgsType(originalArgs, trackers, opts.maxDepth);
|
|
166
|
+
const returnType: TypeNode = { kind: 'unknown' };
|
|
167
|
+
const hash = hashType(argsType, returnType);
|
|
168
|
+
|
|
169
|
+
// Always send error payloads (don't cache-skip them)
|
|
170
|
+
|
|
171
|
+
const errorInfo = extractErrorInfo(error);
|
|
172
|
+
|
|
173
|
+
// Collect accessed paths from trackers as "variable" context
|
|
174
|
+
const accessedPaths: Record<string, TypeNode> = {};
|
|
175
|
+
for (const tracker of trackers) {
|
|
176
|
+
const paths = tracker.getAccessedPaths();
|
|
177
|
+
for (const [path, type] of paths) {
|
|
178
|
+
accessedPaths[path] = type;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const payload: IngestPayload = {
|
|
183
|
+
functionName: opts.functionName,
|
|
184
|
+
module: opts.module,
|
|
185
|
+
language: 'js',
|
|
186
|
+
environment: opts.environment,
|
|
187
|
+
typeHash: hash,
|
|
188
|
+
argsType,
|
|
189
|
+
returnType,
|
|
190
|
+
sampleInput: sanitizeSample(originalArgs),
|
|
191
|
+
error: {
|
|
192
|
+
type: errorInfo.type,
|
|
193
|
+
message: errorInfo.message,
|
|
194
|
+
stackTrace: errorInfo.stack,
|
|
195
|
+
argsSnapshot: sanitizeSample(originalArgs),
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
enqueue(payload);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Build a tuple TypeNode for the args array.
|
|
204
|
+
* Uses tracker data for objects/arrays that were proxied (captures the "shape" actually used),
|
|
205
|
+
* and falls back to full inference for primitives and untracked args.
|
|
206
|
+
*/
|
|
207
|
+
function buildArgsType(
|
|
208
|
+
originalArgs: unknown[],
|
|
209
|
+
trackers: Array<{ proxy: unknown; getAccessedPaths: () => Map<string, TypeNode> }>,
|
|
210
|
+
maxDepth: number,
|
|
211
|
+
): TypeNode {
|
|
212
|
+
const elements: TypeNode[] = originalArgs.map(arg => inferType(arg, maxDepth));
|
|
213
|
+
|
|
214
|
+
if (elements.length === 0) {
|
|
215
|
+
return { kind: 'tuple', elements: [] };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return { kind: 'tuple', elements };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Extract error information safely.
|
|
223
|
+
*/
|
|
224
|
+
function extractErrorInfo(err: unknown): { type: string; message: string; stack?: string } {
|
|
225
|
+
if (err instanceof Error) {
|
|
226
|
+
return {
|
|
227
|
+
type: err.constructor.name || 'Error',
|
|
228
|
+
message: err.message,
|
|
229
|
+
stack: err.stack,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (typeof err === 'string') {
|
|
234
|
+
return { type: 'String', message: err };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
type: typeof err,
|
|
239
|
+
message: String(err),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Sanitize a sample value for safe serialization.
|
|
245
|
+
* Truncates large strings, limits array lengths, strips functions.
|
|
246
|
+
*/
|
|
247
|
+
function sanitizeSample(value: unknown, depth: number = 3): unknown {
|
|
248
|
+
if (depth <= 0) return '[truncated]';
|
|
249
|
+
if (value === null || value === undefined) return value;
|
|
250
|
+
|
|
251
|
+
const t = typeof value;
|
|
252
|
+
if (t === 'string') {
|
|
253
|
+
const s = value as string;
|
|
254
|
+
return s.length > 200 ? s.substring(0, 200) + '...' : s;
|
|
255
|
+
}
|
|
256
|
+
if (t === 'number' || t === 'boolean') return value;
|
|
257
|
+
if (t === 'bigint') return String(value);
|
|
258
|
+
if (t === 'symbol') return String(value);
|
|
259
|
+
if (t === 'function') return `[Function: ${(value as Function).name || 'anonymous'}]`;
|
|
260
|
+
|
|
261
|
+
if (Array.isArray(value)) {
|
|
262
|
+
const sample = value.slice(0, 5);
|
|
263
|
+
return sample.map(item => sanitizeSample(item, depth - 1));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (t === 'object') {
|
|
267
|
+
if (value instanceof Date) return value.toISOString();
|
|
268
|
+
if (value instanceof RegExp) return String(value);
|
|
269
|
+
if (value instanceof Error) return { error: value.message };
|
|
270
|
+
if (value instanceof Map) return `[Map: ${value.size} entries]`;
|
|
271
|
+
if (value instanceof Set) return `[Set: ${value.size} items]`;
|
|
272
|
+
|
|
273
|
+
const obj = value as Record<string, unknown>;
|
|
274
|
+
const result: Record<string, unknown> = {};
|
|
275
|
+
const keys = Object.keys(obj).slice(0, 20);
|
|
276
|
+
for (const key of keys) {
|
|
277
|
+
try {
|
|
278
|
+
result[key] = sanitizeSample(obj[key], depth - 1);
|
|
279
|
+
} catch {
|
|
280
|
+
result[key] = '[unreadable]';
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return String(value);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export { typeCache };
|