trickle-observe 0.2.113 → 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.
@@ -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) {
@@ -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
- if (debug) {
297
- console.log(`[trickle/middleware] Intercepting ${req.method} ${req.originalUrl || req.url}`);
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
- let captured = false;
300
- // We derive the route name lazily once the response is being sent,
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
- const input = extractRequestInput(req);
314
- // Intercept res.json()
315
- const originalJson = res.json;
316
- if (typeof originalJson === 'function') {
317
- res.json = function (data) {
318
- if (!captured) {
319
- captured = true;
320
- const routeName = getRouteName();
321
- if (debug) {
322
- console.log(`[trickle/middleware] Captured res.json for ${routeName}`);
323
- }
324
- // Re-extract input here because body parsers may have run since middleware was entered
325
- const latestInput = extractRequestInput(req);
326
- emitExpressPayload(routeName, opts.environment, opts.maxDepth, latestInput, data);
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
- else if (debug) {
333
- console.log(`[trickle/middleware] res.json is not a function: ${typeof originalJson}`);
323
+ catch {
324
+ // ignore
334
325
  }
335
- // Intercept res.send()
336
- const originalSend = res.send;
337
- if (typeof originalSend === 'function') {
338
- res.send = function (data) {
339
- if (!captured) {
340
- captured = true;
341
- const routeName = getRouteName();
342
- if (debug) {
343
- console.log(`[trickle/middleware] Captured res.send for ${routeName}`);
344
- }
345
- const latestInput = extractRequestInput(req);
346
- const output = typeof data === 'string' ? { __html: true } : data;
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
- res.send = originalSend;
350
- return originalSend.call(res, data);
351
- };
352
- }
353
- next();
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
+ }
@@ -1726,8 +1726,12 @@ ingestUrl) {
1726
1726
  const catchInsertions = traceVars ? findCatchVars(source) : [];
1727
1727
  // Find function parameter names for tracing
1728
1728
  const funcParamInsertions = traceVars ? findFunctionParams(source, isReactFile) : [];
1729
- // Find JSX text expressions for tracing (React files only)
1730
- const jsxExprInsertions = (traceVars && isReactFile) ? findJsxExpressions(source) : [];
1729
+ // Find JSX text expressions for tracing (React files only).
1730
+ // Skip if JSX has already been compiled to _jsxDEV/jsx/jsxs calls (e.g. by Vite's React plugin).
1731
+ // In that case, the `{` characters in the source are plain JS (function bodies, object literals)
1732
+ // and findJsxExpressions would corrupt them by injecting __trickle_tv() calls.
1733
+ const jsxAlreadyCompiled = /\b_?jsxDEV\b|\bjsxs?\s*\(/.test(source);
1734
+ const jsxExprInsertions = (traceVars && isReactFile && !jsxAlreadyCompiled) ? findJsxExpressions(source) : [];
1731
1735
  if (funcInsertions.length === 0 && importInsertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && reassignInsertions.length === 0 && forLoopInsertions.length === 0 && catchInsertions.length === 0 && funcParamInsertions.length === 0 && jsxExprInsertions.length === 0 && bodyInsertions.length === 0 && hookInsertions.length === 0 && stateInsertions.length === 0 && conciseBodyInsertions.length === 0)
1732
1736
  return source;
1733
1737
  // Fix line numbers: Vite transforms (TypeScript stripping) may change line numbers.
@@ -1713,8 +1713,12 @@ ingestUrl) {
1713
1713
  const catchInsertions = traceVars ? findCatchVars(source) : [];
1714
1714
  // Find function parameter names for tracing
1715
1715
  const funcParamInsertions = traceVars ? findFunctionParams(source, isReactFile) : [];
1716
- // Find JSX text expressions for tracing (React files only)
1717
- const jsxExprInsertions = (traceVars && isReactFile) ? findJsxExpressions(source) : [];
1716
+ // Find JSX text expressions for tracing (React files only).
1717
+ // Skip if JSX has already been compiled to _jsxDEV/jsx/jsxs calls (e.g. by Vite's React plugin).
1718
+ // In that case, the `{` characters in the source are plain JS (function bodies, object literals)
1719
+ // and findJsxExpressions would corrupt them by injecting __trickle_tv() calls.
1720
+ const jsxAlreadyCompiled = /\b_?jsxDEV\b|\bjsxs?\s*\(/.test(source);
1721
+ const jsxExprInsertions = (traceVars && isReactFile && !jsxAlreadyCompiled) ? findJsxExpressions(source) : [];
1718
1722
  if (funcInsertions.length === 0 && importInsertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && reassignInsertions.length === 0 && forLoopInsertions.length === 0 && catchInsertions.length === 0 && funcParamInsertions.length === 0 && jsxExprInsertions.length === 0 && bodyInsertions.length === 0 && hookInsertions.length === 0 && stateInsertions.length === 0 && conciseBodyInsertions.length === 0)
1719
1723
  return source;
1720
1724
  // Fix line numbers: Vite transforms (TypeScript stripping) may change line numbers.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-observe",
3
- "version": "0.2.113",
3
+ "version": "0.2.115",
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
@@ -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
@@ -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
+ }
@@ -1658,8 +1658,12 @@ export function transformEsmSource(
1658
1658
  // Find function parameter names for tracing
1659
1659
  const funcParamInsertions = traceVars ? findFunctionParams(source, isReactFile) : [];
1660
1660
 
1661
- // Find JSX text expressions for tracing (React files only)
1662
- const jsxExprInsertions = (traceVars && isReactFile) ? findJsxExpressions(source) : [];
1661
+ // Find JSX text expressions for tracing (React files only).
1662
+ // Skip if JSX has already been compiled to _jsxDEV/jsx/jsxs calls (e.g. by Vite's React plugin).
1663
+ // In that case, the `{` characters in the source are plain JS (function bodies, object literals)
1664
+ // and findJsxExpressions would corrupt them by injecting __trickle_tv() calls.
1665
+ const jsxAlreadyCompiled = /\b_?jsxDEV\b|\bjsxs?\s*\(/.test(source);
1666
+ const jsxExprInsertions = (traceVars && isReactFile && !jsxAlreadyCompiled) ? findJsxExpressions(source) : [];
1663
1667
 
1664
1668
  if (funcInsertions.length === 0 && importInsertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && reassignInsertions.length === 0 && forLoopInsertions.length === 0 && catchInsertions.length === 0 && funcParamInsertions.length === 0 && jsxExprInsertions.length === 0 && bodyInsertions.length === 0 && hookInsertions.length === 0 && stateInsertions.length === 0 && conciseBodyInsertions.length === 0) return source;
1665
1669