trickle-observe 0.2.125 → 0.2.127
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/fastify.d.ts +36 -0
- package/dist/fastify.js +398 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +40 -8
- package/dist/koa.d.ts +45 -0
- package/dist/koa.js +369 -0
- package/dist/llm-observer.js +4 -0
- package/dist/observe-register.js +98 -0
- package/package.json +2 -2
- package/src/fastify.ts +421 -0
- package/src/index.ts +39 -7
- package/src/koa.ts +387 -0
- package/src/llm-observer.ts +6 -0
- package/src/observe-register.ts +91 -0
- package/dist/vite-plugin.test.d.ts +0 -1
- package/dist/vite-plugin.test.js +0 -160
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instrument a Fastify instance by monkey-patching route registration methods.
|
|
3
|
+
*
|
|
4
|
+
* Must be called BEFORE routes are defined:
|
|
5
|
+
*
|
|
6
|
+
* import Fastify from 'fastify';
|
|
7
|
+
* import { instrumentFastify } from 'trickle';
|
|
8
|
+
*
|
|
9
|
+
* const app = Fastify();
|
|
10
|
+
* instrumentFastify(app);
|
|
11
|
+
*
|
|
12
|
+
* app.get('/api/users', async (request, reply) => { ... });
|
|
13
|
+
*
|
|
14
|
+
* Each registered handler is wrapped to capture:
|
|
15
|
+
* - Input: `{ body, params, query }` from the request
|
|
16
|
+
* - Output: the data returned or passed to `reply.send()`
|
|
17
|
+
* - Errors: exceptions thrown in async handlers
|
|
18
|
+
*/
|
|
19
|
+
export declare function instrumentFastify(fastify: any, userOpts?: {
|
|
20
|
+
enabled?: boolean;
|
|
21
|
+
environment?: string;
|
|
22
|
+
sampleRate?: number;
|
|
23
|
+
maxDepth?: number;
|
|
24
|
+
}): void;
|
|
25
|
+
/**
|
|
26
|
+
* Fastify plugin-style instrumentation using onRequest/onResponse hooks.
|
|
27
|
+
*
|
|
28
|
+
* Use this as an alternative to monkey-patching:
|
|
29
|
+
*
|
|
30
|
+
* import Fastify from 'fastify';
|
|
31
|
+
* import { tricklePlugin } from 'trickle';
|
|
32
|
+
*
|
|
33
|
+
* const app = Fastify();
|
|
34
|
+
* app.register(tricklePlugin);
|
|
35
|
+
*/
|
|
36
|
+
export declare function tricklePlugin(fastify: any, userOpts: any, done: () => void): void;
|
package/dist/fastify.js
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
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
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.instrumentFastify = instrumentFastify;
|
|
37
|
+
exports.tricklePlugin = tricklePlugin;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const pathMod = __importStar(require("path"));
|
|
40
|
+
const type_inference_1 = require("./type-inference");
|
|
41
|
+
const type_hash_1 = require("./type-hash");
|
|
42
|
+
const cache_1 = require("./cache");
|
|
43
|
+
const transport_1 = require("./transport");
|
|
44
|
+
const env_detect_1 = require("./env-detect");
|
|
45
|
+
const call_trace_1 = require("./call-trace");
|
|
46
|
+
const fastifyCache = new cache_1.TypeCache();
|
|
47
|
+
// ── Input extraction ──
|
|
48
|
+
function extractFastifyInput(request) {
|
|
49
|
+
const input = {};
|
|
50
|
+
try {
|
|
51
|
+
if (request.body !== undefined && request.body !== null && typeof request.body === 'object' && Object.keys(request.body).length > 0) {
|
|
52
|
+
input.body = request.body;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch { }
|
|
56
|
+
try {
|
|
57
|
+
if (request.params && typeof request.params === 'object' && Object.keys(request.params).length > 0) {
|
|
58
|
+
input.params = request.params;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch { }
|
|
62
|
+
try {
|
|
63
|
+
if (request.query && typeof request.query === 'object' && Object.keys(request.query).length > 0) {
|
|
64
|
+
input.query = request.query;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch { }
|
|
68
|
+
return input;
|
|
69
|
+
}
|
|
70
|
+
// ── Sample sanitization (local copy) ──
|
|
71
|
+
function sanitizeSample(value, depth = 3) {
|
|
72
|
+
if (depth <= 0)
|
|
73
|
+
return '[truncated]';
|
|
74
|
+
if (value === null || value === undefined)
|
|
75
|
+
return value;
|
|
76
|
+
const t = typeof value;
|
|
77
|
+
if (t === 'string') {
|
|
78
|
+
const s = value;
|
|
79
|
+
return s.length > 200 ? s.substring(0, 200) + '...' : s;
|
|
80
|
+
}
|
|
81
|
+
if (t === 'number' || t === 'boolean')
|
|
82
|
+
return value;
|
|
83
|
+
if (t === 'bigint')
|
|
84
|
+
return String(value);
|
|
85
|
+
if (t === 'symbol')
|
|
86
|
+
return String(value);
|
|
87
|
+
if (t === 'function')
|
|
88
|
+
return `[Function: ${value.name || 'anonymous'}]`;
|
|
89
|
+
if (Array.isArray(value)) {
|
|
90
|
+
return value.slice(0, 5).map(item => sanitizeSample(item, depth - 1));
|
|
91
|
+
}
|
|
92
|
+
if (t === 'object') {
|
|
93
|
+
if (value instanceof Date)
|
|
94
|
+
return value.toISOString();
|
|
95
|
+
if (value instanceof RegExp)
|
|
96
|
+
return String(value);
|
|
97
|
+
if (value instanceof Error)
|
|
98
|
+
return { error: value.message };
|
|
99
|
+
if (value instanceof Map)
|
|
100
|
+
return `[Map: ${value.size} entries]`;
|
|
101
|
+
if (value instanceof Set)
|
|
102
|
+
return `[Set: ${value.size} items]`;
|
|
103
|
+
const obj = value;
|
|
104
|
+
const result = {};
|
|
105
|
+
const keys = Object.keys(obj).slice(0, 20);
|
|
106
|
+
for (const key of keys) {
|
|
107
|
+
try {
|
|
108
|
+
result[key] = sanitizeSample(obj[key], depth - 1);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
result[key] = '[unreadable]';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
return String(value);
|
|
117
|
+
}
|
|
118
|
+
// ── Error file writing ──
|
|
119
|
+
function writeErrorToFile(error, input, routeName) {
|
|
120
|
+
try {
|
|
121
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
122
|
+
const isLambda = !!process.env.AWS_LAMBDA_FUNCTION_NAME;
|
|
123
|
+
const defaultDir = isLambda ? '/tmp/.trickle' : pathMod.join(process.cwd(), '.trickle');
|
|
124
|
+
const dir = process.env.TRICKLE_LOCAL_DIR || defaultDir;
|
|
125
|
+
try {
|
|
126
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
127
|
+
}
|
|
128
|
+
catch { }
|
|
129
|
+
const stackLines = (err.stack || '').split('\n');
|
|
130
|
+
let errorFile;
|
|
131
|
+
let errorLine;
|
|
132
|
+
for (const sl of stackLines.slice(1)) {
|
|
133
|
+
const m = sl.match(/\((.+):(\d+):\d+\)/) || sl.match(/at (.+):(\d+):\d+/);
|
|
134
|
+
if (m && !m[1].includes('node_modules') && !m[1].includes('node:') && !m[1].includes('trickle-observe')) {
|
|
135
|
+
errorFile = m[1];
|
|
136
|
+
errorLine = parseInt(m[2]);
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const record = {
|
|
141
|
+
kind: 'error',
|
|
142
|
+
error: err.message,
|
|
143
|
+
type: err.constructor?.name || 'Error',
|
|
144
|
+
message: err.message,
|
|
145
|
+
file: errorFile,
|
|
146
|
+
line: errorLine,
|
|
147
|
+
stack: stackLines.slice(0, 6).join('\n'),
|
|
148
|
+
route: routeName,
|
|
149
|
+
request: input,
|
|
150
|
+
timestamp: new Date().toISOString(),
|
|
151
|
+
};
|
|
152
|
+
const errorsFile = pathMod.join(dir, 'errors.jsonl');
|
|
153
|
+
fs.appendFileSync(errorsFile, JSON.stringify(record) + '\n');
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Never crash the user's app
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// ── Payload emission ──
|
|
160
|
+
function emitFastifyPayload(functionName, environment, maxDepth, input, output, error, durationMs) {
|
|
161
|
+
try {
|
|
162
|
+
const functionKey = `fastify::${functionName}`;
|
|
163
|
+
const argsType = (0, type_inference_1.inferType)(input, maxDepth);
|
|
164
|
+
const returnType = error ? { kind: 'unknown' } : (0, type_inference_1.inferType)(output, maxDepth);
|
|
165
|
+
const hash = (0, type_hash_1.hashType)(argsType, returnType);
|
|
166
|
+
if (!error && !fastifyCache.shouldSend(functionKey, hash)) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (!error) {
|
|
170
|
+
fastifyCache.markSent(functionKey, hash);
|
|
171
|
+
}
|
|
172
|
+
const payload = {
|
|
173
|
+
functionName,
|
|
174
|
+
module: 'fastify',
|
|
175
|
+
language: 'js',
|
|
176
|
+
environment,
|
|
177
|
+
typeHash: hash,
|
|
178
|
+
argsType,
|
|
179
|
+
returnType,
|
|
180
|
+
sampleInput: sanitizeSample(input),
|
|
181
|
+
sampleOutput: error ? undefined : sanitizeSample(output),
|
|
182
|
+
};
|
|
183
|
+
if (durationMs !== undefined) {
|
|
184
|
+
payload.durationMs = Math.round(durationMs * 100) / 100;
|
|
185
|
+
}
|
|
186
|
+
if (error) {
|
|
187
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
188
|
+
payload.error = {
|
|
189
|
+
type: err.constructor?.name || 'Error',
|
|
190
|
+
message: err.message,
|
|
191
|
+
stackTrace: err.stack,
|
|
192
|
+
argsSnapshot: sanitizeSample(input),
|
|
193
|
+
};
|
|
194
|
+
writeErrorToFile(error, input, functionName);
|
|
195
|
+
}
|
|
196
|
+
(0, transport_1.enqueue)(payload);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// Never crash the user's app
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// ── Handler wrapping ──
|
|
203
|
+
function wrapFastifyHandler(handler, routeName, opts) {
|
|
204
|
+
const wrapped = async function (request, reply) {
|
|
205
|
+
// Sample rate check
|
|
206
|
+
if (opts.sampleRate < 1 && Math.random() > opts.sampleRate) {
|
|
207
|
+
return handler.call(this, request, reply);
|
|
208
|
+
}
|
|
209
|
+
const input = extractFastifyInput(request);
|
|
210
|
+
let captured = false;
|
|
211
|
+
const startTime = performance.now();
|
|
212
|
+
const callId = (0, call_trace_1.traceCall)(routeName, 'fastify');
|
|
213
|
+
// Intercept reply.send() to capture output
|
|
214
|
+
const originalSend = reply.send;
|
|
215
|
+
if (typeof originalSend === 'function') {
|
|
216
|
+
reply.send = function (data) {
|
|
217
|
+
if (!captured) {
|
|
218
|
+
captured = true;
|
|
219
|
+
const durationMs = performance.now() - startTime;
|
|
220
|
+
(0, call_trace_1.traceReturn)(callId, routeName, 'fastify', durationMs);
|
|
221
|
+
const output = typeof data === 'string' ? { __html: true } : data;
|
|
222
|
+
emitFastifyPayload(routeName, opts.environment, opts.maxDepth, input, output, undefined, durationMs);
|
|
223
|
+
}
|
|
224
|
+
reply.send = originalSend; // restore
|
|
225
|
+
return originalSend.call(reply, data);
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
const result = await handler.call(this, request, reply);
|
|
230
|
+
// Fastify allows returning a value directly (auto-serialized)
|
|
231
|
+
if (result !== undefined && !captured) {
|
|
232
|
+
captured = true;
|
|
233
|
+
const durationMs = performance.now() - startTime;
|
|
234
|
+
(0, call_trace_1.traceReturn)(callId, routeName, 'fastify', durationMs);
|
|
235
|
+
emitFastifyPayload(routeName, opts.environment, opts.maxDepth, input, result, undefined, durationMs);
|
|
236
|
+
}
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
if (!captured) {
|
|
241
|
+
captured = true;
|
|
242
|
+
const durationMs = performance.now() - startTime;
|
|
243
|
+
(0, call_trace_1.traceReturn)(callId, routeName, 'fastify', durationMs, (err instanceof Error ? err : new Error(String(err))).message);
|
|
244
|
+
emitFastifyPayload(routeName, opts.environment, opts.maxDepth, input, undefined, err, durationMs);
|
|
245
|
+
}
|
|
246
|
+
throw err;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
// Preserve function metadata
|
|
250
|
+
Object.defineProperty(wrapped, 'name', { value: handler.name || routeName, configurable: true });
|
|
251
|
+
Object.defineProperty(wrapped, 'length', { value: handler.length, configurable: true });
|
|
252
|
+
return wrapped;
|
|
253
|
+
}
|
|
254
|
+
// ── Public API ──
|
|
255
|
+
/**
|
|
256
|
+
* Instrument a Fastify instance by monkey-patching route registration methods.
|
|
257
|
+
*
|
|
258
|
+
* Must be called BEFORE routes are defined:
|
|
259
|
+
*
|
|
260
|
+
* import Fastify from 'fastify';
|
|
261
|
+
* import { instrumentFastify } from 'trickle';
|
|
262
|
+
*
|
|
263
|
+
* const app = Fastify();
|
|
264
|
+
* instrumentFastify(app);
|
|
265
|
+
*
|
|
266
|
+
* app.get('/api/users', async (request, reply) => { ... });
|
|
267
|
+
*
|
|
268
|
+
* Each registered handler is wrapped to capture:
|
|
269
|
+
* - Input: `{ body, params, query }` from the request
|
|
270
|
+
* - Output: the data returned or passed to `reply.send()`
|
|
271
|
+
* - Errors: exceptions thrown in async handlers
|
|
272
|
+
*/
|
|
273
|
+
function instrumentFastify(fastify, userOpts) {
|
|
274
|
+
const opts = {
|
|
275
|
+
enabled: userOpts?.enabled !== false,
|
|
276
|
+
environment: userOpts?.environment || (0, env_detect_1.detectEnvironment)(),
|
|
277
|
+
sampleRate: userOpts?.sampleRate ?? 1,
|
|
278
|
+
maxDepth: userOpts?.maxDepth ?? 5,
|
|
279
|
+
};
|
|
280
|
+
if (!opts.enabled)
|
|
281
|
+
return;
|
|
282
|
+
// Fastify shorthand methods: .get(), .post(), .put(), .delete(), .patch(), .all()
|
|
283
|
+
const methods = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'all'];
|
|
284
|
+
for (const method of methods) {
|
|
285
|
+
const original = fastify[method];
|
|
286
|
+
if (typeof original !== 'function')
|
|
287
|
+
continue;
|
|
288
|
+
fastify[method] = function (path, ...args) {
|
|
289
|
+
const pathStr = typeof path === 'string' ? path : String(path);
|
|
290
|
+
const routeName = `${method.toUpperCase()} ${pathStr}`;
|
|
291
|
+
// Fastify shorthand: .get(path, opts?, handler)
|
|
292
|
+
// args can be [handler] or [opts, handler]
|
|
293
|
+
const wrapped = args.map((arg) => {
|
|
294
|
+
if (typeof arg === 'function') {
|
|
295
|
+
try {
|
|
296
|
+
return wrapFastifyHandler(arg, routeName, opts);
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
return arg;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// If it's an options object with a handler property, wrap that
|
|
303
|
+
if (arg && typeof arg === 'object' && typeof arg.handler === 'function') {
|
|
304
|
+
try {
|
|
305
|
+
arg.handler = wrapFastifyHandler(arg.handler, routeName, opts);
|
|
306
|
+
}
|
|
307
|
+
catch { }
|
|
308
|
+
}
|
|
309
|
+
return arg;
|
|
310
|
+
});
|
|
311
|
+
return original.call(this, path, ...wrapped);
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
// Also wrap fastify.route({ method, url, handler })
|
|
315
|
+
const originalRoute = fastify.route;
|
|
316
|
+
if (typeof originalRoute === 'function') {
|
|
317
|
+
fastify.route = function (routeOptions) {
|
|
318
|
+
if (routeOptions && typeof routeOptions.handler === 'function') {
|
|
319
|
+
const method = (Array.isArray(routeOptions.method) ? routeOptions.method[0] : routeOptions.method) || 'ALL';
|
|
320
|
+
const url = routeOptions.url || routeOptions.path || '/';
|
|
321
|
+
const routeName = `${method.toUpperCase()} ${url}`;
|
|
322
|
+
try {
|
|
323
|
+
routeOptions.handler = wrapFastifyHandler(routeOptions.handler, routeName, opts);
|
|
324
|
+
}
|
|
325
|
+
catch { }
|
|
326
|
+
}
|
|
327
|
+
return originalRoute.call(this, routeOptions);
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Fastify plugin-style instrumentation using onRequest/onResponse hooks.
|
|
333
|
+
*
|
|
334
|
+
* Use this as an alternative to monkey-patching:
|
|
335
|
+
*
|
|
336
|
+
* import Fastify from 'fastify';
|
|
337
|
+
* import { tricklePlugin } from 'trickle';
|
|
338
|
+
*
|
|
339
|
+
* const app = Fastify();
|
|
340
|
+
* app.register(tricklePlugin);
|
|
341
|
+
*/
|
|
342
|
+
function tricklePlugin(fastify, userOpts, done) {
|
|
343
|
+
const opts = {
|
|
344
|
+
enabled: userOpts?.enabled !== false,
|
|
345
|
+
environment: userOpts?.environment || (0, env_detect_1.detectEnvironment)(),
|
|
346
|
+
sampleRate: userOpts?.sampleRate ?? 1,
|
|
347
|
+
maxDepth: userOpts?.maxDepth ?? 5,
|
|
348
|
+
};
|
|
349
|
+
if (!opts.enabled) {
|
|
350
|
+
done();
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
// Use onRequest to start timing and onResponse to capture result
|
|
354
|
+
fastify.addHook('onRequest', async (request, _reply) => {
|
|
355
|
+
if (opts.sampleRate < 1 && Math.random() > opts.sampleRate)
|
|
356
|
+
return;
|
|
357
|
+
request.__trickleStart = performance.now();
|
|
358
|
+
request.__trickleInput = extractFastifyInput(request);
|
|
359
|
+
request.__trickleCallId = (0, call_trace_1.traceCall)(`${request.method} ${request.url}`, 'fastify');
|
|
360
|
+
});
|
|
361
|
+
fastify.addHook('onSend', async (request, reply, payload) => {
|
|
362
|
+
if (!request.__trickleStart)
|
|
363
|
+
return payload;
|
|
364
|
+
const durationMs = performance.now() - request.__trickleStart;
|
|
365
|
+
const routeName = `${request.method} ${request.routeOptions?.url || request.url}`;
|
|
366
|
+
const input = request.__trickleInput || extractFastifyInput(request);
|
|
367
|
+
(0, call_trace_1.traceReturn)(request.__trickleCallId, routeName, 'fastify', durationMs);
|
|
368
|
+
// Parse payload if it's JSON string
|
|
369
|
+
let output;
|
|
370
|
+
if (typeof payload === 'string') {
|
|
371
|
+
try {
|
|
372
|
+
output = JSON.parse(payload);
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
output = { __html: true };
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
output = payload;
|
|
380
|
+
}
|
|
381
|
+
emitFastifyPayload(routeName, opts.environment, opts.maxDepth, input, output, undefined, durationMs);
|
|
382
|
+
delete request.__trickleStart;
|
|
383
|
+
delete request.__trickleInput;
|
|
384
|
+
delete request.__trickleCallId;
|
|
385
|
+
return payload; // Must return payload for Fastify hook chain
|
|
386
|
+
});
|
|
387
|
+
fastify.addHook('onError', async (request, _reply, error) => {
|
|
388
|
+
if (!request.__trickleStart)
|
|
389
|
+
return;
|
|
390
|
+
const durationMs = performance.now() - request.__trickleStart;
|
|
391
|
+
const routeName = `${request.method} ${request.routeOptions?.url || request.url}`;
|
|
392
|
+
const input = request.__trickleInput || extractFastifyInput(request);
|
|
393
|
+
(0, call_trace_1.traceReturn)(request.__trickleCallId, routeName, 'fastify', durationMs, error?.message);
|
|
394
|
+
emitFastifyPayload(routeName, opts.environment, opts.maxDepth, input, undefined, error, durationMs);
|
|
395
|
+
delete request.__trickleStart;
|
|
396
|
+
});
|
|
397
|
+
done();
|
|
398
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -42,14 +42,16 @@ export declare function trickleExpress(app: any, opts?: {
|
|
|
42
42
|
maxDepth?: number;
|
|
43
43
|
}): void;
|
|
44
44
|
/**
|
|
45
|
-
* Auto-instrument a framework app.
|
|
45
|
+
* Auto-instrument a framework app. Supports Express, Fastify, and Koa.
|
|
46
46
|
*
|
|
47
47
|
* Usage:
|
|
48
48
|
* import { instrument } from 'trickle';
|
|
49
|
-
* const app = express();
|
|
50
49
|
* instrument(app);
|
|
51
50
|
*
|
|
52
|
-
* Detects
|
|
51
|
+
* Detects the framework automatically:
|
|
52
|
+
* - Express: has `app.listen`, `app.get`, `app.use`, `app.set`
|
|
53
|
+
* - Fastify: has `app.route`, `app.register`, `app.addHook`
|
|
54
|
+
* - Koa: has `app.use`, `app.listen`, `app.context` (but no `app.get` method on the app itself)
|
|
53
55
|
*/
|
|
54
56
|
export declare function instrument(app: any, opts?: {
|
|
55
57
|
enabled?: boolean;
|
|
@@ -60,6 +62,8 @@ export declare function instrument(app: any, opts?: {
|
|
|
60
62
|
export type { TypeNode, GlobalOpts, TrickleOpts, IngestPayload } from './types';
|
|
61
63
|
export { flush } from './transport';
|
|
62
64
|
export { instrumentExpress, trickleMiddleware } from './express';
|
|
65
|
+
export { instrumentFastify, tricklePlugin } from './fastify';
|
|
66
|
+
export { instrumentKoa, instrumentKoaRouter } from './koa';
|
|
63
67
|
export { observe, observeFn } from './observe';
|
|
64
68
|
export type { ObserveOpts } from './observe';
|
|
65
69
|
export { wrapFunction } from './wrap';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.wrapFunction = exports.observeFn = exports.observe = exports.trickleMiddleware = exports.instrumentExpress = exports.flush = void 0;
|
|
3
|
+
exports.wrapFunction = exports.observeFn = exports.observe = exports.instrumentKoaRouter = exports.instrumentKoa = exports.tricklePlugin = exports.instrumentFastify = exports.trickleMiddleware = exports.instrumentExpress = exports.flush = void 0;
|
|
4
4
|
exports.configure = configure;
|
|
5
5
|
exports.trickle = trickle;
|
|
6
6
|
exports.trickleHandler = trickleHandler;
|
|
@@ -10,6 +10,8 @@ const transport_1 = require("./transport");
|
|
|
10
10
|
const wrap_1 = require("./wrap");
|
|
11
11
|
const env_detect_1 = require("./env-detect");
|
|
12
12
|
const express_1 = require("./express");
|
|
13
|
+
const fastify_1 = require("./fastify");
|
|
14
|
+
const koa_1 = require("./koa");
|
|
13
15
|
let globalOpts = {
|
|
14
16
|
backendUrl: 'http://localhost:4888',
|
|
15
17
|
batchIntervalMs: 2000,
|
|
@@ -108,22 +110,46 @@ function trickleExpress(app, opts) {
|
|
|
108
110
|
});
|
|
109
111
|
}
|
|
110
112
|
/**
|
|
111
|
-
* Auto-instrument a framework app.
|
|
113
|
+
* Auto-instrument a framework app. Supports Express, Fastify, and Koa.
|
|
112
114
|
*
|
|
113
115
|
* Usage:
|
|
114
116
|
* import { instrument } from 'trickle';
|
|
115
|
-
* const app = express();
|
|
116
117
|
* instrument(app);
|
|
117
118
|
*
|
|
118
|
-
* Detects
|
|
119
|
+
* Detects the framework automatically:
|
|
120
|
+
* - Express: has `app.listen`, `app.get`, `app.use`, `app.set`
|
|
121
|
+
* - Fastify: has `app.route`, `app.register`, `app.addHook`
|
|
122
|
+
* - Koa: has `app.use`, `app.listen`, `app.context` (but no `app.get` method on the app itself)
|
|
119
123
|
*/
|
|
120
124
|
function instrument(app, opts) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
125
|
+
if (!app) {
|
|
126
|
+
if (typeof console !== 'undefined' && console.warn) {
|
|
127
|
+
console.warn('[trickle] instrument(): received null/undefined app');
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const mergedOpts = {
|
|
132
|
+
enabled: opts?.enabled ?? globalOpts.enabled,
|
|
133
|
+
environment: opts?.environment ?? globalOpts.environment ?? (0, env_detect_1.detectEnvironment)(),
|
|
134
|
+
sampleRate: opts?.sampleRate ?? 1,
|
|
135
|
+
maxDepth: opts?.maxDepth ?? 5,
|
|
136
|
+
};
|
|
137
|
+
// Detect Fastify: has .route(), .register(), .addHook()
|
|
138
|
+
if (typeof app.route === 'function' && typeof app.register === 'function' && typeof app.addHook === 'function') {
|
|
139
|
+
(0, fastify_1.instrumentFastify)(app, mergedOpts);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// Detect Koa: has .use(), .listen(), .context (but NOT .get as a route method on the app object)
|
|
143
|
+
// Koa apps have a .context property and .use() but .get is only defined on koa-router
|
|
144
|
+
if (typeof app.use === 'function' && typeof app.listen === 'function' && app.context !== undefined && typeof app.set !== 'function') {
|
|
145
|
+
(0, koa_1.instrumentKoa)(app, mergedOpts);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
// Detect Express: has .listen(), .get(), .use(), .set()
|
|
149
|
+
if (typeof app.listen === 'function' && typeof app.get === 'function' && typeof app.use === 'function') {
|
|
150
|
+
trickleExpress(app, mergedOpts);
|
|
124
151
|
return;
|
|
125
152
|
}
|
|
126
|
-
// Future: detect other frameworks here (Koa, Fastify, etc.)
|
|
127
153
|
if (typeof console !== 'undefined' && console.warn) {
|
|
128
154
|
console.warn('[trickle] instrument(): could not detect a supported framework on the provided object');
|
|
129
155
|
}
|
|
@@ -167,6 +193,12 @@ Object.defineProperty(exports, "flush", { enumerable: true, get: function () { r
|
|
|
167
193
|
var express_2 = require("./express");
|
|
168
194
|
Object.defineProperty(exports, "instrumentExpress", { enumerable: true, get: function () { return express_2.instrumentExpress; } });
|
|
169
195
|
Object.defineProperty(exports, "trickleMiddleware", { enumerable: true, get: function () { return express_2.trickleMiddleware; } });
|
|
196
|
+
var fastify_2 = require("./fastify");
|
|
197
|
+
Object.defineProperty(exports, "instrumentFastify", { enumerable: true, get: function () { return fastify_2.instrumentFastify; } });
|
|
198
|
+
Object.defineProperty(exports, "tricklePlugin", { enumerable: true, get: function () { return fastify_2.tricklePlugin; } });
|
|
199
|
+
var koa_2 = require("./koa");
|
|
200
|
+
Object.defineProperty(exports, "instrumentKoa", { enumerable: true, get: function () { return koa_2.instrumentKoa; } });
|
|
201
|
+
Object.defineProperty(exports, "instrumentKoaRouter", { enumerable: true, get: function () { return koa_2.instrumentKoaRouter; } });
|
|
170
202
|
var observe_1 = require("./observe");
|
|
171
203
|
Object.defineProperty(exports, "observe", { enumerable: true, get: function () { return observe_1.observe; } });
|
|
172
204
|
Object.defineProperty(exports, "observeFn", { enumerable: true, get: function () { return observe_1.observeFn; } });
|
package/dist/koa.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instrument a Koa application with observability middleware.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import Koa from 'koa';
|
|
6
|
+
* import { instrumentKoa } from 'trickle';
|
|
7
|
+
*
|
|
8
|
+
* const app = new Koa();
|
|
9
|
+
* instrumentKoa(app); // Add BEFORE routes
|
|
10
|
+
*
|
|
11
|
+
* Captures:
|
|
12
|
+
* - Input: `{ body, params, query }` from the request context
|
|
13
|
+
* - Output: `ctx.body` value set by route handlers
|
|
14
|
+
* - Errors: exceptions thrown in middleware/handlers
|
|
15
|
+
* - Timing: request duration in milliseconds
|
|
16
|
+
*
|
|
17
|
+
* Works with koa-router and @koa/router for named route paths.
|
|
18
|
+
*/
|
|
19
|
+
export declare function instrumentKoa(app: any, userOpts?: {
|
|
20
|
+
enabled?: boolean;
|
|
21
|
+
environment?: string;
|
|
22
|
+
sampleRate?: number;
|
|
23
|
+
maxDepth?: number;
|
|
24
|
+
}): void;
|
|
25
|
+
/**
|
|
26
|
+
* Instrument a koa-router (or @koa/router) instance by monkey-patching route methods.
|
|
27
|
+
*
|
|
28
|
+
* Usage:
|
|
29
|
+
* import Router from '@koa/router';
|
|
30
|
+
* import { instrumentKoaRouter } from 'trickle';
|
|
31
|
+
*
|
|
32
|
+
* const router = new Router();
|
|
33
|
+
* instrumentKoaRouter(router);
|
|
34
|
+
*
|
|
35
|
+
* router.get('/api/users', async (ctx) => { ... });
|
|
36
|
+
*
|
|
37
|
+
* This provides more precise route names (e.g., "GET /api/users/:id")
|
|
38
|
+
* compared to the middleware approach.
|
|
39
|
+
*/
|
|
40
|
+
export declare function instrumentKoaRouter(router: any, userOpts?: {
|
|
41
|
+
enabled?: boolean;
|
|
42
|
+
environment?: string;
|
|
43
|
+
sampleRate?: number;
|
|
44
|
+
maxDepth?: number;
|
|
45
|
+
}): void;
|