trickle-observe 0.2.89 → 0.2.91
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/dist/trace-var.js +57 -0
- package/dist/types.d.ts +1 -0
- package/dist/wrap.js +15 -7
- package/package.json +1 -1
- package/src/trace-var.ts +57 -0
- package/src/types.ts +1 -0
- package/src/wrap.ts +16 -5
package/dist/trace-var.js
CHANGED
|
@@ -238,4 +238,61 @@ if (typeof process !== 'undefined' && process.on) {
|
|
|
238
238
|
process.on('beforeExit', exitFlush);
|
|
239
239
|
process.on('SIGTERM', exitFlush);
|
|
240
240
|
process.on('SIGINT', exitFlush);
|
|
241
|
+
// Capture uncaught exceptions with variable context for agent debugging.
|
|
242
|
+
// Write error + nearby variable values to errors.jsonl before the process exits.
|
|
243
|
+
process.on('uncaughtException', (err) => {
|
|
244
|
+
flushVarBuffer();
|
|
245
|
+
try {
|
|
246
|
+
const dir = varsFilePath
|
|
247
|
+
? path.dirname(varsFilePath)
|
|
248
|
+
: process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
|
|
249
|
+
try {
|
|
250
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
251
|
+
}
|
|
252
|
+
catch { }
|
|
253
|
+
const errorsFile = path.join(dir, 'errors.jsonl');
|
|
254
|
+
// Extract file and line from stack trace
|
|
255
|
+
const stackLines = (err.stack || '').split('\n');
|
|
256
|
+
let errorFile;
|
|
257
|
+
let errorLine;
|
|
258
|
+
for (const sl of stackLines.slice(1)) {
|
|
259
|
+
const m = sl.match(/\((.+):(\d+):\d+\)/) || sl.match(/at (.+):(\d+):\d+/);
|
|
260
|
+
if (m && !m[1].includes('node_modules') && !m[1].includes('node:') && !m[1].includes('trickle')) {
|
|
261
|
+
errorFile = m[1];
|
|
262
|
+
errorLine = parseInt(m[2]);
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// Find nearby variable values from the cache
|
|
267
|
+
const nearbyVars = {};
|
|
268
|
+
if (errorFile && errorLine) {
|
|
269
|
+
for (const [key, entry] of varCache) {
|
|
270
|
+
const parts = key.split(':');
|
|
271
|
+
const file = parts[0];
|
|
272
|
+
const line = parseInt(parts[1]);
|
|
273
|
+
const varName = parts.slice(2).join(':');
|
|
274
|
+
if (file === errorFile && Math.abs(line - errorLine) <= 10) {
|
|
275
|
+
nearbyVars[`L${parts[1]} ${varName}`] = entry.fp;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const record = {
|
|
280
|
+
kind: 'error',
|
|
281
|
+
error: err.message,
|
|
282
|
+
type: err.constructor.name,
|
|
283
|
+
file: errorFile,
|
|
284
|
+
line: errorLine,
|
|
285
|
+
stack: stackLines.slice(0, 6).join('\n'),
|
|
286
|
+
nearbyVariables: Object.keys(nearbyVars).length > 0 ? nearbyVars : undefined,
|
|
287
|
+
timestamp: new Date().toISOString(),
|
|
288
|
+
};
|
|
289
|
+
fs.appendFileSync(errorsFile, JSON.stringify(record) + '\n');
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
// Never suppress the original error
|
|
293
|
+
}
|
|
294
|
+
// Print the original error and exit (don't re-throw to preserve original stack)
|
|
295
|
+
console.error(err.stack || err.message);
|
|
296
|
+
process.exit(1);
|
|
297
|
+
});
|
|
241
298
|
}
|
package/dist/types.d.ts
CHANGED
package/dist/wrap.js
CHANGED
|
@@ -32,6 +32,7 @@ function wrapFunction(fn, opts) {
|
|
|
32
32
|
let threwError = false;
|
|
33
33
|
let caughtError;
|
|
34
34
|
const trackers = [];
|
|
35
|
+
const startTime = performance.now();
|
|
35
36
|
try {
|
|
36
37
|
// Always pass ORIGINAL args to the function — never proxied ones.
|
|
37
38
|
// Proxied args can break framework internals (Express Router, DI containers, etc.)
|
|
@@ -40,9 +41,10 @@ function wrapFunction(fn, opts) {
|
|
|
40
41
|
catch (err) {
|
|
41
42
|
threwError = true;
|
|
42
43
|
caughtError = err;
|
|
43
|
-
// Capture error context
|
|
44
|
+
// Capture error context with timing
|
|
44
45
|
try {
|
|
45
|
-
|
|
46
|
+
const durationMs = performance.now() - startTime;
|
|
47
|
+
captureErrorPayload(functionKey, opts, args, trackers, err, durationMs);
|
|
46
48
|
}
|
|
47
49
|
catch {
|
|
48
50
|
// Never let our instrumentation interfere
|
|
@@ -54,7 +56,8 @@ function wrapFunction(fn, opts) {
|
|
|
54
56
|
if (result !== null && result !== undefined && typeof result === 'object' && typeof result.then === 'function') {
|
|
55
57
|
return result.then((resolved) => {
|
|
56
58
|
try {
|
|
57
|
-
|
|
59
|
+
const durationMs = performance.now() - startTime;
|
|
60
|
+
capturePayload(functionKey, opts, args, trackers, resolved, true, durationMs);
|
|
58
61
|
}
|
|
59
62
|
catch {
|
|
60
63
|
// Never let our instrumentation interfere
|
|
@@ -62,7 +65,8 @@ function wrapFunction(fn, opts) {
|
|
|
62
65
|
return resolved;
|
|
63
66
|
}, (err) => {
|
|
64
67
|
try {
|
|
65
|
-
|
|
68
|
+
const durationMs = performance.now() - startTime;
|
|
69
|
+
captureErrorPayload(functionKey, opts, args, trackers, err, durationMs);
|
|
66
70
|
}
|
|
67
71
|
catch {
|
|
68
72
|
// Never let our instrumentation interfere
|
|
@@ -73,7 +77,8 @@ function wrapFunction(fn, opts) {
|
|
|
73
77
|
}
|
|
74
78
|
// Synchronous return
|
|
75
79
|
try {
|
|
76
|
-
|
|
80
|
+
const durationMs = performance.now() - startTime;
|
|
81
|
+
capturePayload(functionKey, opts, args, trackers, result, false, durationMs);
|
|
77
82
|
}
|
|
78
83
|
catch {
|
|
79
84
|
// Never let our instrumentation interfere
|
|
@@ -90,7 +95,7 @@ function wrapFunction(fn, opts) {
|
|
|
90
95
|
/**
|
|
91
96
|
* Capture and enqueue a successful invocation's type data.
|
|
92
97
|
*/
|
|
93
|
-
function capturePayload(functionKey, opts, originalArgs, trackers, returnValue, isAsync = false) {
|
|
98
|
+
function capturePayload(functionKey, opts, originalArgs, trackers, returnValue, isAsync = false, durationMs) {
|
|
94
99
|
// Build args type as a tuple
|
|
95
100
|
const argsType = buildArgsType(originalArgs, trackers, opts.maxDepth);
|
|
96
101
|
const returnType = (0, type_inference_1.inferType)(returnValue, opts.maxDepth);
|
|
@@ -117,12 +122,15 @@ function capturePayload(functionKey, opts, originalArgs, trackers, returnValue,
|
|
|
117
122
|
if (opts.paramNames && opts.paramNames.length > 0) {
|
|
118
123
|
payload.paramNames = opts.paramNames;
|
|
119
124
|
}
|
|
125
|
+
if (durationMs !== undefined) {
|
|
126
|
+
payload.durationMs = Math.round(durationMs * 100) / 100;
|
|
127
|
+
}
|
|
120
128
|
(0, transport_1.enqueue)(payload);
|
|
121
129
|
}
|
|
122
130
|
/**
|
|
123
131
|
* Capture type context for a failed invocation.
|
|
124
132
|
*/
|
|
125
|
-
function captureErrorPayload(functionKey, opts, originalArgs, trackers, error) {
|
|
133
|
+
function captureErrorPayload(functionKey, opts, originalArgs, trackers, error, durationMs) {
|
|
126
134
|
const argsType = buildArgsType(originalArgs, trackers, opts.maxDepth);
|
|
127
135
|
const returnType = { kind: 'unknown' };
|
|
128
136
|
const hash = (0, type_hash_1.hashType)(argsType, returnType);
|
package/package.json
CHANGED
package/src/trace-var.ts
CHANGED
|
@@ -221,4 +221,61 @@ if (typeof process !== 'undefined' && process.on) {
|
|
|
221
221
|
process.on('beforeExit', exitFlush);
|
|
222
222
|
process.on('SIGTERM', exitFlush);
|
|
223
223
|
process.on('SIGINT', exitFlush);
|
|
224
|
+
|
|
225
|
+
// Capture uncaught exceptions with variable context for agent debugging.
|
|
226
|
+
// Write error + nearby variable values to errors.jsonl before the process exits.
|
|
227
|
+
process.on('uncaughtException', (err: Error) => {
|
|
228
|
+
flushVarBuffer();
|
|
229
|
+
try {
|
|
230
|
+
const dir = varsFilePath
|
|
231
|
+
? path.dirname(varsFilePath)
|
|
232
|
+
: process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
|
|
233
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch {}
|
|
234
|
+
const errorsFile = path.join(dir, 'errors.jsonl');
|
|
235
|
+
|
|
236
|
+
// Extract file and line from stack trace
|
|
237
|
+
const stackLines = (err.stack || '').split('\n');
|
|
238
|
+
let errorFile: string | undefined;
|
|
239
|
+
let errorLine: number | undefined;
|
|
240
|
+
for (const sl of stackLines.slice(1)) {
|
|
241
|
+
const m = sl.match(/\((.+):(\d+):\d+\)/) || sl.match(/at (.+):(\d+):\d+/);
|
|
242
|
+
if (m && !m[1].includes('node_modules') && !m[1].includes('node:') && !m[1].includes('trickle')) {
|
|
243
|
+
errorFile = m[1];
|
|
244
|
+
errorLine = parseInt(m[2]);
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Find nearby variable values from the cache
|
|
250
|
+
const nearbyVars: Record<string, string> = {};
|
|
251
|
+
if (errorFile && errorLine) {
|
|
252
|
+
for (const [key, entry] of varCache) {
|
|
253
|
+
const parts = key.split(':');
|
|
254
|
+
const file = parts[0];
|
|
255
|
+
const line = parseInt(parts[1]);
|
|
256
|
+
const varName = parts.slice(2).join(':');
|
|
257
|
+
if (file === errorFile && Math.abs(line - errorLine) <= 10) {
|
|
258
|
+
nearbyVars[`L${parts[1]} ${varName}`] = entry.fp;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const record = {
|
|
264
|
+
kind: 'error',
|
|
265
|
+
error: err.message,
|
|
266
|
+
type: err.constructor.name,
|
|
267
|
+
file: errorFile,
|
|
268
|
+
line: errorLine,
|
|
269
|
+
stack: stackLines.slice(0, 6).join('\n'),
|
|
270
|
+
nearbyVariables: Object.keys(nearbyVars).length > 0 ? nearbyVars : undefined,
|
|
271
|
+
timestamp: new Date().toISOString(),
|
|
272
|
+
};
|
|
273
|
+
fs.appendFileSync(errorsFile, JSON.stringify(record) + '\n');
|
|
274
|
+
} catch {
|
|
275
|
+
// Never suppress the original error
|
|
276
|
+
}
|
|
277
|
+
// Print the original error and exit (don't re-throw to preserve original stack)
|
|
278
|
+
console.error(err.stack || err.message);
|
|
279
|
+
process.exit(1);
|
|
280
|
+
});
|
|
224
281
|
}
|
package/src/types.ts
CHANGED
package/src/wrap.ts
CHANGED
|
@@ -34,6 +34,7 @@ export function wrapFunction<T extends (...args: any[]) => any>(fn: T, opts: Wra
|
|
|
34
34
|
let threwError = false;
|
|
35
35
|
let caughtError: unknown;
|
|
36
36
|
const trackers: Array<{ proxy: unknown; getAccessedPaths: () => Map<string, TypeNode> }> = [];
|
|
37
|
+
const startTime = performance.now();
|
|
37
38
|
|
|
38
39
|
try {
|
|
39
40
|
// Always pass ORIGINAL args to the function — never proxied ones.
|
|
@@ -43,9 +44,10 @@ export function wrapFunction<T extends (...args: any[]) => any>(fn: T, opts: Wra
|
|
|
43
44
|
threwError = true;
|
|
44
45
|
caughtError = err;
|
|
45
46
|
|
|
46
|
-
// Capture error context
|
|
47
|
+
// Capture error context with timing
|
|
47
48
|
try {
|
|
48
|
-
|
|
49
|
+
const durationMs = performance.now() - startTime;
|
|
50
|
+
captureErrorPayload(functionKey, opts, args, trackers, err, durationMs);
|
|
49
51
|
} catch {
|
|
50
52
|
// Never let our instrumentation interfere
|
|
51
53
|
}
|
|
@@ -59,7 +61,8 @@ export function wrapFunction<T extends (...args: any[]) => any>(fn: T, opts: Wra
|
|
|
59
61
|
return result.then(
|
|
60
62
|
(resolved: unknown) => {
|
|
61
63
|
try {
|
|
62
|
-
|
|
64
|
+
const durationMs = performance.now() - startTime;
|
|
65
|
+
capturePayload(functionKey, opts, args, trackers, resolved, true, durationMs);
|
|
63
66
|
} catch {
|
|
64
67
|
// Never let our instrumentation interfere
|
|
65
68
|
}
|
|
@@ -67,7 +70,8 @@ export function wrapFunction<T extends (...args: any[]) => any>(fn: T, opts: Wra
|
|
|
67
70
|
},
|
|
68
71
|
(err: unknown) => {
|
|
69
72
|
try {
|
|
70
|
-
|
|
73
|
+
const durationMs = performance.now() - startTime;
|
|
74
|
+
captureErrorPayload(functionKey, opts, args, trackers, err, durationMs);
|
|
71
75
|
} catch {
|
|
72
76
|
// Never let our instrumentation interfere
|
|
73
77
|
}
|
|
@@ -79,7 +83,8 @@ export function wrapFunction<T extends (...args: any[]) => any>(fn: T, opts: Wra
|
|
|
79
83
|
|
|
80
84
|
// Synchronous return
|
|
81
85
|
try {
|
|
82
|
-
|
|
86
|
+
const durationMs = performance.now() - startTime;
|
|
87
|
+
capturePayload(functionKey, opts, args, trackers, result, false, durationMs);
|
|
83
88
|
} catch {
|
|
84
89
|
// Never let our instrumentation interfere
|
|
85
90
|
}
|
|
@@ -107,6 +112,7 @@ function capturePayload(
|
|
|
107
112
|
trackers: Array<{ proxy: unknown; getAccessedPaths: () => Map<string, TypeNode> }>,
|
|
108
113
|
returnValue: unknown,
|
|
109
114
|
isAsync: boolean = false,
|
|
115
|
+
durationMs?: number,
|
|
110
116
|
): void {
|
|
111
117
|
// Build args type as a tuple
|
|
112
118
|
const argsType = buildArgsType(originalArgs, trackers, opts.maxDepth);
|
|
@@ -141,6 +147,10 @@ function capturePayload(
|
|
|
141
147
|
payload.paramNames = opts.paramNames;
|
|
142
148
|
}
|
|
143
149
|
|
|
150
|
+
if (durationMs !== undefined) {
|
|
151
|
+
payload.durationMs = Math.round(durationMs * 100) / 100;
|
|
152
|
+
}
|
|
153
|
+
|
|
144
154
|
enqueue(payload);
|
|
145
155
|
}
|
|
146
156
|
|
|
@@ -153,6 +163,7 @@ function captureErrorPayload(
|
|
|
153
163
|
originalArgs: unknown[],
|
|
154
164
|
trackers: Array<{ proxy: unknown; getAccessedPaths: () => Map<string, TypeNode> }>,
|
|
155
165
|
error: unknown,
|
|
166
|
+
durationMs?: number,
|
|
156
167
|
): void {
|
|
157
168
|
const argsType = buildArgsType(originalArgs, trackers, opts.maxDepth);
|
|
158
169
|
const returnType: TypeNode = { kind: 'unknown' };
|