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.
- package/dist/call-trace.js +22 -5
- package/dist/express.js +129 -22
- package/package.json +1 -1
- package/src/call-trace.ts +17 -4
- package/src/express.ts +107 -21
- package/dist/vite-plugin.test.d.ts +0 -1
- package/dist/vite-plugin.test.js +0 -160
package/dist/call-trace.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
|
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
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 =
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
|
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 {};
|
package/dist/vite-plugin.test.js
DELETED
|
@@ -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
|
-
});
|