trickle-observe 0.2.114 → 0.2.115
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 +8 -0
- package/dist/db-observer.js +8 -0
- package/dist/express.js +68 -54
- package/dist/request-context.d.ts +32 -0
- package/dist/request-context.js +61 -0
- package/dist/vite-plugin.test.d.ts +1 -0
- package/dist/vite-plugin.test.js +160 -0
- package/package.json +1 -1
- package/src/call-trace.ts +9 -0
- package/src/db-observer.ts +7 -0
- package/src/express.ts +16 -1
- package/src/request-context.ts +64 -0
package/dist/call-trace.js
CHANGED
|
@@ -93,6 +93,13 @@ function traceCall(functionName, moduleName) {
|
|
|
93
93
|
function traceReturn(callId, functionName, moduleName, durationMs, error) {
|
|
94
94
|
const parentId = callStack.length >= 2 ? callStack[callStack.length - 2] : 0;
|
|
95
95
|
const depth = callStack.length - 1;
|
|
96
|
+
// Get request ID from async context (if inside an Express request)
|
|
97
|
+
let requestId;
|
|
98
|
+
try {
|
|
99
|
+
const { getRequestId } = require('./request-context');
|
|
100
|
+
requestId = getRequestId();
|
|
101
|
+
}
|
|
102
|
+
catch { }
|
|
96
103
|
writeEvent({
|
|
97
104
|
kind: 'call',
|
|
98
105
|
function: functionName,
|
|
@@ -103,6 +110,7 @@ function traceReturn(callId, functionName, moduleName, durationMs, error) {
|
|
|
103
110
|
timestamp: Date.now(),
|
|
104
111
|
durationMs: Math.round(durationMs * 100) / 100,
|
|
105
112
|
...(error ? { error } : {}),
|
|
113
|
+
...(requestId ? { requestId } : {}),
|
|
106
114
|
});
|
|
107
115
|
// Pop from stack
|
|
108
116
|
if (callStack[callStack.length - 1] === callId) {
|
package/dist/db-observer.js
CHANGED
|
@@ -80,6 +80,14 @@ function writeQuery(record) {
|
|
|
80
80
|
if (queryCount >= MAX_QUERIES)
|
|
81
81
|
return;
|
|
82
82
|
queryCount++;
|
|
83
|
+
// Add request ID from async context
|
|
84
|
+
try {
|
|
85
|
+
const { getRequestId } = require('./request-context');
|
|
86
|
+
const reqId = getRequestId();
|
|
87
|
+
if (reqId)
|
|
88
|
+
record.requestId = reqId;
|
|
89
|
+
}
|
|
90
|
+
catch { }
|
|
83
91
|
try {
|
|
84
92
|
fs.appendFileSync(getQueriesFile(), JSON.stringify(record) + '\n');
|
|
85
93
|
}
|
package/dist/express.js
CHANGED
|
@@ -293,63 +293,77 @@ function trickleMiddleware(userOpts) {
|
|
|
293
293
|
next();
|
|
294
294
|
return;
|
|
295
295
|
}
|
|
296
|
-
|
|
297
|
-
|
|
296
|
+
// 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;
|
|
298
303
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
// because req.route is only populated after the handler matches.
|
|
302
|
-
function getRouteName() {
|
|
303
|
-
try {
|
|
304
|
-
if (req.route && req.route.path) {
|
|
305
|
-
return `${req.method} ${req.baseUrl || ''}${req.route.path}`;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
catch {
|
|
309
|
-
// ignore
|
|
310
|
-
}
|
|
311
|
-
return `${req.method || 'UNKNOWN'} ${req.originalUrl || req.url || '/'}`;
|
|
304
|
+
catch {
|
|
305
|
+
// Fall through to non-context version
|
|
312
306
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
res.json = originalJson;
|
|
329
|
-
return originalJson.call(res, data);
|
|
330
|
-
};
|
|
307
|
+
_handleRequest(req, res, next, opts, debug);
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function _handleRequest(req, res, next, opts, debug) {
|
|
311
|
+
if (debug) {
|
|
312
|
+
console.log(`[trickle/middleware] Intercepting ${req.method} ${req.originalUrl || req.url}`);
|
|
313
|
+
}
|
|
314
|
+
let captured = false;
|
|
315
|
+
// We derive the route name lazily once the response is being sent,
|
|
316
|
+
// because req.route is only populated after the handler matches.
|
|
317
|
+
function getRouteName() {
|
|
318
|
+
try {
|
|
319
|
+
if (req.route && req.route.path) {
|
|
320
|
+
return `${req.method} ${req.baseUrl || ''}${req.route.path}`;
|
|
321
|
+
}
|
|
331
322
|
}
|
|
332
|
-
|
|
333
|
-
|
|
323
|
+
catch {
|
|
324
|
+
// ignore
|
|
334
325
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
emitExpressPayload(routeName, opts.environment, opts.maxDepth, latestInput, output);
|
|
326
|
+
return `${req.method || 'UNKNOWN'} ${req.originalUrl || req.url || '/'}`;
|
|
327
|
+
}
|
|
328
|
+
const input = extractRequestInput(req);
|
|
329
|
+
// Intercept res.json()
|
|
330
|
+
const originalJson = res.json;
|
|
331
|
+
if (typeof originalJson === 'function') {
|
|
332
|
+
res.json = function (data) {
|
|
333
|
+
if (!captured) {
|
|
334
|
+
captured = true;
|
|
335
|
+
const routeName = getRouteName();
|
|
336
|
+
if (debug) {
|
|
337
|
+
console.log(`[trickle/middleware] Captured res.json for ${routeName}`);
|
|
348
338
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
339
|
+
// Re-extract input here because body parsers may have run since middleware was entered
|
|
340
|
+
const latestInput = extractRequestInput(req);
|
|
341
|
+
emitExpressPayload(routeName, opts.environment, opts.maxDepth, latestInput, data);
|
|
342
|
+
}
|
|
343
|
+
res.json = originalJson;
|
|
344
|
+
return originalJson.call(res, data);
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
else if (debug) {
|
|
348
|
+
console.log(`[trickle/middleware] res.json is not a function: ${typeof originalJson}`);
|
|
349
|
+
}
|
|
350
|
+
// Intercept res.send()
|
|
351
|
+
const originalSend = res.send;
|
|
352
|
+
if (typeof originalSend === 'function') {
|
|
353
|
+
res.send = function (data) {
|
|
354
|
+
if (!captured) {
|
|
355
|
+
captured = true;
|
|
356
|
+
const routeName = getRouteName();
|
|
357
|
+
if (debug) {
|
|
358
|
+
console.log(`[trickle/middleware] Captured res.send for ${routeName}`);
|
|
359
|
+
}
|
|
360
|
+
const latestInput = extractRequestInput(req);
|
|
361
|
+
const output = typeof data === 'string' ? { __html: true } : data;
|
|
362
|
+
emitExpressPayload(routeName, opts.environment, opts.maxDepth, latestInput, output);
|
|
363
|
+
}
|
|
364
|
+
res.send = originalSend;
|
|
365
|
+
return originalSend.call(res, data);
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
next();
|
|
355
369
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request context — propagates a request ID through async call chains.
|
|
3
|
+
*
|
|
4
|
+
* Uses Node.js AsyncLocalStorage so that all functions, queries, and logs
|
|
5
|
+
* within a single HTTP request share the same requestId, enabling
|
|
6
|
+
* per-request tracing (like Jaeger but with trickle's richer data).
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* import { withRequestContext, getRequestId } from './request-context';
|
|
10
|
+
* // In Express middleware:
|
|
11
|
+
* app.use((req, res, next) => withRequestContext(req, next));
|
|
12
|
+
* // Anywhere in the call chain:
|
|
13
|
+
* const id = getRequestId(); // returns the current request's ID
|
|
14
|
+
*/
|
|
15
|
+
export interface RequestContext {
|
|
16
|
+
requestId: string;
|
|
17
|
+
method?: string;
|
|
18
|
+
path?: string;
|
|
19
|
+
startTime: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Run a callback within a request context.
|
|
23
|
+
*/
|
|
24
|
+
export declare function withRequestContext(req: any, callback: () => void): void;
|
|
25
|
+
/**
|
|
26
|
+
* Get the current request ID (if inside a request context).
|
|
27
|
+
*/
|
|
28
|
+
export declare function getRequestId(): string | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Get the full request context.
|
|
31
|
+
*/
|
|
32
|
+
export declare function getRequestContext(): RequestContext | undefined;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Request context — propagates a request ID through async call chains.
|
|
4
|
+
*
|
|
5
|
+
* Uses Node.js AsyncLocalStorage so that all functions, queries, and logs
|
|
6
|
+
* within a single HTTP request share the same requestId, enabling
|
|
7
|
+
* per-request tracing (like Jaeger but with trickle's richer data).
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* import { withRequestContext, getRequestId } from './request-context';
|
|
11
|
+
* // In Express middleware:
|
|
12
|
+
* app.use((req, res, next) => withRequestContext(req, next));
|
|
13
|
+
* // Anywhere in the call chain:
|
|
14
|
+
* const id = getRequestId(); // returns the current request's ID
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.withRequestContext = withRequestContext;
|
|
18
|
+
exports.getRequestId = getRequestId;
|
|
19
|
+
exports.getRequestContext = getRequestContext;
|
|
20
|
+
let als = null;
|
|
21
|
+
let counter = 0;
|
|
22
|
+
try {
|
|
23
|
+
const { AsyncLocalStorage } = require('async_hooks');
|
|
24
|
+
als = new AsyncLocalStorage();
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// AsyncLocalStorage not available (older Node versions)
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Run a callback within a request context.
|
|
31
|
+
*/
|
|
32
|
+
function withRequestContext(req, callback) {
|
|
33
|
+
if (!als) {
|
|
34
|
+
callback();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const ctx = {
|
|
38
|
+
requestId: `req-${++counter}-${Date.now().toString(36)}`,
|
|
39
|
+
method: req?.method,
|
|
40
|
+
path: req?.path || req?.url,
|
|
41
|
+
startTime: Date.now(),
|
|
42
|
+
};
|
|
43
|
+
als.run(ctx, callback);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Get the current request ID (if inside a request context).
|
|
47
|
+
*/
|
|
48
|
+
function getRequestId() {
|
|
49
|
+
if (!als)
|
|
50
|
+
return undefined;
|
|
51
|
+
const ctx = als.getStore();
|
|
52
|
+
return ctx?.requestId;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Get the full request context.
|
|
56
|
+
*/
|
|
57
|
+
function getRequestContext() {
|
|
58
|
+
if (!als)
|
|
59
|
+
return undefined;
|
|
60
|
+
return als.getStore();
|
|
61
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,160 @@
|
|
|
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
|
+
});
|
package/package.json
CHANGED
package/src/call-trace.ts
CHANGED
|
@@ -21,6 +21,7 @@ interface CallEvent {
|
|
|
21
21
|
timestamp: number;
|
|
22
22
|
durationMs: number;
|
|
23
23
|
error?: string;
|
|
24
|
+
requestId?: string;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
let traceFile: string | null = null;
|
|
@@ -71,6 +72,13 @@ export function traceReturn(
|
|
|
71
72
|
const parentId = callStack.length >= 2 ? callStack[callStack.length - 2] : 0;
|
|
72
73
|
const depth = callStack.length - 1;
|
|
73
74
|
|
|
75
|
+
// Get request ID from async context (if inside an Express request)
|
|
76
|
+
let requestId: string | undefined;
|
|
77
|
+
try {
|
|
78
|
+
const { getRequestId } = require('./request-context');
|
|
79
|
+
requestId = getRequestId();
|
|
80
|
+
} catch {}
|
|
81
|
+
|
|
74
82
|
writeEvent({
|
|
75
83
|
kind: 'call',
|
|
76
84
|
function: functionName,
|
|
@@ -81,6 +89,7 @@ export function traceReturn(
|
|
|
81
89
|
timestamp: Date.now(),
|
|
82
90
|
durationMs: Math.round(durationMs * 100) / 100,
|
|
83
91
|
...(error ? { error } : {}),
|
|
92
|
+
...(requestId ? { requestId } : {}),
|
|
84
93
|
});
|
|
85
94
|
|
|
86
95
|
// Pop from stack
|
package/src/db-observer.ts
CHANGED
|
@@ -21,6 +21,7 @@ interface QueryRecord {
|
|
|
21
21
|
columns?: string[];
|
|
22
22
|
error?: string;
|
|
23
23
|
timestamp: number;
|
|
24
|
+
requestId?: string;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
let queriesFile: string | null = null;
|
|
@@ -42,6 +43,12 @@ function getQueriesFile(): string {
|
|
|
42
43
|
function writeQuery(record: QueryRecord): void {
|
|
43
44
|
if (queryCount >= MAX_QUERIES) return;
|
|
44
45
|
queryCount++;
|
|
46
|
+
// Add request ID from async context
|
|
47
|
+
try {
|
|
48
|
+
const { getRequestId } = require('./request-context');
|
|
49
|
+
const reqId = getRequestId();
|
|
50
|
+
if (reqId) record.requestId = reqId;
|
|
51
|
+
} catch {}
|
|
45
52
|
try {
|
|
46
53
|
fs.appendFileSync(getQueriesFile(), JSON.stringify(record) + '\n');
|
|
47
54
|
} catch {}
|
package/src/express.ts
CHANGED
|
@@ -334,6 +334,22 @@ export function trickleMiddleware(
|
|
|
334
334
|
return;
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
+
// 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);
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function _handleRequest(req: any, res: any, next: any, opts: any, debug: boolean): void {
|
|
337
353
|
if (debug) {
|
|
338
354
|
console.log(`[trickle/middleware] Intercepting ${req.method} ${req.originalUrl || req.url}`);
|
|
339
355
|
}
|
|
@@ -396,5 +412,4 @@ export function trickleMiddleware(
|
|
|
396
412
|
}
|
|
397
413
|
|
|
398
414
|
next();
|
|
399
|
-
};
|
|
400
415
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request context — propagates a request ID through async call chains.
|
|
3
|
+
*
|
|
4
|
+
* Uses Node.js AsyncLocalStorage so that all functions, queries, and logs
|
|
5
|
+
* within a single HTTP request share the same requestId, enabling
|
|
6
|
+
* per-request tracing (like Jaeger but with trickle's richer data).
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* import { withRequestContext, getRequestId } from './request-context';
|
|
10
|
+
* // In Express middleware:
|
|
11
|
+
* app.use((req, res, next) => withRequestContext(req, next));
|
|
12
|
+
* // Anywhere in the call chain:
|
|
13
|
+
* const id = getRequestId(); // returns the current request's ID
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
let als: any = null;
|
|
17
|
+
let counter = 0;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const { AsyncLocalStorage } = require('async_hooks');
|
|
21
|
+
als = new AsyncLocalStorage();
|
|
22
|
+
} catch {
|
|
23
|
+
// AsyncLocalStorage not available (older Node versions)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface RequestContext {
|
|
27
|
+
requestId: string;
|
|
28
|
+
method?: string;
|
|
29
|
+
path?: string;
|
|
30
|
+
startTime: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Run a callback within a request context.
|
|
35
|
+
*/
|
|
36
|
+
export function withRequestContext(req: any, callback: () => void): void {
|
|
37
|
+
if (!als) { callback(); return; }
|
|
38
|
+
|
|
39
|
+
const ctx: RequestContext = {
|
|
40
|
+
requestId: `req-${++counter}-${Date.now().toString(36)}`,
|
|
41
|
+
method: req?.method,
|
|
42
|
+
path: req?.path || req?.url,
|
|
43
|
+
startTime: Date.now(),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
als.run(ctx, callback);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get the current request ID (if inside a request context).
|
|
51
|
+
*/
|
|
52
|
+
export function getRequestId(): string | undefined {
|
|
53
|
+
if (!als) return undefined;
|
|
54
|
+
const ctx = als.getStore() as RequestContext | undefined;
|
|
55
|
+
return ctx?.requestId;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get the full request context.
|
|
60
|
+
*/
|
|
61
|
+
export function getRequestContext(): RequestContext | undefined {
|
|
62
|
+
if (!als) return undefined;
|
|
63
|
+
return als.getStore() as RequestContext | undefined;
|
|
64
|
+
}
|