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 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
@@ -42,6 +42,7 @@ export interface IngestPayload {
42
42
  paramNames?: string[];
43
43
  sampleInput?: unknown;
44
44
  sampleOutput?: unknown;
45
+ durationMs?: number;
45
46
  error?: {
46
47
  type: string;
47
48
  message: string;
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
- captureErrorPayload(functionKey, opts, args, trackers, err);
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
- capturePayload(functionKey, opts, args, trackers, resolved, true);
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
- captureErrorPayload(functionKey, opts, args, trackers, err);
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
- capturePayload(functionKey, opts, args, trackers, result);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-observe",
3
- "version": "0.2.89",
3
+ "version": "0.2.91",
4
4
  "description": "Runtime type observability for JavaScript applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
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
@@ -22,6 +22,7 @@ export interface IngestPayload {
22
22
  paramNames?: string[];
23
23
  sampleInput?: unknown;
24
24
  sampleOutput?: unknown;
25
+ durationMs?: number;
25
26
  error?: {
26
27
  type: string;
27
28
  message: string;
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
- captureErrorPayload(functionKey, opts, args, trackers, err);
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
- capturePayload(functionKey, opts, args, trackers, resolved, true);
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
- captureErrorPayload(functionKey, opts, args, trackers, err);
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
- capturePayload(functionKey, opts, args, trackers, result);
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' };