trickle-observe 0.1.0
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/auto-env.js +13 -0
- package/auto-esm.mjs +128 -0
- package/auto.js +3 -0
- package/dist/auto-codegen.d.ts +29 -0
- package/dist/auto-codegen.js +999 -0
- package/dist/auto-register.d.ts +16 -0
- package/dist/auto-register.js +99 -0
- package/dist/cache.d.ts +27 -0
- package/dist/cache.js +52 -0
- package/dist/env-detect.d.ts +5 -0
- package/dist/env-detect.js +35 -0
- package/dist/express.d.ts +44 -0
- package/dist/express.js +342 -0
- package/dist/fetch-observer.d.ts +24 -0
- package/dist/fetch-observer.js +217 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +172 -0
- package/dist/observe-register.d.ts +29 -0
- package/dist/observe-register.js +455 -0
- package/dist/observe.d.ts +44 -0
- package/dist/observe.js +109 -0
- package/dist/proxy-tracker.d.ts +15 -0
- package/dist/proxy-tracker.js +172 -0
- package/dist/register.d.ts +21 -0
- package/dist/register.js +105 -0
- package/dist/transport.d.ts +22 -0
- package/dist/transport.js +228 -0
- package/dist/type-hash.d.ts +5 -0
- package/dist/type-hash.js +60 -0
- package/dist/type-inference.d.ts +14 -0
- package/dist/type-inference.js +259 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.js +2 -0
- package/dist/wrap.d.ts +10 -0
- package/dist/wrap.js +247 -0
- package/observe-esm-hooks.mjs +367 -0
- package/observe-esm.mjs +40 -0
- package/observe.js +2 -0
- package/package.json +26 -0
- package/register.js +2 -0
- package/src/auto-codegen.ts +1058 -0
- package/src/auto-register.ts +102 -0
- package/src/cache.ts +53 -0
- package/src/env-detect.ts +22 -0
- package/src/express.ts +386 -0
- package/src/fetch-observer.ts +226 -0
- package/src/index.ts +199 -0
- package/src/observe-register.ts +453 -0
- package/src/observe.ts +127 -0
- package/src/proxy-tracker.ts +208 -0
- package/src/register.ts +110 -0
- package/src/transport.ts +207 -0
- package/src/type-hash.ts +71 -0
- package/src/type-inference.ts +285 -0
- package/src/types.ts +61 -0
- package/src/wrap.ts +289 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* trickle/auto — Zero-config type generation.
|
|
3
|
+
*
|
|
4
|
+
* Add ONE LINE to your app and types appear automatically:
|
|
5
|
+
*
|
|
6
|
+
* require('trickle/auto');
|
|
7
|
+
*
|
|
8
|
+
* This module:
|
|
9
|
+
* 1. Forces local mode (no backend needed)
|
|
10
|
+
* 2. Activates the observe-register hooks (instruments all user functions)
|
|
11
|
+
* 3. Runs a background timer that generates .d.ts files from observations
|
|
12
|
+
* 4. On process exit, does a final type generation
|
|
13
|
+
*
|
|
14
|
+
* No CLI. No backend. No configuration. Just types.
|
|
15
|
+
*/
|
|
16
|
+
import './observe-register';
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* trickle/auto — Zero-config type generation.
|
|
4
|
+
*
|
|
5
|
+
* Add ONE LINE to your app and types appear automatically:
|
|
6
|
+
*
|
|
7
|
+
* require('trickle/auto');
|
|
8
|
+
*
|
|
9
|
+
* This module:
|
|
10
|
+
* 1. Forces local mode (no backend needed)
|
|
11
|
+
* 2. Activates the observe-register hooks (instruments all user functions)
|
|
12
|
+
* 3. Runs a background timer that generates .d.ts files from observations
|
|
13
|
+
* 4. On process exit, does a final type generation
|
|
14
|
+
*
|
|
15
|
+
* No CLI. No backend. No configuration. Just types.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
// Force local mode BEFORE importing observe-register (which calls configure)
|
|
19
|
+
process.env.TRICKLE_LOCAL = '1';
|
|
20
|
+
// Import the observe-register hooks (instruments all functions)
|
|
21
|
+
require("./observe-register");
|
|
22
|
+
// Import the auto codegen
|
|
23
|
+
const auto_codegen_1 = require("./auto-codegen");
|
|
24
|
+
const debug = process.env.TRICKLE_DEBUG === '1' || process.env.TRICKLE_DEBUG === 'true';
|
|
25
|
+
let lastFunctionCount = 0;
|
|
26
|
+
let generationCount = 0;
|
|
27
|
+
/**
|
|
28
|
+
* Run type generation and optionally log results.
|
|
29
|
+
*/
|
|
30
|
+
function runGeneration(isFinal) {
|
|
31
|
+
try {
|
|
32
|
+
const count = (0, auto_codegen_1.generateTypes)();
|
|
33
|
+
if (count === -1)
|
|
34
|
+
return; // no change
|
|
35
|
+
if (count > 0) {
|
|
36
|
+
generationCount++;
|
|
37
|
+
if (debug || (count > lastFunctionCount)) {
|
|
38
|
+
const newTypes = count - lastFunctionCount;
|
|
39
|
+
if (newTypes > 0 && generationCount > 1) {
|
|
40
|
+
// Only log after first generation (avoid noise on startup)
|
|
41
|
+
console.log(`[trickle/auto] +${newTypes} type(s) generated (${count} total)`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
lastFunctionCount = count;
|
|
45
|
+
}
|
|
46
|
+
if (isFinal && lastFunctionCount > 0) {
|
|
47
|
+
console.log(`[trickle/auto] ${lastFunctionCount} function type(s) written to .d.ts`);
|
|
48
|
+
// Inject JSDoc into source files if TRICKLE_INJECT=1
|
|
49
|
+
try {
|
|
50
|
+
const injected = (0, auto_codegen_1.injectTypes)();
|
|
51
|
+
if (injected > 0) {
|
|
52
|
+
console.log(`[trickle/auto] ${injected} function(s) annotated with JSDoc in source`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch { /* don't crash */ }
|
|
56
|
+
// Print coverage report if TRICKLE_COVERAGE=1
|
|
57
|
+
try {
|
|
58
|
+
const report = (0, auto_codegen_1.generateCoverageReport)();
|
|
59
|
+
if (report)
|
|
60
|
+
console.log(report);
|
|
61
|
+
}
|
|
62
|
+
catch { /* don't crash */ }
|
|
63
|
+
// Print type summary if TRICKLE_SUMMARY=1
|
|
64
|
+
try {
|
|
65
|
+
const summary = (0, auto_codegen_1.generateTypeSummary)();
|
|
66
|
+
if (summary)
|
|
67
|
+
console.log(summary);
|
|
68
|
+
}
|
|
69
|
+
catch { /* don't crash */ }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Never crash user's app
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Background timer — regenerate types every 3 seconds
|
|
77
|
+
const timer = setInterval(() => runGeneration(false), 3000);
|
|
78
|
+
// Don't keep the process alive just for type generation
|
|
79
|
+
if (timer && typeof timer === 'object' && 'unref' in timer) {
|
|
80
|
+
timer.unref();
|
|
81
|
+
}
|
|
82
|
+
// Also do a first check after 1 second
|
|
83
|
+
const initialTimer = setTimeout(() => runGeneration(false), 1000);
|
|
84
|
+
if (initialTimer && typeof initialTimer === 'object' && 'unref' in initialTimer) {
|
|
85
|
+
initialTimer.unref();
|
|
86
|
+
}
|
|
87
|
+
// Final generation on exit
|
|
88
|
+
if (typeof process !== 'undefined' && process.on) {
|
|
89
|
+
process.on('beforeExit', () => {
|
|
90
|
+
runGeneration(true);
|
|
91
|
+
});
|
|
92
|
+
// On SIGTERM/SIGINT, do final generation
|
|
93
|
+
const exitHandler = () => {
|
|
94
|
+
clearInterval(timer);
|
|
95
|
+
runGeneration(true);
|
|
96
|
+
};
|
|
97
|
+
process.on('SIGTERM', exitHandler);
|
|
98
|
+
process.on('SIGINT', exitHandler);
|
|
99
|
+
}
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory cache to avoid sending redundant type data to the backend.
|
|
3
|
+
* Keyed by function identity (name + module), stores the last type hash sent.
|
|
4
|
+
* Re-sends if the hash changes or if the entry becomes stale.
|
|
5
|
+
*/
|
|
6
|
+
export declare class TypeCache {
|
|
7
|
+
private cache;
|
|
8
|
+
private maxStalenessMs;
|
|
9
|
+
constructor(maxStalenessMs?: number);
|
|
10
|
+
/**
|
|
11
|
+
* Returns true if this type observation should be sent to the backend.
|
|
12
|
+
* Returns false if we recently sent the same hash for this function.
|
|
13
|
+
*/
|
|
14
|
+
shouldSend(functionKey: string, hash: string): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Mark that we just sent data for this function with this hash.
|
|
17
|
+
*/
|
|
18
|
+
markSent(functionKey: string, hash: string): void;
|
|
19
|
+
/**
|
|
20
|
+
* Clear the cache (useful for testing or reconfiguration).
|
|
21
|
+
*/
|
|
22
|
+
clear(): void;
|
|
23
|
+
/**
|
|
24
|
+
* Get the number of cached entries.
|
|
25
|
+
*/
|
|
26
|
+
get size(): number;
|
|
27
|
+
}
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TypeCache = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* In-memory cache to avoid sending redundant type data to the backend.
|
|
6
|
+
* Keyed by function identity (name + module), stores the last type hash sent.
|
|
7
|
+
* Re-sends if the hash changes or if the entry becomes stale.
|
|
8
|
+
*/
|
|
9
|
+
class TypeCache {
|
|
10
|
+
cache;
|
|
11
|
+
maxStalenessMs;
|
|
12
|
+
constructor(maxStalenessMs = 5 * 60 * 1000) {
|
|
13
|
+
this.cache = new Map();
|
|
14
|
+
this.maxStalenessMs = maxStalenessMs;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Returns true if this type observation should be sent to the backend.
|
|
18
|
+
* Returns false if we recently sent the same hash for this function.
|
|
19
|
+
*/
|
|
20
|
+
shouldSend(functionKey, hash) {
|
|
21
|
+
const entry = this.cache.get(functionKey);
|
|
22
|
+
if (!entry)
|
|
23
|
+
return true;
|
|
24
|
+
// Different type shape — always send
|
|
25
|
+
if (entry.hash !== hash)
|
|
26
|
+
return true;
|
|
27
|
+
// Same shape but stale — re-send to confirm it's still alive
|
|
28
|
+
const age = Date.now() - entry.lastSentAt;
|
|
29
|
+
if (age > this.maxStalenessMs)
|
|
30
|
+
return true;
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Mark that we just sent data for this function with this hash.
|
|
35
|
+
*/
|
|
36
|
+
markSent(functionKey, hash) {
|
|
37
|
+
this.cache.set(functionKey, { hash, lastSentAt: Date.now() });
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Clear the cache (useful for testing or reconfiguration).
|
|
41
|
+
*/
|
|
42
|
+
clear() {
|
|
43
|
+
this.cache.clear();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Get the number of cached entries.
|
|
47
|
+
*/
|
|
48
|
+
get size() {
|
|
49
|
+
return this.cache.size;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exports.TypeCache = TypeCache;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.detectEnvironment = detectEnvironment;
|
|
4
|
+
/**
|
|
5
|
+
* Detect the runtime environment from environment variables.
|
|
6
|
+
* Returns a human-readable string identifying where this code is running.
|
|
7
|
+
*/
|
|
8
|
+
function detectEnvironment() {
|
|
9
|
+
const env = typeof process !== 'undefined' && process.env ? process.env : {};
|
|
10
|
+
if (env.AWS_LAMBDA_FUNCTION_NAME)
|
|
11
|
+
return 'lambda';
|
|
12
|
+
if (env.VERCEL)
|
|
13
|
+
return 'vercel';
|
|
14
|
+
if (env.RAILWAY_ENVIRONMENT)
|
|
15
|
+
return 'railway';
|
|
16
|
+
if (env.K_SERVICE)
|
|
17
|
+
return 'cloud-run';
|
|
18
|
+
if (env.AZURE_FUNCTIONS_ENVIRONMENT)
|
|
19
|
+
return 'azure-functions';
|
|
20
|
+
if (env.GOOGLE_CLOUD_PROJECT)
|
|
21
|
+
return 'gcp';
|
|
22
|
+
if (env.ECS_CONTAINER_METADATA_URI)
|
|
23
|
+
return 'ecs';
|
|
24
|
+
if (env.FLY_APP_NAME)
|
|
25
|
+
return 'fly';
|
|
26
|
+
if (env.RENDER_SERVICE_ID)
|
|
27
|
+
return 'render';
|
|
28
|
+
if (env.HEROKU_APP_NAME || env.DYNO)
|
|
29
|
+
return 'heroku';
|
|
30
|
+
if (env.CODESPACE_NAME)
|
|
31
|
+
return 'codespace';
|
|
32
|
+
if (env.GITPOD_WORKSPACE_ID)
|
|
33
|
+
return 'gitpod';
|
|
34
|
+
return 'node';
|
|
35
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instrument an Express application by monkey-patching route registration methods.
|
|
3
|
+
*
|
|
4
|
+
* Must be called BEFORE routes are defined:
|
|
5
|
+
*
|
|
6
|
+
* import express from 'express';
|
|
7
|
+
* import { instrumentExpress } from 'trickle';
|
|
8
|
+
*
|
|
9
|
+
* const app = express();
|
|
10
|
+
* instrumentExpress(app);
|
|
11
|
+
*
|
|
12
|
+
* app.get('/api/users', (req, res) => { ... });
|
|
13
|
+
*
|
|
14
|
+
* Each registered handler is wrapped to capture:
|
|
15
|
+
* - Input: `{ body, params, query }` from the request
|
|
16
|
+
* - Output: the data passed to `res.json()` or `res.send()`
|
|
17
|
+
* - Errors: exceptions or `next(err)` calls
|
|
18
|
+
*/
|
|
19
|
+
export declare function instrumentExpress(app: any, userOpts?: {
|
|
20
|
+
enabled?: boolean;
|
|
21
|
+
environment?: string;
|
|
22
|
+
sampleRate?: number;
|
|
23
|
+
maxDepth?: number;
|
|
24
|
+
}): void;
|
|
25
|
+
/**
|
|
26
|
+
* Express middleware that intercepts responses to capture type information.
|
|
27
|
+
*
|
|
28
|
+
* Use this when you prefer middleware over monkey-patching:
|
|
29
|
+
*
|
|
30
|
+
* import express from 'express';
|
|
31
|
+
* import { trickleMiddleware } from 'trickle';
|
|
32
|
+
*
|
|
33
|
+
* const app = express();
|
|
34
|
+
* app.use(trickleMiddleware());
|
|
35
|
+
*
|
|
36
|
+
* The middleware captures the route `METHOD /path` once the response is sent,
|
|
37
|
+
* by intercepting `res.json()` and `res.send()`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function trickleMiddleware(userOpts?: {
|
|
40
|
+
enabled?: boolean;
|
|
41
|
+
environment?: string;
|
|
42
|
+
sampleRate?: number;
|
|
43
|
+
maxDepth?: number;
|
|
44
|
+
}): (req: any, res: any, next: (...args: any[]) => void) => void;
|
package/dist/express.js
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.instrumentExpress = instrumentExpress;
|
|
4
|
+
exports.trickleMiddleware = trickleMiddleware;
|
|
5
|
+
const type_inference_1 = require("./type-inference");
|
|
6
|
+
const type_hash_1 = require("./type-hash");
|
|
7
|
+
const cache_1 = require("./cache");
|
|
8
|
+
const transport_1 = require("./transport");
|
|
9
|
+
const env_detect_1 = require("./env-detect");
|
|
10
|
+
const expressCache = new cache_1.TypeCache();
|
|
11
|
+
/**
|
|
12
|
+
* Extract the interesting parts of an Express request as a plain object
|
|
13
|
+
* suitable for type inference. We deliberately avoid the full req object
|
|
14
|
+
* because it is enormous and contains circular references.
|
|
15
|
+
*/
|
|
16
|
+
function extractRequestInput(req) {
|
|
17
|
+
const input = {};
|
|
18
|
+
try {
|
|
19
|
+
if (req.body !== undefined && req.body !== null && Object.keys(req.body).length > 0) {
|
|
20
|
+
input.body = req.body;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// body might not be readable
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
if (req.params && Object.keys(req.params).length > 0) {
|
|
28
|
+
input.params = req.params;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// ignore
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
if (req.query && Object.keys(req.query).length > 0) {
|
|
36
|
+
input.query = req.query;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// ignore
|
|
41
|
+
}
|
|
42
|
+
return input;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Sanitize a sample value for safe serialization (local copy to avoid circular import).
|
|
46
|
+
*/
|
|
47
|
+
function sanitizeSample(value, depth = 3) {
|
|
48
|
+
if (depth <= 0)
|
|
49
|
+
return '[truncated]';
|
|
50
|
+
if (value === null || value === undefined)
|
|
51
|
+
return value;
|
|
52
|
+
const t = typeof value;
|
|
53
|
+
if (t === 'string') {
|
|
54
|
+
const s = value;
|
|
55
|
+
return s.length > 200 ? s.substring(0, 200) + '...' : s;
|
|
56
|
+
}
|
|
57
|
+
if (t === 'number' || t === 'boolean')
|
|
58
|
+
return value;
|
|
59
|
+
if (t === 'bigint')
|
|
60
|
+
return String(value);
|
|
61
|
+
if (t === 'symbol')
|
|
62
|
+
return String(value);
|
|
63
|
+
if (t === 'function')
|
|
64
|
+
return `[Function: ${value.name || 'anonymous'}]`;
|
|
65
|
+
if (Array.isArray(value)) {
|
|
66
|
+
return value.slice(0, 5).map(item => sanitizeSample(item, depth - 1));
|
|
67
|
+
}
|
|
68
|
+
if (t === 'object') {
|
|
69
|
+
if (value instanceof Date)
|
|
70
|
+
return value.toISOString();
|
|
71
|
+
if (value instanceof RegExp)
|
|
72
|
+
return String(value);
|
|
73
|
+
if (value instanceof Error)
|
|
74
|
+
return { error: value.message };
|
|
75
|
+
if (value instanceof Map)
|
|
76
|
+
return `[Map: ${value.size} entries]`;
|
|
77
|
+
if (value instanceof Set)
|
|
78
|
+
return `[Set: ${value.size} items]`;
|
|
79
|
+
const obj = value;
|
|
80
|
+
const result = {};
|
|
81
|
+
const keys = Object.keys(obj).slice(0, 20);
|
|
82
|
+
for (const key of keys) {
|
|
83
|
+
try {
|
|
84
|
+
result[key] = sanitizeSample(obj[key], depth - 1);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
result[key] = '[unreadable]';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
return String(value);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Emit a type payload for a single Express route invocation.
|
|
96
|
+
*/
|
|
97
|
+
function emitExpressPayload(functionName, environment, maxDepth, input, output, error) {
|
|
98
|
+
try {
|
|
99
|
+
const functionKey = `express::${functionName}`;
|
|
100
|
+
const argsType = (0, type_inference_1.inferType)(input, maxDepth);
|
|
101
|
+
const returnType = error ? { kind: 'unknown' } : (0, type_inference_1.inferType)(output, maxDepth);
|
|
102
|
+
const hash = (0, type_hash_1.hashType)(argsType, returnType);
|
|
103
|
+
// For errors, always send. For success, use cache.
|
|
104
|
+
if (!error && !expressCache.shouldSend(functionKey, hash)) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!error) {
|
|
108
|
+
expressCache.markSent(functionKey, hash);
|
|
109
|
+
}
|
|
110
|
+
const payload = {
|
|
111
|
+
functionName,
|
|
112
|
+
module: 'express',
|
|
113
|
+
language: 'js',
|
|
114
|
+
environment,
|
|
115
|
+
typeHash: hash,
|
|
116
|
+
argsType,
|
|
117
|
+
returnType,
|
|
118
|
+
sampleInput: sanitizeSample(input),
|
|
119
|
+
sampleOutput: error ? undefined : sanitizeSample(output),
|
|
120
|
+
};
|
|
121
|
+
if (error) {
|
|
122
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
123
|
+
payload.error = {
|
|
124
|
+
type: err.constructor?.name || 'Error',
|
|
125
|
+
message: err.message,
|
|
126
|
+
stackTrace: err.stack,
|
|
127
|
+
argsSnapshot: sanitizeSample(input),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
(0, transport_1.enqueue)(payload);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Never crash the user's app
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Wrap a single Express route handler so that it captures request input
|
|
138
|
+
* (body, params, query) and response output (json/send payloads) as type data.
|
|
139
|
+
*/
|
|
140
|
+
function wrapExpressHandler(handler, routeName, opts) {
|
|
141
|
+
const wrapped = function (req, res, next) {
|
|
142
|
+
// Sample rate check
|
|
143
|
+
if (opts.sampleRate < 1 && Math.random() > opts.sampleRate) {
|
|
144
|
+
return handler.call(this, req, res, next);
|
|
145
|
+
}
|
|
146
|
+
const input = extractRequestInput(req);
|
|
147
|
+
let captured = false;
|
|
148
|
+
// Intercept res.json()
|
|
149
|
+
const originalJson = res.json;
|
|
150
|
+
if (typeof originalJson === 'function') {
|
|
151
|
+
res.json = function (data) {
|
|
152
|
+
if (!captured) {
|
|
153
|
+
captured = true;
|
|
154
|
+
emitExpressPayload(routeName, opts.environment, opts.maxDepth, input, data);
|
|
155
|
+
}
|
|
156
|
+
res.json = originalJson; // restore
|
|
157
|
+
return originalJson.call(res, data);
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
// Intercept res.send()
|
|
161
|
+
const originalSend = res.send;
|
|
162
|
+
if (typeof originalSend === 'function') {
|
|
163
|
+
res.send = function (data) {
|
|
164
|
+
if (!captured) {
|
|
165
|
+
captured = true;
|
|
166
|
+
// Only capture non-string data as typed output; strings are usually HTML
|
|
167
|
+
const output = typeof data === 'string' ? { __html: true } : data;
|
|
168
|
+
emitExpressPayload(routeName, opts.environment, opts.maxDepth, input, output);
|
|
169
|
+
}
|
|
170
|
+
res.send = originalSend; // restore
|
|
171
|
+
return originalSend.call(res, data);
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
// Wrap next to capture errors passed via next(err)
|
|
175
|
+
const wrappedNext = function (err) {
|
|
176
|
+
if (err && !captured) {
|
|
177
|
+
captured = true;
|
|
178
|
+
emitExpressPayload(routeName, opts.environment, opts.maxDepth, input, undefined, err);
|
|
179
|
+
}
|
|
180
|
+
if (typeof next === 'function') {
|
|
181
|
+
return next(err);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
try {
|
|
185
|
+
const result = handler.call(this, req, res, wrappedNext);
|
|
186
|
+
// Handle async handlers that return a promise
|
|
187
|
+
if (result && typeof result === 'object' && typeof result.then === 'function') {
|
|
188
|
+
return result.catch((err) => {
|
|
189
|
+
if (!captured) {
|
|
190
|
+
captured = true;
|
|
191
|
+
emitExpressPayload(routeName, opts.environment, opts.maxDepth, input, undefined, err);
|
|
192
|
+
}
|
|
193
|
+
// Re-throw so Express error handling picks it up
|
|
194
|
+
throw err;
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
if (!captured) {
|
|
201
|
+
captured = true;
|
|
202
|
+
emitExpressPayload(routeName, opts.environment, opts.maxDepth, input, undefined, err);
|
|
203
|
+
}
|
|
204
|
+
throw err;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
// Preserve function metadata
|
|
208
|
+
Object.defineProperty(wrapped, 'name', { value: handler.name || routeName, configurable: true });
|
|
209
|
+
Object.defineProperty(wrapped, 'length', { value: handler.length, configurable: true });
|
|
210
|
+
return wrapped;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Instrument an Express application by monkey-patching route registration methods.
|
|
214
|
+
*
|
|
215
|
+
* Must be called BEFORE routes are defined:
|
|
216
|
+
*
|
|
217
|
+
* import express from 'express';
|
|
218
|
+
* import { instrumentExpress } from 'trickle';
|
|
219
|
+
*
|
|
220
|
+
* const app = express();
|
|
221
|
+
* instrumentExpress(app);
|
|
222
|
+
*
|
|
223
|
+
* app.get('/api/users', (req, res) => { ... });
|
|
224
|
+
*
|
|
225
|
+
* Each registered handler is wrapped to capture:
|
|
226
|
+
* - Input: `{ body, params, query }` from the request
|
|
227
|
+
* - Output: the data passed to `res.json()` or `res.send()`
|
|
228
|
+
* - Errors: exceptions or `next(err)` calls
|
|
229
|
+
*/
|
|
230
|
+
function instrumentExpress(app, userOpts) {
|
|
231
|
+
const opts = {
|
|
232
|
+
enabled: userOpts?.enabled !== false,
|
|
233
|
+
environment: userOpts?.environment || (0, env_detect_1.detectEnvironment)(),
|
|
234
|
+
sampleRate: userOpts?.sampleRate ?? 1,
|
|
235
|
+
maxDepth: userOpts?.maxDepth ?? 5,
|
|
236
|
+
};
|
|
237
|
+
if (!opts.enabled)
|
|
238
|
+
return;
|
|
239
|
+
const methods = ['get', 'post', 'put', 'delete', 'patch', 'all'];
|
|
240
|
+
for (const method of methods) {
|
|
241
|
+
const original = app[method];
|
|
242
|
+
if (typeof original !== 'function')
|
|
243
|
+
continue;
|
|
244
|
+
app[method] = function (path, ...handlers) {
|
|
245
|
+
// Express allows non-string first args (RegExp, array of paths, etc.)
|
|
246
|
+
// We only label with a string path; otherwise fall back to the method name.
|
|
247
|
+
const pathStr = typeof path === 'string' ? path : String(path);
|
|
248
|
+
const routeName = `${method.toUpperCase()} ${pathStr}`;
|
|
249
|
+
const wrapped = handlers.map((handler) => {
|
|
250
|
+
if (typeof handler !== 'function')
|
|
251
|
+
return handler;
|
|
252
|
+
try {
|
|
253
|
+
return wrapExpressHandler(handler, routeName, opts);
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
// If wrapping fails for any reason, return the original handler
|
|
257
|
+
return handler;
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
return original.call(this, path, ...wrapped);
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Express middleware that intercepts responses to capture type information.
|
|
266
|
+
*
|
|
267
|
+
* Use this when you prefer middleware over monkey-patching:
|
|
268
|
+
*
|
|
269
|
+
* import express from 'express';
|
|
270
|
+
* import { trickleMiddleware } from 'trickle';
|
|
271
|
+
*
|
|
272
|
+
* const app = express();
|
|
273
|
+
* app.use(trickleMiddleware());
|
|
274
|
+
*
|
|
275
|
+
* The middleware captures the route `METHOD /path` once the response is sent,
|
|
276
|
+
* by intercepting `res.json()` and `res.send()`.
|
|
277
|
+
*/
|
|
278
|
+
function trickleMiddleware(userOpts) {
|
|
279
|
+
const opts = {
|
|
280
|
+
enabled: userOpts?.enabled !== false,
|
|
281
|
+
environment: userOpts?.environment || (0, env_detect_1.detectEnvironment)(),
|
|
282
|
+
sampleRate: userOpts?.sampleRate ?? 1,
|
|
283
|
+
maxDepth: userOpts?.maxDepth ?? 5,
|
|
284
|
+
};
|
|
285
|
+
return function trickleMiddlewareHandler(req, res, next) {
|
|
286
|
+
if (!opts.enabled) {
|
|
287
|
+
next();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
// Sample rate check
|
|
291
|
+
if (opts.sampleRate < 1 && Math.random() > opts.sampleRate) {
|
|
292
|
+
next();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
let captured = false;
|
|
296
|
+
// We derive the route name lazily once the response is being sent,
|
|
297
|
+
// because req.route is only populated after the handler matches.
|
|
298
|
+
function getRouteName() {
|
|
299
|
+
try {
|
|
300
|
+
if (req.route && req.route.path) {
|
|
301
|
+
return `${req.method} ${req.baseUrl || ''}${req.route.path}`;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
// ignore
|
|
306
|
+
}
|
|
307
|
+
return `${req.method || 'UNKNOWN'} ${req.originalUrl || req.url || '/'}`;
|
|
308
|
+
}
|
|
309
|
+
const input = extractRequestInput(req);
|
|
310
|
+
// Intercept res.json()
|
|
311
|
+
const originalJson = res.json;
|
|
312
|
+
if (typeof originalJson === 'function') {
|
|
313
|
+
res.json = function (data) {
|
|
314
|
+
if (!captured) {
|
|
315
|
+
captured = true;
|
|
316
|
+
const routeName = getRouteName();
|
|
317
|
+
// Re-extract input here because body parsers may have run since middleware was entered
|
|
318
|
+
const latestInput = extractRequestInput(req);
|
|
319
|
+
emitExpressPayload(routeName, opts.environment, opts.maxDepth, latestInput, data);
|
|
320
|
+
}
|
|
321
|
+
res.json = originalJson;
|
|
322
|
+
return originalJson.call(res, data);
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
// Intercept res.send()
|
|
326
|
+
const originalSend = res.send;
|
|
327
|
+
if (typeof originalSend === 'function') {
|
|
328
|
+
res.send = function (data) {
|
|
329
|
+
if (!captured) {
|
|
330
|
+
captured = true;
|
|
331
|
+
const routeName = getRouteName();
|
|
332
|
+
const latestInput = extractRequestInput(req);
|
|
333
|
+
const output = typeof data === 'string' ? { __html: true } : data;
|
|
334
|
+
emitExpressPayload(routeName, opts.environment, opts.maxDepth, latestInput, output);
|
|
335
|
+
}
|
|
336
|
+
res.send = originalSend;
|
|
337
|
+
return originalSend.call(res, data);
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
next();
|
|
341
|
+
};
|
|
342
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch observer — patches global.fetch to automatically capture
|
|
3
|
+
* request/response types from HTTP calls made by user code.
|
|
4
|
+
*
|
|
5
|
+
* When your app does:
|
|
6
|
+
* const data = await fetch('https://api.example.com/users').then(r => r.json());
|
|
7
|
+
*
|
|
8
|
+
* Trickle captures:
|
|
9
|
+
* - Function name: "GET /users" (method + path)
|
|
10
|
+
* - Module: "api.example.com" (hostname)
|
|
11
|
+
* - Input type: request body (for POST/PUT/PATCH)
|
|
12
|
+
* - Return type: inferred from JSON response
|
|
13
|
+
* - Sample data: actual response payload
|
|
14
|
+
*
|
|
15
|
+
* The observer only intercepts when .json() is called, so:
|
|
16
|
+
* - Non-JSON responses (HTML, binary) are ignored
|
|
17
|
+
* - The original response is never modified
|
|
18
|
+
* - No extra network requests are made
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Patch global.fetch to observe JSON responses.
|
|
22
|
+
* Safe to call multiple times (idempotent).
|
|
23
|
+
*/
|
|
24
|
+
export declare function patchFetch(environment: string, debugMode: boolean): void;
|