trickle-observe 0.2.115 → 0.2.117

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.
@@ -51,7 +51,7 @@ let traceFile = null;
51
51
  let callCounter = 0;
52
52
  let currentCallId = 0; // 0 = top level
53
53
  const callStack = [0];
54
- const MAX_TRACE_EVENTS = 500;
54
+ const MAX_TRACE_EVENTS = 1000;
55
55
  let eventCount = 0;
56
56
  function getTraceFile() {
57
57
  if (traceFile)
@@ -62,12 +62,17 @@ function getTraceFile() {
62
62
  }
63
63
  catch { }
64
64
  traceFile = path.join(dir, 'calltrace.jsonl');
65
+ // Only create the file if it doesn't already exist — another module instance
66
+ // (e.g. the observe hook) may have already initialised it.
65
67
  try {
66
- fs.writeFileSync(traceFile, '');
68
+ fs.writeFileSync(traceFile, '', { flag: 'wx' });
69
+ }
70
+ catch {
71
+ // File already exists — that's fine
67
72
  }
68
- catch { }
69
73
  return traceFile;
70
74
  }
75
+ let _initialized = false;
71
76
  function writeEvent(event) {
72
77
  if (eventCount >= MAX_TRACE_EVENTS)
73
78
  return;
@@ -118,6 +123,18 @@ function traceReturn(callId, functionName, moduleName, durationMs, error) {
118
123
  }
119
124
  }
120
125
  function initCallTrace() {
121
- // Ensure trace file is initialized
122
- getTraceFile();
126
+ if (_initialized)
127
+ return;
128
+ _initialized = true;
129
+ // Truncate the file on explicit init (only the observe hook calls this)
130
+ const dir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
131
+ try {
132
+ fs.mkdirSync(dir, { recursive: true });
133
+ }
134
+ catch { }
135
+ traceFile = path.join(dir, 'calltrace.jsonl');
136
+ try {
137
+ fs.writeFileSync(traceFile, '');
138
+ }
139
+ catch { }
123
140
  }
package/dist/express.js CHANGED
@@ -1,12 +1,49 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.instrumentExpress = instrumentExpress;
4
37
  exports.trickleMiddleware = trickleMiddleware;
38
+ const fs = __importStar(require("fs"));
39
+ const pathMod = __importStar(require("path"));
5
40
  const type_inference_1 = require("./type-inference");
6
41
  const type_hash_1 = require("./type-hash");
7
42
  const cache_1 = require("./cache");
8
43
  const transport_1 = require("./transport");
9
44
  const env_detect_1 = require("./env-detect");
45
+ const request_context_1 = require("./request-context");
46
+ const call_trace_1 = require("./call-trace");
10
47
  const expressCache = new cache_1.TypeCache();
11
48
  /**
12
49
  * Extract the interesting parts of an Express request as a plain object
@@ -91,10 +128,55 @@ function sanitizeSample(value, depth = 3) {
91
128
  }
92
129
  return String(value);
93
130
  }
131
+ /**
132
+ * Write an error record to .trickle/errors.jsonl so that `trickle monitor`
133
+ * and `trickle heal` can detect runtime errors from Express handlers.
134
+ */
135
+ function writeErrorToFile(error, input, routeName) {
136
+ try {
137
+ const err = error instanceof Error ? error : new Error(String(error));
138
+ const isLambda = !!process.env.AWS_LAMBDA_FUNCTION_NAME;
139
+ const defaultDir = isLambda ? '/tmp/.trickle' : pathMod.join(process.cwd(), '.trickle');
140
+ const dir = process.env.TRICKLE_LOCAL_DIR || defaultDir;
141
+ try {
142
+ fs.mkdirSync(dir, { recursive: true });
143
+ }
144
+ catch { }
145
+ // Extract file and line from stack trace
146
+ const stackLines = (err.stack || '').split('\n');
147
+ let errorFile;
148
+ let errorLine;
149
+ for (const sl of stackLines.slice(1)) {
150
+ const m = sl.match(/\((.+):(\d+):\d+\)/) || sl.match(/at (.+):(\d+):\d+/);
151
+ if (m && !m[1].includes('node_modules') && !m[1].includes('node:') && !m[1].includes('trickle-observe')) {
152
+ errorFile = m[1];
153
+ errorLine = parseInt(m[2]);
154
+ break;
155
+ }
156
+ }
157
+ const record = {
158
+ kind: 'error',
159
+ error: err.message,
160
+ type: err.constructor?.name || 'Error',
161
+ message: err.message,
162
+ file: errorFile,
163
+ line: errorLine,
164
+ stack: stackLines.slice(0, 6).join('\n'),
165
+ route: routeName,
166
+ request: input,
167
+ timestamp: new Date().toISOString(),
168
+ };
169
+ const errorsFile = pathMod.join(dir, 'errors.jsonl');
170
+ fs.appendFileSync(errorsFile, JSON.stringify(record) + '\n');
171
+ }
172
+ catch {
173
+ // Never crash the user's app
174
+ }
175
+ }
94
176
  /**
95
177
  * Emit a type payload for a single Express route invocation.
96
178
  */
97
- function emitExpressPayload(functionName, environment, maxDepth, input, output, error) {
179
+ function emitExpressPayload(functionName, environment, maxDepth, input, output, error, durationMs) {
98
180
  try {
99
181
  const functionKey = `express::${functionName}`;
100
182
  const argsType = (0, type_inference_1.inferType)(input, maxDepth);
@@ -118,6 +200,9 @@ function emitExpressPayload(functionName, environment, maxDepth, input, output,
118
200
  sampleInput: sanitizeSample(input),
119
201
  sampleOutput: error ? undefined : sanitizeSample(output),
120
202
  };
203
+ if (durationMs !== undefined) {
204
+ payload.durationMs = Math.round(durationMs * 100) / 100;
205
+ }
121
206
  if (error) {
122
207
  const err = error instanceof Error ? error : new Error(String(error));
123
208
  payload.error = {
@@ -126,6 +211,8 @@ function emitExpressPayload(functionName, environment, maxDepth, input, output,
126
211
  stackTrace: err.stack,
127
212
  argsSnapshot: sanitizeSample(input),
128
213
  };
214
+ // Also write to errors.jsonl for monitor/heal detection
215
+ writeErrorToFile(error, input, functionName);
129
216
  }
130
217
  (0, transport_1.enqueue)(payload);
131
218
  }
@@ -143,15 +230,28 @@ function wrapExpressHandler(handler, routeName, opts) {
143
230
  if (opts.sampleRate < 1 && Math.random() > opts.sampleRate) {
144
231
  return handler.call(this, req, res, next);
145
232
  }
233
+ const self = this;
234
+ // Wrap entire handler execution in request context so downstream calls get a request ID
235
+ let returnValue;
236
+ (0, request_context_1.withRequestContext)(req, () => {
237
+ returnValue = _executeHandler(self, handler, req, res, next, routeName, opts);
238
+ });
239
+ return returnValue;
240
+ };
241
+ function _executeHandler(self, handler, req, res, next, routeName, opts) {
146
242
  const input = extractRequestInput(req);
147
243
  let captured = false;
244
+ const startTime = performance.now();
245
+ const callId = (0, call_trace_1.traceCall)(routeName, 'express');
148
246
  // Intercept res.json()
149
247
  const originalJson = res.json;
150
248
  if (typeof originalJson === 'function') {
151
249
  res.json = function (data) {
152
250
  if (!captured) {
153
251
  captured = true;
154
- emitExpressPayload(routeName, opts.environment, opts.maxDepth, input, data);
252
+ const durationMs = performance.now() - startTime;
253
+ (0, call_trace_1.traceReturn)(callId, routeName, 'express', durationMs);
254
+ emitExpressPayload(routeName, opts.environment, opts.maxDepth, input, data, undefined, durationMs);
155
255
  }
156
256
  res.json = originalJson; // restore
157
257
  return originalJson.call(res, data);
@@ -163,9 +263,11 @@ function wrapExpressHandler(handler, routeName, opts) {
163
263
  res.send = function (data) {
164
264
  if (!captured) {
165
265
  captured = true;
266
+ const durationMs = performance.now() - startTime;
267
+ (0, call_trace_1.traceReturn)(callId, routeName, 'express', durationMs);
166
268
  // Only capture non-string data as typed output; strings are usually HTML
167
269
  const output = typeof data === 'string' ? { __html: true } : data;
168
- emitExpressPayload(routeName, opts.environment, opts.maxDepth, input, output);
270
+ emitExpressPayload(routeName, opts.environment, opts.maxDepth, input, output, undefined, durationMs);
169
271
  }
170
272
  res.send = originalSend; // restore
171
273
  return originalSend.call(res, data);
@@ -175,20 +277,24 @@ function wrapExpressHandler(handler, routeName, opts) {
175
277
  const wrappedNext = function (err) {
176
278
  if (err && !captured) {
177
279
  captured = true;
178
- emitExpressPayload(routeName, opts.environment, opts.maxDepth, input, undefined, err);
280
+ const durationMs = performance.now() - startTime;
281
+ (0, call_trace_1.traceReturn)(callId, routeName, 'express', durationMs, (err instanceof Error ? err : new Error(String(err))).message);
282
+ emitExpressPayload(routeName, opts.environment, opts.maxDepth, input, undefined, err, durationMs);
179
283
  }
180
284
  if (typeof next === 'function') {
181
285
  return next(err);
182
286
  }
183
287
  };
184
288
  try {
185
- const result = handler.call(this, req, res, wrappedNext);
289
+ const result = handler.call(self, req, res, wrappedNext);
186
290
  // Handle async handlers that return a promise
187
291
  if (result && typeof result === 'object' && typeof result.then === 'function') {
188
292
  return result.catch((err) => {
189
293
  if (!captured) {
190
294
  captured = true;
191
- emitExpressPayload(routeName, opts.environment, opts.maxDepth, input, undefined, err);
295
+ const durationMs = performance.now() - startTime;
296
+ (0, call_trace_1.traceReturn)(callId, routeName, 'express', durationMs, (err instanceof Error ? err : new Error(String(err))).message);
297
+ emitExpressPayload(routeName, opts.environment, opts.maxDepth, input, undefined, err, durationMs);
192
298
  }
193
299
  // Re-throw so Express error handling picks it up
194
300
  throw err;
@@ -199,11 +305,13 @@ function wrapExpressHandler(handler, routeName, opts) {
199
305
  catch (err) {
200
306
  if (!captured) {
201
307
  captured = true;
202
- emitExpressPayload(routeName, opts.environment, opts.maxDepth, input, undefined, err);
308
+ const durationMs = performance.now() - startTime;
309
+ (0, call_trace_1.traceReturn)(callId, routeName, 'express', durationMs, (err instanceof Error ? err : new Error(String(err))).message);
310
+ emitExpressPayload(routeName, opts.environment, opts.maxDepth, input, undefined, err, durationMs);
203
311
  }
204
312
  throw err;
205
313
  }
206
- };
314
+ }
207
315
  // Preserve function metadata
208
316
  Object.defineProperty(wrapped, 'name', { value: handler.name || routeName, configurable: true });
209
317
  Object.defineProperty(wrapped, 'length', { value: handler.length, configurable: true });
@@ -294,17 +402,9 @@ function trickleMiddleware(userOpts) {
294
402
  return;
295
403
  }
296
404
  // Wrap in request context for per-request correlation
297
- try {
298
- const { withRequestContext } = require('./request-context');
299
- withRequestContext(req, () => {
300
- _handleRequest(req, res, next, opts, debug);
301
- });
302
- return;
303
- }
304
- catch {
305
- // Fall through to non-context version
306
- }
307
- _handleRequest(req, res, next, opts, debug);
405
+ (0, request_context_1.withRequestContext)(req, () => {
406
+ _handleRequest(req, res, next, opts, debug);
407
+ });
308
408
  };
309
409
  }
310
410
  function _handleRequest(req, res, next, opts, debug) {
@@ -312,6 +412,9 @@ function _handleRequest(req, res, next, opts, debug) {
312
412
  console.log(`[trickle/middleware] Intercepting ${req.method} ${req.originalUrl || req.url}`);
313
413
  }
314
414
  let captured = false;
415
+ const startTime = performance.now();
416
+ const prelimRouteName = `${req.method || 'UNKNOWN'} ${req.originalUrl || req.url || '/'}`;
417
+ const callId = (0, call_trace_1.traceCall)(prelimRouteName, 'express');
315
418
  // We derive the route name lazily once the response is being sent,
316
419
  // because req.route is only populated after the handler matches.
317
420
  function getRouteName() {
@@ -323,7 +426,7 @@ function _handleRequest(req, res, next, opts, debug) {
323
426
  catch {
324
427
  // ignore
325
428
  }
326
- return `${req.method || 'UNKNOWN'} ${req.originalUrl || req.url || '/'}`;
429
+ return prelimRouteName;
327
430
  }
328
431
  const input = extractRequestInput(req);
329
432
  // Intercept res.json()
@@ -333,12 +436,14 @@ function _handleRequest(req, res, next, opts, debug) {
333
436
  if (!captured) {
334
437
  captured = true;
335
438
  const routeName = getRouteName();
439
+ const durationMs = performance.now() - startTime;
440
+ (0, call_trace_1.traceReturn)(callId, routeName, 'express', durationMs);
336
441
  if (debug) {
337
442
  console.log(`[trickle/middleware] Captured res.json for ${routeName}`);
338
443
  }
339
444
  // Re-extract input here because body parsers may have run since middleware was entered
340
445
  const latestInput = extractRequestInput(req);
341
- emitExpressPayload(routeName, opts.environment, opts.maxDepth, latestInput, data);
446
+ emitExpressPayload(routeName, opts.environment, opts.maxDepth, latestInput, data, undefined, durationMs);
342
447
  }
343
448
  res.json = originalJson;
344
449
  return originalJson.call(res, data);
@@ -354,12 +459,14 @@ function _handleRequest(req, res, next, opts, debug) {
354
459
  if (!captured) {
355
460
  captured = true;
356
461
  const routeName = getRouteName();
462
+ const durationMs = performance.now() - startTime;
463
+ (0, call_trace_1.traceReturn)(callId, routeName, 'express', durationMs);
357
464
  if (debug) {
358
465
  console.log(`[trickle/middleware] Captured res.send for ${routeName}`);
359
466
  }
360
467
  const latestInput = extractRequestInput(req);
361
468
  const output = typeof data === 'string' ? { __html: true } : data;
362
- emitExpressPayload(routeName, opts.environment, opts.maxDepth, latestInput, output);
469
+ emitExpressPayload(routeName, opts.environment, opts.maxDepth, latestInput, output, undefined, durationMs);
363
470
  }
364
471
  res.send = originalSend;
365
472
  return originalSend.call(res, data);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-observe",
3
- "version": "0.2.115",
3
+ "version": "0.2.117",
4
4
  "description": "Runtime type observability for JavaScript applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/call-trace.ts CHANGED
@@ -28,7 +28,7 @@ let traceFile: string | null = null;
28
28
  let callCounter = 0;
29
29
  let currentCallId = 0; // 0 = top level
30
30
  const callStack: number[] = [0];
31
- const MAX_TRACE_EVENTS = 500;
31
+ const MAX_TRACE_EVENTS = 1000;
32
32
  let eventCount = 0;
33
33
 
34
34
  function getTraceFile(): string {
@@ -36,10 +36,18 @@ function getTraceFile(): string {
36
36
  const dir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
37
37
  try { fs.mkdirSync(dir, { recursive: true }); } catch {}
38
38
  traceFile = path.join(dir, 'calltrace.jsonl');
39
- try { fs.writeFileSync(traceFile, ''); } catch {}
39
+ // Only create the file if it doesn't already exist — another module instance
40
+ // (e.g. the observe hook) may have already initialised it.
41
+ try {
42
+ fs.writeFileSync(traceFile, '', { flag: 'wx' });
43
+ } catch {
44
+ // File already exists — that's fine
45
+ }
40
46
  return traceFile;
41
47
  }
42
48
 
49
+ let _initialized = false;
50
+
43
51
  function writeEvent(event: CallEvent): void {
44
52
  if (eventCount >= MAX_TRACE_EVENTS) return;
45
53
  eventCount++;
@@ -99,6 +107,11 @@ export function traceReturn(
99
107
  }
100
108
 
101
109
  export function initCallTrace(): void {
102
- // Ensure trace file is initialized
103
- getTraceFile();
110
+ if (_initialized) return;
111
+ _initialized = true;
112
+ // Truncate the file on explicit init (only the observe hook calls this)
113
+ const dir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
114
+ try { fs.mkdirSync(dir, { recursive: true }); } catch {}
115
+ traceFile = path.join(dir, 'calltrace.jsonl');
116
+ try { fs.writeFileSync(traceFile, ''); } catch {}
104
117
  }
package/src/express.ts CHANGED
@@ -1,9 +1,13 @@
1
+ import * as fs from 'fs';
2
+ import * as pathMod from 'path';
1
3
  import { TypeNode, IngestPayload, WrapOptions } from './types';
2
4
  import { inferType } from './type-inference';
3
5
  import { hashType } from './type-hash';
4
6
  import { TypeCache } from './cache';
5
7
  import { enqueue } from './transport';
6
8
  import { detectEnvironment } from './env-detect';
9
+ import { withRequestContext } from './request-context';
10
+ import { traceCall, traceReturn } from './call-trace';
7
11
 
8
12
  const expressCache = new TypeCache();
9
13
 
@@ -94,6 +98,51 @@ function sanitizeSample(value: unknown, depth: number = 3): unknown {
94
98
  return String(value);
95
99
  }
96
100
 
101
+ /**
102
+ * Write an error record to .trickle/errors.jsonl so that `trickle monitor`
103
+ * and `trickle heal` can detect runtime errors from Express handlers.
104
+ */
105
+ function writeErrorToFile(error: unknown, input: Record<string, unknown>, routeName: string): void {
106
+ try {
107
+ const err = error instanceof Error ? error : new Error(String(error));
108
+ const isLambda = !!process.env.AWS_LAMBDA_FUNCTION_NAME;
109
+ const defaultDir = isLambda ? '/tmp/.trickle' : pathMod.join(process.cwd(), '.trickle');
110
+ const dir = process.env.TRICKLE_LOCAL_DIR || defaultDir;
111
+ try { fs.mkdirSync(dir, { recursive: true }); } catch {}
112
+
113
+ // Extract file and line from stack trace
114
+ const stackLines = (err.stack || '').split('\n');
115
+ let errorFile: string | undefined;
116
+ let errorLine: number | undefined;
117
+ for (const sl of stackLines.slice(1)) {
118
+ const m = sl.match(/\((.+):(\d+):\d+\)/) || sl.match(/at (.+):(\d+):\d+/);
119
+ if (m && !m[1].includes('node_modules') && !m[1].includes('node:') && !m[1].includes('trickle-observe')) {
120
+ errorFile = m[1];
121
+ errorLine = parseInt(m[2]);
122
+ break;
123
+ }
124
+ }
125
+
126
+ const record = {
127
+ kind: 'error',
128
+ error: err.message,
129
+ type: err.constructor?.name || 'Error',
130
+ message: err.message,
131
+ file: errorFile,
132
+ line: errorLine,
133
+ stack: stackLines.slice(0, 6).join('\n'),
134
+ route: routeName,
135
+ request: input,
136
+ timestamp: new Date().toISOString(),
137
+ };
138
+
139
+ const errorsFile = pathMod.join(dir, 'errors.jsonl');
140
+ fs.appendFileSync(errorsFile, JSON.stringify(record) + '\n');
141
+ } catch {
142
+ // Never crash the user's app
143
+ }
144
+ }
145
+
97
146
  /**
98
147
  * Emit a type payload for a single Express route invocation.
99
148
  */
@@ -104,6 +153,7 @@ function emitExpressPayload(
104
153
  input: Record<string, unknown>,
105
154
  output: unknown,
106
155
  error?: unknown,
156
+ durationMs?: number,
107
157
  ): void {
108
158
  try {
109
159
  const functionKey = `express::${functionName}`;
@@ -132,6 +182,10 @@ function emitExpressPayload(
132
182
  sampleOutput: error ? undefined : sanitizeSample(output),
133
183
  };
134
184
 
185
+ if (durationMs !== undefined) {
186
+ payload.durationMs = Math.round(durationMs * 100) / 100;
187
+ }
188
+
135
189
  if (error) {
136
190
  const err = error instanceof Error ? error : new Error(String(error));
137
191
  payload.error = {
@@ -140,6 +194,8 @@ function emitExpressPayload(
140
194
  stackTrace: err.stack,
141
195
  argsSnapshot: sanitizeSample(input),
142
196
  };
197
+ // Also write to errors.jsonl for monitor/heal detection
198
+ writeErrorToFile(error, input, functionName);
143
199
  }
144
200
 
145
201
  enqueue(payload);
@@ -163,8 +219,29 @@ function wrapExpressHandler(
163
219
  return handler.call(this, req, res, next);
164
220
  }
165
221
 
222
+ const self = this;
223
+
224
+ // Wrap entire handler execution in request context so downstream calls get a request ID
225
+ let returnValue: any;
226
+ withRequestContext(req, () => {
227
+ returnValue = _executeHandler(self, handler, req, res, next, routeName, opts);
228
+ });
229
+ return returnValue;
230
+ };
231
+
232
+ function _executeHandler(
233
+ self: any,
234
+ handler: Function,
235
+ req: any,
236
+ res: any,
237
+ next: any,
238
+ routeName: string,
239
+ opts: ExpressInstrumentOpts,
240
+ ): any {
166
241
  const input = extractRequestInput(req);
167
242
  let captured = false;
243
+ const startTime = performance.now();
244
+ const callId = traceCall(routeName, 'express');
168
245
 
169
246
  // Intercept res.json()
170
247
  const originalJson = res.json;
@@ -172,7 +249,9 @@ function wrapExpressHandler(
172
249
  res.json = function (data: any) {
173
250
  if (!captured) {
174
251
  captured = true;
175
- emitExpressPayload(routeName, opts.environment, opts.maxDepth, input, data);
252
+ const durationMs = performance.now() - startTime;
253
+ traceReturn(callId, routeName, 'express', durationMs);
254
+ emitExpressPayload(routeName, opts.environment, opts.maxDepth, input, data, undefined, durationMs);
176
255
  }
177
256
  res.json = originalJson; // restore
178
257
  return originalJson.call(res, data);
@@ -185,9 +264,11 @@ function wrapExpressHandler(
185
264
  res.send = function (data: any) {
186
265
  if (!captured) {
187
266
  captured = true;
267
+ const durationMs = performance.now() - startTime;
268
+ traceReturn(callId, routeName, 'express', durationMs);
188
269
  // Only capture non-string data as typed output; strings are usually HTML
189
270
  const output = typeof data === 'string' ? { __html: true } : data;
190
- emitExpressPayload(routeName, opts.environment, opts.maxDepth, input, output);
271
+ emitExpressPayload(routeName, opts.environment, opts.maxDepth, input, output, undefined, durationMs);
191
272
  }
192
273
  res.send = originalSend; // restore
193
274
  return originalSend.call(res, data);
@@ -198,7 +279,9 @@ function wrapExpressHandler(
198
279
  const wrappedNext = function (err?: any) {
199
280
  if (err && !captured) {
200
281
  captured = true;
201
- emitExpressPayload(routeName, opts.environment, opts.maxDepth, input, undefined, err);
282
+ const durationMs = performance.now() - startTime;
283
+ traceReturn(callId, routeName, 'express', durationMs, (err instanceof Error ? err : new Error(String(err))).message);
284
+ emitExpressPayload(routeName, opts.environment, opts.maxDepth, input, undefined, err, durationMs);
202
285
  }
203
286
  if (typeof next === 'function') {
204
287
  return next(err);
@@ -206,14 +289,16 @@ function wrapExpressHandler(
206
289
  };
207
290
 
208
291
  try {
209
- const result = handler.call(this, req, res, wrappedNext);
292
+ const result = handler.call(self, req, res, wrappedNext);
210
293
 
211
294
  // Handle async handlers that return a promise
212
295
  if (result && typeof result === 'object' && typeof result.then === 'function') {
213
296
  return result.catch((err: unknown) => {
214
297
  if (!captured) {
215
298
  captured = true;
216
- emitExpressPayload(routeName, opts.environment, opts.maxDepth, input, undefined, err);
299
+ const durationMs = performance.now() - startTime;
300
+ traceReturn(callId, routeName, 'express', durationMs, (err instanceof Error ? err : new Error(String(err))).message);
301
+ emitExpressPayload(routeName, opts.environment, opts.maxDepth, input, undefined, err, durationMs);
217
302
  }
218
303
  // Re-throw so Express error handling picks it up
219
304
  throw err;
@@ -224,11 +309,13 @@ function wrapExpressHandler(
224
309
  } catch (err) {
225
310
  if (!captured) {
226
311
  captured = true;
227
- emitExpressPayload(routeName, opts.environment, opts.maxDepth, input, undefined, err);
312
+ const durationMs = performance.now() - startTime;
313
+ traceReturn(callId, routeName, 'express', durationMs, (err instanceof Error ? err : new Error(String(err))).message);
314
+ emitExpressPayload(routeName, opts.environment, opts.maxDepth, input, undefined, err, durationMs);
228
315
  }
229
316
  throw err;
230
317
  }
231
- };
318
+ }
232
319
 
233
320
  // Preserve function metadata
234
321
  Object.defineProperty(wrapped, 'name', { value: handler.name || routeName, configurable: true });
@@ -335,17 +422,9 @@ export function trickleMiddleware(
335
422
  }
336
423
 
337
424
  // Wrap in request context for per-request correlation
338
- try {
339
- const { withRequestContext } = require('./request-context');
340
- withRequestContext(req, () => {
341
- _handleRequest(req, res, next, opts, debug);
342
- });
343
- return;
344
- } catch {
345
- // Fall through to non-context version
346
- }
347
-
348
- _handleRequest(req, res, next, opts, debug);
425
+ withRequestContext(req, () => {
426
+ _handleRequest(req, res, next, opts, debug);
427
+ });
349
428
  };
350
429
  }
351
430
 
@@ -355,6 +434,9 @@ function _handleRequest(req: any, res: any, next: any, opts: any, debug: boolean
355
434
  }
356
435
 
357
436
  let captured = false;
437
+ const startTime = performance.now();
438
+ const prelimRouteName = `${req.method || 'UNKNOWN'} ${req.originalUrl || req.url || '/'}`;
439
+ const callId = traceCall(prelimRouteName, 'express');
358
440
 
359
441
  // We derive the route name lazily once the response is being sent,
360
442
  // because req.route is only populated after the handler matches.
@@ -366,7 +448,7 @@ function _handleRequest(req: any, res: any, next: any, opts: any, debug: boolean
366
448
  } catch {
367
449
  // ignore
368
450
  }
369
- return `${req.method || 'UNKNOWN'} ${req.originalUrl || req.url || '/'}`;
451
+ return prelimRouteName;
370
452
  }
371
453
 
372
454
  const input = extractRequestInput(req);
@@ -378,12 +460,14 @@ function _handleRequest(req: any, res: any, next: any, opts: any, debug: boolean
378
460
  if (!captured) {
379
461
  captured = true;
380
462
  const routeName = getRouteName();
463
+ const durationMs = performance.now() - startTime;
464
+ traceReturn(callId, routeName, 'express', durationMs);
381
465
  if (debug) {
382
466
  console.log(`[trickle/middleware] Captured res.json for ${routeName}`);
383
467
  }
384
468
  // Re-extract input here because body parsers may have run since middleware was entered
385
469
  const latestInput = extractRequestInput(req);
386
- emitExpressPayload(routeName, opts.environment, opts.maxDepth, latestInput, data);
470
+ emitExpressPayload(routeName, opts.environment, opts.maxDepth, latestInput, data, undefined, durationMs);
387
471
  }
388
472
  res.json = originalJson;
389
473
  return originalJson.call(res, data);
@@ -399,12 +483,14 @@ function _handleRequest(req: any, res: any, next: any, opts: any, debug: boolean
399
483
  if (!captured) {
400
484
  captured = true;
401
485
  const routeName = getRouteName();
486
+ const durationMs = performance.now() - startTime;
487
+ traceReturn(callId, routeName, 'express', durationMs);
402
488
  if (debug) {
403
489
  console.log(`[trickle/middleware] Captured res.send for ${routeName}`);
404
490
  }
405
491
  const latestInput = extractRequestInput(req);
406
492
  const output = typeof data === 'string' ? { __html: true } : data;
407
- emitExpressPayload(routeName, opts.environment, opts.maxDepth, latestInput, output);
493
+ emitExpressPayload(routeName, opts.environment, opts.maxDepth, latestInput, output, undefined, durationMs);
408
494
  }
409
495
  res.send = originalSend;
410
496
  return originalSend.call(res, data);
@@ -1 +0,0 @@
1
- export {};
@@ -1,160 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- /**
7
- * Unit tests for the Vite plugin transform (React component tracking).
8
- *
9
- * Run with: node --experimental-strip-types --test src/vite-plugin.test.ts
10
- * Or after build: node --test dist/vite-plugin.test.js
11
- */
12
- const node_test_1 = require("node:test");
13
- const strict_1 = __importDefault(require("node:assert/strict"));
14
- const vite_plugin_js_1 = require("../dist/vite-plugin.js");
15
- // Helper: transform code as if it came from a .tsx file
16
- function transformTsx(code) {
17
- const plugin = (0, vite_plugin_js_1.tricklePlugin)({ debug: false, traceVars: false });
18
- const result = plugin.transform(code, '/test/App.tsx');
19
- return result ? result.code : null;
20
- }
21
- function transformTs(code) {
22
- const plugin = (0, vite_plugin_js_1.tricklePlugin)({ debug: false, traceVars: false });
23
- const result = plugin.transform(code, '/test/util.ts');
24
- return result ? result.code : null;
25
- }
26
- // ── React file detection ─────────────────────────────────────────────────────
27
- (0, node_test_1.describe)('React file detection', () => {
28
- (0, node_test_1.it)('tracks uppercase components in .tsx files', () => {
29
- const code = `function UserCard(props) { return null; }`;
30
- const out = transformTsx(code);
31
- strict_1.default.ok(out, 'should transform');
32
- strict_1.default.ok(out.includes('__trickle_rc'), 'should inject render tracker');
33
- });
34
- (0, node_test_1.it)('does not inject render tracker for .ts files', () => {
35
- const code = `function UserCard(props) { return null; }`;
36
- const out = transformTs(code);
37
- // May still transform for function wrapping, but not for render tracking
38
- if (out) {
39
- strict_1.default.ok(!out.includes('__trickle_rc'), 'should NOT inject render tracker in .ts files');
40
- }
41
- });
42
- (0, node_test_1.it)('does not track lowercase functions as components', () => {
43
- const code = `function helper(x) { return x + 1; }`;
44
- const out = transformTsx(code);
45
- if (out) {
46
- strict_1.default.ok(!out.includes('__trickle_rc'), 'lowercase function should not be tracked');
47
- }
48
- });
49
- });
50
- // ── Props capture: function declarations ─────────────────────────────────────
51
- (0, node_test_1.describe)('Props capture — function declarations', () => {
52
- (0, node_test_1.it)('uses arguments[0] for simple param: function Component(props)', () => {
53
- const code = `function MyComponent(props) { return null; }`;
54
- const out = transformTsx(code);
55
- strict_1.default.ok(out, 'should transform');
56
- strict_1.default.ok(out.includes('arguments[0]'), 'should pass arguments[0] as props');
57
- });
58
- (0, node_test_1.it)('uses arguments[0] for destructured param: function Component({ name })', () => {
59
- const code = `function UserCard({ name, age }) { return null; }`;
60
- const out = transformTsx(code);
61
- strict_1.default.ok(out, 'should transform');
62
- strict_1.default.ok(out.includes('arguments[0]'), 'should pass arguments[0] for destructured params');
63
- });
64
- (0, node_test_1.it)('injects __trickle_rc call at start of function body', () => {
65
- const code = `function MyComponent(props) {\n const x = 1;\n return null;\n}`;
66
- const out = transformTsx(code);
67
- strict_1.default.ok(out, 'should transform');
68
- // __trickle_rc should appear before body statements
69
- const rcIdx = out.indexOf('__trickle_rc');
70
- const bodyIdx = out.indexOf('const x = 1');
71
- strict_1.default.ok(rcIdx !== -1, '__trickle_rc should be present');
72
- strict_1.default.ok(bodyIdx !== -1, 'body code should be present');
73
- strict_1.default.ok(rcIdx < bodyIdx, '__trickle_rc should come before body statements');
74
- });
75
- (0, node_test_1.it)('includes correct component name and line in __trickle_rc call', () => {
76
- const code = `function UserCard(props) { return null; }`;
77
- const out = transformTsx(code);
78
- strict_1.default.ok(out, 'should transform');
79
- strict_1.default.ok(out.includes('"UserCard"'), 'should include component name');
80
- strict_1.default.ok(out.includes('__trickle_rc("UserCard"'), 'should call with component name');
81
- });
82
- });
83
- // ── Props capture: arrow function components ──────────────────────────────────
84
- (0, node_test_1.describe)('Props capture — arrow function components', () => {
85
- (0, node_test_1.it)('uses single param name for simple arrow: const C = (props) => {}', () => {
86
- const code = `const Dashboard = (props) => { return null; };`;
87
- const out = transformTsx(code);
88
- strict_1.default.ok(out, 'should transform');
89
- strict_1.default.ok(out.includes('__trickle_rc'), 'should inject render tracker');
90
- // props should be the param variable, not arguments[0]
91
- strict_1.default.ok(out.includes('__trickle_rc("Dashboard"'), 'should use component name');
92
- // should NOT use arguments[0] for arrow functions
93
- const rcCall = out.match(/__trickle_rc\("Dashboard",[^)]+\)/);
94
- strict_1.default.ok(rcCall, 'should have __trickle_rc call');
95
- strict_1.default.ok(!rcCall[0].includes('arguments[0]'), 'arrow functions should not use arguments[0]');
96
- });
97
- (0, node_test_1.it)('reconstructs object for destructured arrow: const C = ({ a, b }) => {}', () => {
98
- const code = `const Counter = ({ count, label }) => { return null; };`;
99
- const out = transformTsx(code);
100
- strict_1.default.ok(out, 'should transform');
101
- strict_1.default.ok(out.includes('__trickle_rc'), 'should inject render tracker');
102
- // Should reconstruct { count, label }
103
- const rcCall = out.match(/__trickle_rc\("Counter",[^,]+,([^)]+)\)/);
104
- if (rcCall) {
105
- strict_1.default.ok(rcCall[1].includes('count') && rcCall[1].includes('label'), 'should reconstruct props object from destructured fields');
106
- }
107
- });
108
- (0, node_test_1.it)('passes undefined for no-param arrow: const C = () => {}', () => {
109
- const code = `const NoProps = () => { return null; };`;
110
- const out = transformTsx(code);
111
- if (out && out.includes('__trickle_rc')) {
112
- strict_1.default.ok(out.includes('undefined'), 'should pass undefined for no-param component');
113
- }
114
- });
115
- });
116
- // ── render count tracking ─────────────────────────────────────────────────────
117
- (0, node_test_1.describe)('Render count tracking', () => {
118
- (0, node_test_1.it)('includes react_render kind in emitted record code', () => {
119
- const code = `function Card(props) { return null; }`;
120
- const out = transformTsx(code);
121
- strict_1.default.ok(out, 'should transform');
122
- strict_1.default.ok(out.includes("'react_render'"), 'emitted record should have kind react_render');
123
- });
124
- (0, node_test_1.it)('includes props data in emitted record', () => {
125
- const code = `function Card(props) { return null; }`;
126
- const out = transformTsx(code);
127
- strict_1.default.ok(out, 'should transform');
128
- strict_1.default.ok(out.includes('rec.props'), 'should capture props onto the record');
129
- strict_1.default.ok(out.includes('propKeys'), 'should include propKeys');
130
- });
131
- (0, node_test_1.it)('tracks multiple components in one file', () => {
132
- const code = [
133
- `function Header(props) { return null; }`,
134
- `function Footer(props) { return null; }`,
135
- `function helper(x) { return x; }`,
136
- ].join('\n');
137
- const out = transformTsx(code);
138
- strict_1.default.ok(out, 'should transform');
139
- strict_1.default.ok(out.includes('"Header"'), 'should track Header');
140
- strict_1.default.ok(out.includes('"Footer"'), 'should track Footer');
141
- // helper should not be tracked as a component
142
- const rcCalls = out.match(/__trickle_rc\("helper"/g);
143
- strict_1.default.ok(!rcCalls, 'lowercase helper should not be tracked');
144
- });
145
- });
146
- // ── findFunctionBodyBrace — destructured params don't confuse brace finding ───
147
- (0, node_test_1.describe)('Correct function body brace detection', () => {
148
- (0, node_test_1.it)('finds body brace even with destructured object params', () => {
149
- const code = `function Form({ onSubmit, title }) {\n const x = 1;\n return null;\n}`;
150
- const out = transformTsx(code);
151
- strict_1.default.ok(out, 'should transform');
152
- // __trickle_rc should be INSIDE the function body (before 'const x = 1')
153
- const rcIdx = out.indexOf('__trickle_rc');
154
- const bodyIdx = out.indexOf('const x = 1');
155
- strict_1.default.ok(rcIdx < bodyIdx, 'render tracker must be inside the function body, before first statement');
156
- // The wrap insertion should be AFTER the closing brace of the function
157
- const wrapIdx = out.indexOf('__trickle_wrap');
158
- strict_1.default.ok(wrapIdx > bodyIdx, 'function wrap should be after the function body');
159
- });
160
- });