trickle-observe 0.2.127 → 0.2.128

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/hono.d.ts ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Instrument a Hono app by monkey-patching route registration methods.
3
+ *
4
+ * Must be called BEFORE routes are defined:
5
+ *
6
+ * import { Hono } from 'hono';
7
+ * import { instrumentHono } from 'trickle';
8
+ *
9
+ * const app = new Hono();
10
+ * instrumentHono(app);
11
+ *
12
+ * app.get('/api/users', (c) => c.json({ users: [] }));
13
+ *
14
+ * Captures:
15
+ * - Input: body (JSON), params, query from the Hono context
16
+ * - Output: the data passed to c.json() / c.text() or returned directly
17
+ * - Errors: exceptions thrown in handlers
18
+ * - Timing: request duration in milliseconds
19
+ */
20
+ export declare function instrumentHono(app: any, userOpts?: {
21
+ enabled?: boolean;
22
+ environment?: string;
23
+ sampleRate?: number;
24
+ maxDepth?: number;
25
+ }): void;
26
+ /**
27
+ * Hono middleware for observability. Use this as an alternative to
28
+ * monkey-patching route methods:
29
+ *
30
+ * import { Hono } from 'hono';
31
+ * import { trickleHonoMiddleware } from 'trickle';
32
+ *
33
+ * const app = new Hono();
34
+ * app.use('*', trickleHonoMiddleware());
35
+ */
36
+ export declare function trickleHonoMiddleware(userOpts?: {
37
+ enabled?: boolean;
38
+ environment?: string;
39
+ sampleRate?: number;
40
+ maxDepth?: number;
41
+ }): (c: any, next: () => Promise<void>) => Promise<void | Response>;
package/dist/hono.js ADDED
@@ -0,0 +1,396 @@
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.instrumentHono = instrumentHono;
37
+ exports.trickleHonoMiddleware = trickleHonoMiddleware;
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 honoCache = new cache_1.TypeCache();
47
+ // ── Input extraction ──
48
+ async function extractHonoInput(c) {
49
+ const input = {};
50
+ try {
51
+ // Hono body: c.req.json() / c.req.text() — need to clone to avoid consuming
52
+ const contentType = c.req.header('content-type') || '';
53
+ if (contentType.includes('application/json')) {
54
+ try {
55
+ const body = await c.req.json();
56
+ if (body && typeof body === 'object' && Object.keys(body).length > 0) {
57
+ input.body = body;
58
+ }
59
+ }
60
+ catch { /* body may not be parseable */ }
61
+ }
62
+ }
63
+ catch { }
64
+ try {
65
+ // c.req.param() returns all params as an object
66
+ const params = c.req.param();
67
+ if (params && typeof params === 'object' && Object.keys(params).length > 0) {
68
+ input.params = params;
69
+ }
70
+ }
71
+ catch { }
72
+ try {
73
+ // c.req.query() returns all query params
74
+ const query = c.req.query();
75
+ if (query && typeof query === 'object' && Object.keys(query).length > 0) {
76
+ input.query = query;
77
+ }
78
+ }
79
+ catch { }
80
+ return input;
81
+ }
82
+ // ── Sample sanitization (local copy) ──
83
+ function sanitizeSample(value, depth = 3) {
84
+ if (depth <= 0)
85
+ return '[truncated]';
86
+ if (value === null || value === undefined)
87
+ return value;
88
+ const t = typeof value;
89
+ if (t === 'string') {
90
+ const s = value;
91
+ return s.length > 200 ? s.substring(0, 200) + '...' : s;
92
+ }
93
+ if (t === 'number' || t === 'boolean')
94
+ return value;
95
+ if (t === 'bigint')
96
+ return String(value);
97
+ if (t === 'function')
98
+ return `[Function: ${value.name || 'anonymous'}]`;
99
+ if (Array.isArray(value)) {
100
+ return value.slice(0, 5).map(item => sanitizeSample(item, depth - 1));
101
+ }
102
+ if (t === 'object') {
103
+ if (value instanceof Date)
104
+ return value.toISOString();
105
+ if (value instanceof Error)
106
+ return { error: value.message };
107
+ if (value instanceof Map)
108
+ return `[Map: ${value.size} entries]`;
109
+ if (value instanceof Set)
110
+ return `[Set: ${value.size} items]`;
111
+ const obj = value;
112
+ const result = {};
113
+ const keys = Object.keys(obj).slice(0, 20);
114
+ for (const key of keys) {
115
+ try {
116
+ result[key] = sanitizeSample(obj[key], depth - 1);
117
+ }
118
+ catch {
119
+ result[key] = '[unreadable]';
120
+ }
121
+ }
122
+ return result;
123
+ }
124
+ return String(value);
125
+ }
126
+ // ── Error file writing ──
127
+ function writeErrorToFile(error, input, routeName) {
128
+ try {
129
+ const err = error instanceof Error ? error : new Error(String(error));
130
+ const defaultDir = pathMod.join(process.cwd(), '.trickle');
131
+ const dir = process.env.TRICKLE_LOCAL_DIR || defaultDir;
132
+ try {
133
+ fs.mkdirSync(dir, { recursive: true });
134
+ }
135
+ catch { }
136
+ const stackLines = (err.stack || '').split('\n');
137
+ let errorFile;
138
+ let errorLine;
139
+ for (const sl of stackLines.slice(1)) {
140
+ const m = sl.match(/\((.+):(\d+):\d+\)/) || sl.match(/at (.+):(\d+):\d+/);
141
+ if (m && !m[1].includes('node_modules') && !m[1].includes('node:') && !m[1].includes('trickle-observe')) {
142
+ errorFile = m[1];
143
+ errorLine = parseInt(m[2]);
144
+ break;
145
+ }
146
+ }
147
+ const record = {
148
+ kind: 'error',
149
+ error: err.message,
150
+ type: err.constructor?.name || 'Error',
151
+ message: err.message,
152
+ file: errorFile,
153
+ line: errorLine,
154
+ stack: stackLines.slice(0, 6).join('\n'),
155
+ route: routeName,
156
+ request: input,
157
+ timestamp: new Date().toISOString(),
158
+ };
159
+ fs.appendFileSync(pathMod.join(dir, 'errors.jsonl'), JSON.stringify(record) + '\n');
160
+ }
161
+ catch {
162
+ // Never crash the user's app
163
+ }
164
+ }
165
+ // ── Payload emission ──
166
+ function emitHonoPayload(functionName, environment, maxDepth, input, output, error, durationMs) {
167
+ try {
168
+ const functionKey = `hono::${functionName}`;
169
+ const argsType = (0, type_inference_1.inferType)(input, maxDepth);
170
+ const returnType = error ? { kind: 'unknown' } : (0, type_inference_1.inferType)(output, maxDepth);
171
+ const hash = (0, type_hash_1.hashType)(argsType, returnType);
172
+ if (!error && !honoCache.shouldSend(functionKey, hash))
173
+ return;
174
+ if (!error)
175
+ honoCache.markSent(functionKey, hash);
176
+ const payload = {
177
+ functionName,
178
+ module: 'hono',
179
+ language: 'js',
180
+ environment,
181
+ typeHash: hash,
182
+ argsType,
183
+ returnType,
184
+ sampleInput: sanitizeSample(input),
185
+ sampleOutput: error ? undefined : sanitizeSample(output),
186
+ };
187
+ if (durationMs !== undefined) {
188
+ payload.durationMs = Math.round(durationMs * 100) / 100;
189
+ }
190
+ if (error) {
191
+ const err = error instanceof Error ? error : new Error(String(error));
192
+ payload.error = {
193
+ type: err.constructor?.name || 'Error',
194
+ message: err.message,
195
+ stackTrace: err.stack,
196
+ argsSnapshot: sanitizeSample(input),
197
+ };
198
+ writeErrorToFile(error, input, functionName);
199
+ }
200
+ (0, transport_1.enqueue)(payload);
201
+ }
202
+ catch {
203
+ // Never crash the user's app
204
+ }
205
+ }
206
+ // ── Public API ──
207
+ /**
208
+ * Instrument a Hono app by monkey-patching route registration methods.
209
+ *
210
+ * Must be called BEFORE routes are defined:
211
+ *
212
+ * import { Hono } from 'hono';
213
+ * import { instrumentHono } from 'trickle';
214
+ *
215
+ * const app = new Hono();
216
+ * instrumentHono(app);
217
+ *
218
+ * app.get('/api/users', (c) => c.json({ users: [] }));
219
+ *
220
+ * Captures:
221
+ * - Input: body (JSON), params, query from the Hono context
222
+ * - Output: the data passed to c.json() / c.text() or returned directly
223
+ * - Errors: exceptions thrown in handlers
224
+ * - Timing: request duration in milliseconds
225
+ */
226
+ function instrumentHono(app, userOpts) {
227
+ const opts = {
228
+ enabled: userOpts?.enabled !== false,
229
+ environment: userOpts?.environment || (0, env_detect_1.detectEnvironment)(),
230
+ sampleRate: userOpts?.sampleRate ?? 1,
231
+ maxDepth: userOpts?.maxDepth ?? 5,
232
+ };
233
+ if (!opts.enabled)
234
+ return;
235
+ const methods = ['get', 'post', 'put', 'delete', 'patch', 'all', 'options', 'head'];
236
+ for (const method of methods) {
237
+ const original = app[method];
238
+ if (typeof original !== 'function')
239
+ continue;
240
+ app[method] = function (path, ...handlers) {
241
+ const pathStr = typeof path === 'string' ? path : String(path);
242
+ const routeName = `${method.toUpperCase()} ${pathStr}`;
243
+ const wrapped = handlers.map((handler) => {
244
+ if (typeof handler !== 'function')
245
+ return handler;
246
+ return wrapHonoHandler(handler, routeName, opts);
247
+ });
248
+ return original.call(this, path, ...wrapped);
249
+ };
250
+ }
251
+ }
252
+ function wrapHonoHandler(handler, routeName, opts) {
253
+ const wrapped = async function (c, next) {
254
+ // Sample rate check
255
+ if (opts.sampleRate < 1 && Math.random() > opts.sampleRate) {
256
+ return handler.call(this, c, next);
257
+ }
258
+ const startTime = performance.now();
259
+ const callId = (0, call_trace_1.traceCall)(routeName, 'hono');
260
+ // Extract input (async because body parsing is async in Hono)
261
+ let input = {};
262
+ try {
263
+ input = await extractHonoInput(c);
264
+ }
265
+ catch { }
266
+ // Intercept c.json() to capture output
267
+ let captured = false;
268
+ const originalJson = c.json;
269
+ if (typeof originalJson === 'function') {
270
+ c.json = function (data, ...args) {
271
+ if (!captured) {
272
+ captured = true;
273
+ const durationMs = performance.now() - startTime;
274
+ (0, call_trace_1.traceReturn)(callId, routeName, 'hono', durationMs);
275
+ emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, data, undefined, durationMs);
276
+ }
277
+ return originalJson.call(c, data, ...args);
278
+ };
279
+ }
280
+ // Intercept c.text() for text responses
281
+ const originalText = c.text;
282
+ if (typeof originalText === 'function') {
283
+ c.text = function (data, ...args) {
284
+ if (!captured) {
285
+ captured = true;
286
+ const durationMs = performance.now() - startTime;
287
+ (0, call_trace_1.traceReturn)(callId, routeName, 'hono', durationMs);
288
+ emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, { __text: true }, undefined, durationMs);
289
+ }
290
+ return originalText.call(c, data, ...args);
291
+ };
292
+ }
293
+ try {
294
+ const result = await handler.call(this, c, next);
295
+ // Hono handlers can return a Response object directly
296
+ if (result && !captured) {
297
+ captured = true;
298
+ const durationMs = performance.now() - startTime;
299
+ (0, call_trace_1.traceReturn)(callId, routeName, 'hono', durationMs);
300
+ // Try to extract JSON from the Response
301
+ if (result instanceof Response) {
302
+ try {
303
+ const cloned = result.clone();
304
+ const ct = cloned.headers.get('content-type') || '';
305
+ if (ct.includes('application/json')) {
306
+ const body = await cloned.json();
307
+ emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, body, undefined, durationMs);
308
+ }
309
+ else {
310
+ emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, { __response: true }, undefined, durationMs);
311
+ }
312
+ }
313
+ catch {
314
+ emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, { __response: true }, undefined, durationMs);
315
+ }
316
+ }
317
+ else {
318
+ emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, result, undefined, durationMs);
319
+ }
320
+ }
321
+ return result;
322
+ }
323
+ catch (err) {
324
+ if (!captured) {
325
+ captured = true;
326
+ const durationMs = performance.now() - startTime;
327
+ (0, call_trace_1.traceReturn)(callId, routeName, 'hono', durationMs, (err instanceof Error ? err : new Error(String(err))).message);
328
+ emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, undefined, err, durationMs);
329
+ }
330
+ throw err;
331
+ }
332
+ };
333
+ Object.defineProperty(wrapped, 'name', { value: handler.name || routeName, configurable: true });
334
+ Object.defineProperty(wrapped, 'length', { value: handler.length, configurable: true });
335
+ return wrapped;
336
+ }
337
+ /**
338
+ * Hono middleware for observability. Use this as an alternative to
339
+ * monkey-patching route methods:
340
+ *
341
+ * import { Hono } from 'hono';
342
+ * import { trickleHonoMiddleware } from 'trickle';
343
+ *
344
+ * const app = new Hono();
345
+ * app.use('*', trickleHonoMiddleware());
346
+ */
347
+ function trickleHonoMiddleware(userOpts) {
348
+ const opts = {
349
+ enabled: userOpts?.enabled !== false,
350
+ environment: userOpts?.environment || (0, env_detect_1.detectEnvironment)(),
351
+ sampleRate: userOpts?.sampleRate ?? 1,
352
+ maxDepth: userOpts?.maxDepth ?? 5,
353
+ };
354
+ return async function trickleHonoMw(c, next) {
355
+ if (!opts.enabled)
356
+ return next();
357
+ if (opts.sampleRate < 1 && Math.random() > opts.sampleRate)
358
+ return next();
359
+ const startTime = performance.now();
360
+ const routeName = `${c.req.method} ${c.req.path}`;
361
+ const callId = (0, call_trace_1.traceCall)(routeName, 'hono');
362
+ let input = {};
363
+ try {
364
+ input = await extractHonoInput(c);
365
+ }
366
+ catch { }
367
+ try {
368
+ await next();
369
+ const durationMs = performance.now() - startTime;
370
+ (0, call_trace_1.traceReturn)(callId, routeName, 'hono', durationMs);
371
+ // After next(), capture from c.res if available
372
+ if (c.res) {
373
+ try {
374
+ const ct = c.res.headers?.get('content-type') || '';
375
+ if (ct.includes('application/json')) {
376
+ const cloned = c.res.clone();
377
+ const body = await cloned.json();
378
+ emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, body, undefined, durationMs);
379
+ }
380
+ else {
381
+ emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, { __response: true }, undefined, durationMs);
382
+ }
383
+ }
384
+ catch {
385
+ emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, { __response: true }, undefined, durationMs);
386
+ }
387
+ }
388
+ }
389
+ catch (err) {
390
+ const durationMs = performance.now() - startTime;
391
+ (0, call_trace_1.traceReturn)(callId, routeName, 'hono', durationMs, (err instanceof Error ? err : new Error(String(err))).message);
392
+ emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, undefined, err, durationMs);
393
+ throw err;
394
+ }
395
+ };
396
+ }
package/dist/index.d.ts CHANGED
@@ -42,7 +42,7 @@ export declare function trickleExpress(app: any, opts?: {
42
42
  maxDepth?: number;
43
43
  }): void;
44
44
  /**
45
- * Auto-instrument a framework app. Supports Express, Fastify, and Koa.
45
+ * Auto-instrument a framework app. Supports Express, Fastify, Koa, and Hono.
46
46
  *
47
47
  * Usage:
48
48
  * import { instrument } from 'trickle';
@@ -64,6 +64,7 @@ export { flush } from './transport';
64
64
  export { instrumentExpress, trickleMiddleware } from './express';
65
65
  export { instrumentFastify, tricklePlugin } from './fastify';
66
66
  export { instrumentKoa, instrumentKoaRouter } from './koa';
67
+ export { instrumentHono, trickleHonoMiddleware } from './hono';
67
68
  export { observe, observeFn } from './observe';
68
69
  export type { ObserveOpts } from './observe';
69
70
  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.instrumentKoaRouter = exports.instrumentKoa = exports.tricklePlugin = exports.instrumentFastify = exports.trickleMiddleware = exports.instrumentExpress = exports.flush = void 0;
3
+ exports.wrapFunction = exports.observeFn = exports.observe = exports.trickleHonoMiddleware = exports.instrumentHono = 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;
@@ -12,6 +12,7 @@ const env_detect_1 = require("./env-detect");
12
12
  const express_1 = require("./express");
13
13
  const fastify_1 = require("./fastify");
14
14
  const koa_1 = require("./koa");
15
+ const hono_1 = require("./hono");
15
16
  let globalOpts = {
16
17
  backendUrl: 'http://localhost:4888',
17
18
  batchIntervalMs: 2000,
@@ -110,7 +111,7 @@ function trickleExpress(app, opts) {
110
111
  });
111
112
  }
112
113
  /**
113
- * Auto-instrument a framework app. Supports Express, Fastify, and Koa.
114
+ * Auto-instrument a framework app. Supports Express, Fastify, Koa, and Hono.
114
115
  *
115
116
  * Usage:
116
117
  * import { instrument } from 'trickle';
@@ -134,6 +135,12 @@ function instrument(app, opts) {
134
135
  sampleRate: opts?.sampleRate ?? 1,
135
136
  maxDepth: opts?.maxDepth ?? 5,
136
137
  };
138
+ // Detect Hono: has .fetch (bound method), .route(), .get(), but NOT .listen() on the app itself
139
+ // Hono apps use serve() from @hono/node-server rather than app.listen()
140
+ if (typeof app.fetch === 'function' && typeof app.get === 'function' && typeof app.route === 'function' && typeof app.fire === 'function') {
141
+ (0, hono_1.instrumentHono)(app, mergedOpts);
142
+ return;
143
+ }
137
144
  // Detect Fastify: has .route(), .register(), .addHook()
138
145
  if (typeof app.route === 'function' && typeof app.register === 'function' && typeof app.addHook === 'function') {
139
146
  (0, fastify_1.instrumentFastify)(app, mergedOpts);
@@ -199,6 +206,9 @@ Object.defineProperty(exports, "tricklePlugin", { enumerable: true, get: functio
199
206
  var koa_2 = require("./koa");
200
207
  Object.defineProperty(exports, "instrumentKoa", { enumerable: true, get: function () { return koa_2.instrumentKoa; } });
201
208
  Object.defineProperty(exports, "instrumentKoaRouter", { enumerable: true, get: function () { return koa_2.instrumentKoaRouter; } });
209
+ var hono_2 = require("./hono");
210
+ Object.defineProperty(exports, "instrumentHono", { enumerable: true, get: function () { return hono_2.instrumentHono; } });
211
+ Object.defineProperty(exports, "trickleHonoMiddleware", { enumerable: true, get: function () { return hono_2.trickleHonoMiddleware; } });
202
212
  var observe_1 = require("./observe");
203
213
  Object.defineProperty(exports, "observe", { enumerable: true, get: function () { return observe_1.observe; } });
204
214
  Object.defineProperty(exports, "observeFn", { enumerable: true, get: function () { return observe_1.observeFn; } });
@@ -41,6 +41,7 @@ const fetch_observer_1 = require("./fetch-observer");
41
41
  const express_1 = require("./express");
42
42
  const fastify_1 = require("./fastify");
43
43
  const koa_1 = require("./koa");
44
+ const hono_1 = require("./hono");
44
45
  const trace_var_1 = require("./trace-var");
45
46
  const call_trace_1 = require("./call-trace");
46
47
  const llm_observer_1 = require("./llm-observer");
@@ -1502,6 +1503,49 @@ if (enabled) {
1502
1503
  }
1503
1504
  catch { /* fall through */ }
1504
1505
  }
1506
+ // ── Hono auto-detection ──
1507
+ if (request === 'hono' && !expressPatched.has('hono')) {
1508
+ expressPatched.add('hono');
1509
+ try {
1510
+ const honoMod = exports;
1511
+ const HonoClass = honoMod.Hono || (honoMod.default && honoMod.default.Hono);
1512
+ if (HonoClass && typeof HonoClass === 'function') {
1513
+ const OrigHono = HonoClass;
1514
+ const WrappedHono = function (...args) {
1515
+ const app = new OrigHono(...args);
1516
+ try {
1517
+ (0, hono_1.instrumentHono)(app, { environment });
1518
+ if (debug) {
1519
+ console.log('[trickle/observe] Auto-instrumented Hono app');
1520
+ }
1521
+ }
1522
+ catch (e) {
1523
+ if (debug) {
1524
+ console.log('[trickle/observe] Hono instrumentation error:', e.message);
1525
+ }
1526
+ }
1527
+ return app;
1528
+ };
1529
+ WrappedHono.prototype = OrigHono.prototype;
1530
+ for (const key of Object.keys(OrigHono)) {
1531
+ WrappedHono[key] = OrigHono[key];
1532
+ }
1533
+ if (honoMod.Hono) {
1534
+ honoMod.Hono = WrappedHono;
1535
+ }
1536
+ try {
1537
+ const resolvedPath = M._resolveFilename(request, parent);
1538
+ if (require.cache[resolvedPath]) {
1539
+ const cached = require.cache[resolvedPath].exports;
1540
+ if (cached.Hono)
1541
+ cached.Hono = WrappedHono;
1542
+ }
1543
+ }
1544
+ catch { /* non-critical */ }
1545
+ }
1546
+ }
1547
+ catch { /* fall through */ }
1548
+ }
1505
1549
  // ── Database auto-detection: patch database drivers to capture SQL queries ──
1506
1550
  if (request === 'pg' && !expressPatched.has('pg')) {
1507
1551
  expressPatched.add('pg');
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "trickle-observe",
3
- "version": "0.2.127",
4
- "description": "Zero-code runtime observability for JavaScript/TypeScript. Auto-instruments Express, Fastify, Koa, OpenAI, Anthropic, Gemini, MCP. Captures functions, variables, LLM calls, agent workflows.",
3
+ "version": "0.2.128",
4
+ "description": "Zero-code runtime observability for JavaScript/TypeScript. Auto-instruments Express, Fastify, Koa, Hono, OpenAI, Anthropic, Gemini, MCP. Captures functions, variables, LLM calls, agent workflows.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {
package/src/hono.ts ADDED
@@ -0,0 +1,403 @@
1
+ import * as fs from 'fs';
2
+ import * as pathMod from 'path';
3
+ import { TypeNode, IngestPayload } from './types';
4
+ import { inferType } from './type-inference';
5
+ import { hashType } from './type-hash';
6
+ import { TypeCache } from './cache';
7
+ import { enqueue } from './transport';
8
+ import { detectEnvironment } from './env-detect';
9
+ import { withRequestContext } from './request-context';
10
+ import { traceCall, traceReturn } from './call-trace';
11
+
12
+ const honoCache = new TypeCache();
13
+
14
+ interface HonoInstrumentOpts {
15
+ enabled: boolean;
16
+ environment: string;
17
+ sampleRate: number;
18
+ maxDepth: number;
19
+ }
20
+
21
+ // ── Input extraction ──
22
+
23
+ async function extractHonoInput(c: any): Promise<Record<string, unknown>> {
24
+ const input: Record<string, unknown> = {};
25
+
26
+ try {
27
+ // Hono body: c.req.json() / c.req.text() — need to clone to avoid consuming
28
+ const contentType = c.req.header('content-type') || '';
29
+ if (contentType.includes('application/json')) {
30
+ try {
31
+ const body = await c.req.json();
32
+ if (body && typeof body === 'object' && Object.keys(body).length > 0) {
33
+ input.body = body;
34
+ }
35
+ } catch { /* body may not be parseable */ }
36
+ }
37
+ } catch {}
38
+
39
+ try {
40
+ // c.req.param() returns all params as an object
41
+ const params = c.req.param();
42
+ if (params && typeof params === 'object' && Object.keys(params).length > 0) {
43
+ input.params = params;
44
+ }
45
+ } catch {}
46
+
47
+ try {
48
+ // c.req.query() returns all query params
49
+ const query = c.req.query();
50
+ if (query && typeof query === 'object' && Object.keys(query).length > 0) {
51
+ input.query = query;
52
+ }
53
+ } catch {}
54
+
55
+ return input;
56
+ }
57
+
58
+ // ── Sample sanitization (local copy) ──
59
+
60
+ function sanitizeSample(value: unknown, depth: number = 3): unknown {
61
+ if (depth <= 0) return '[truncated]';
62
+ if (value === null || value === undefined) return value;
63
+
64
+ const t = typeof value;
65
+ if (t === 'string') {
66
+ const s = value as string;
67
+ return s.length > 200 ? s.substring(0, 200) + '...' : s;
68
+ }
69
+ if (t === 'number' || t === 'boolean') return value;
70
+ if (t === 'bigint') return String(value);
71
+ if (t === 'function') return `[Function: ${(value as Function).name || 'anonymous'}]`;
72
+
73
+ if (Array.isArray(value)) {
74
+ return value.slice(0, 5).map(item => sanitizeSample(item, depth - 1));
75
+ }
76
+
77
+ if (t === 'object') {
78
+ if (value instanceof Date) return value.toISOString();
79
+ if (value instanceof Error) return { error: value.message };
80
+ if (value instanceof Map) return `[Map: ${value.size} entries]`;
81
+ if (value instanceof Set) return `[Set: ${value.size} items]`;
82
+
83
+ const obj = value as Record<string, unknown>;
84
+ const result: Record<string, unknown> = {};
85
+ const keys = Object.keys(obj).slice(0, 20);
86
+ for (const key of keys) {
87
+ try {
88
+ result[key] = sanitizeSample(obj[key], depth - 1);
89
+ } catch {
90
+ result[key] = '[unreadable]';
91
+ }
92
+ }
93
+ return result;
94
+ }
95
+
96
+ return String(value);
97
+ }
98
+
99
+ // ── Error file writing ──
100
+
101
+ function writeErrorToFile(error: unknown, input: Record<string, unknown>, routeName: string): void {
102
+ try {
103
+ const err = error instanceof Error ? error : new Error(String(error));
104
+ const defaultDir = pathMod.join(process.cwd(), '.trickle');
105
+ const dir = process.env.TRICKLE_LOCAL_DIR || defaultDir;
106
+ try { fs.mkdirSync(dir, { recursive: true }); } catch {}
107
+
108
+ const stackLines = (err.stack || '').split('\n');
109
+ let errorFile: string | undefined;
110
+ let errorLine: number | undefined;
111
+ for (const sl of stackLines.slice(1)) {
112
+ const m = sl.match(/\((.+):(\d+):\d+\)/) || sl.match(/at (.+):(\d+):\d+/);
113
+ if (m && !m[1].includes('node_modules') && !m[1].includes('node:') && !m[1].includes('trickle-observe')) {
114
+ errorFile = m[1];
115
+ errorLine = parseInt(m[2]);
116
+ break;
117
+ }
118
+ }
119
+
120
+ const record = {
121
+ kind: 'error',
122
+ error: err.message,
123
+ type: err.constructor?.name || 'Error',
124
+ message: err.message,
125
+ file: errorFile,
126
+ line: errorLine,
127
+ stack: stackLines.slice(0, 6).join('\n'),
128
+ route: routeName,
129
+ request: input,
130
+ timestamp: new Date().toISOString(),
131
+ };
132
+
133
+ fs.appendFileSync(pathMod.join(dir, 'errors.jsonl'), JSON.stringify(record) + '\n');
134
+ } catch {
135
+ // Never crash the user's app
136
+ }
137
+ }
138
+
139
+ // ── Payload emission ──
140
+
141
+ function emitHonoPayload(
142
+ functionName: string,
143
+ environment: string,
144
+ maxDepth: number,
145
+ input: Record<string, unknown>,
146
+ output: unknown,
147
+ error?: unknown,
148
+ durationMs?: number,
149
+ ): void {
150
+ try {
151
+ const functionKey = `hono::${functionName}`;
152
+ const argsType = inferType(input, maxDepth);
153
+ const returnType = error ? ({ kind: 'unknown' } as TypeNode) : inferType(output, maxDepth);
154
+ const hash = hashType(argsType, returnType);
155
+
156
+ if (!error && !honoCache.shouldSend(functionKey, hash)) return;
157
+ if (!error) honoCache.markSent(functionKey, hash);
158
+
159
+ const payload: IngestPayload = {
160
+ functionName,
161
+ module: 'hono',
162
+ language: 'js',
163
+ environment,
164
+ typeHash: hash,
165
+ argsType,
166
+ returnType,
167
+ sampleInput: sanitizeSample(input),
168
+ sampleOutput: error ? undefined : sanitizeSample(output),
169
+ };
170
+
171
+ if (durationMs !== undefined) {
172
+ payload.durationMs = Math.round(durationMs * 100) / 100;
173
+ }
174
+
175
+ if (error) {
176
+ const err = error instanceof Error ? error : new Error(String(error));
177
+ payload.error = {
178
+ type: err.constructor?.name || 'Error',
179
+ message: err.message,
180
+ stackTrace: err.stack,
181
+ argsSnapshot: sanitizeSample(input),
182
+ };
183
+ writeErrorToFile(error, input, functionName);
184
+ }
185
+
186
+ enqueue(payload);
187
+ } catch {
188
+ // Never crash the user's app
189
+ }
190
+ }
191
+
192
+ // ── Public API ──
193
+
194
+ /**
195
+ * Instrument a Hono app by monkey-patching route registration methods.
196
+ *
197
+ * Must be called BEFORE routes are defined:
198
+ *
199
+ * import { Hono } from 'hono';
200
+ * import { instrumentHono } from 'trickle';
201
+ *
202
+ * const app = new Hono();
203
+ * instrumentHono(app);
204
+ *
205
+ * app.get('/api/users', (c) => c.json({ users: [] }));
206
+ *
207
+ * Captures:
208
+ * - Input: body (JSON), params, query from the Hono context
209
+ * - Output: the data passed to c.json() / c.text() or returned directly
210
+ * - Errors: exceptions thrown in handlers
211
+ * - Timing: request duration in milliseconds
212
+ */
213
+ export function instrumentHono(
214
+ app: any,
215
+ userOpts?: { enabled?: boolean; environment?: string; sampleRate?: number; maxDepth?: number },
216
+ ): void {
217
+ const opts: HonoInstrumentOpts = {
218
+ enabled: userOpts?.enabled !== false,
219
+ environment: userOpts?.environment || detectEnvironment(),
220
+ sampleRate: userOpts?.sampleRate ?? 1,
221
+ maxDepth: userOpts?.maxDepth ?? 5,
222
+ };
223
+
224
+ if (!opts.enabled) return;
225
+
226
+ const methods = ['get', 'post', 'put', 'delete', 'patch', 'all', 'options', 'head'] as const;
227
+
228
+ for (const method of methods) {
229
+ const original = app[method];
230
+ if (typeof original !== 'function') continue;
231
+
232
+ app[method] = function (this: any, path: string, ...handlers: any[]) {
233
+ const pathStr = typeof path === 'string' ? path : String(path);
234
+ const routeName = `${method.toUpperCase()} ${pathStr}`;
235
+
236
+ const wrapped = handlers.map((handler: any) => {
237
+ if (typeof handler !== 'function') return handler;
238
+
239
+ return wrapHonoHandler(handler, routeName, opts);
240
+ });
241
+
242
+ return original.call(this, path, ...wrapped);
243
+ };
244
+ }
245
+ }
246
+
247
+ function wrapHonoHandler(
248
+ handler: Function,
249
+ routeName: string,
250
+ opts: HonoInstrumentOpts,
251
+ ): Function {
252
+ const wrapped = async function (this: any, c: any, next?: any) {
253
+ // Sample rate check
254
+ if (opts.sampleRate < 1 && Math.random() > opts.sampleRate) {
255
+ return handler.call(this, c, next);
256
+ }
257
+
258
+ const startTime = performance.now();
259
+ const callId = traceCall(routeName, 'hono');
260
+
261
+ // Extract input (async because body parsing is async in Hono)
262
+ let input: Record<string, unknown> = {};
263
+ try {
264
+ input = await extractHonoInput(c);
265
+ } catch {}
266
+
267
+ // Intercept c.json() to capture output
268
+ let captured = false;
269
+ const originalJson = c.json;
270
+ if (typeof originalJson === 'function') {
271
+ c.json = function (data: any, ...args: any[]) {
272
+ if (!captured) {
273
+ captured = true;
274
+ const durationMs = performance.now() - startTime;
275
+ traceReturn(callId, routeName, 'hono', durationMs);
276
+ emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, data, undefined, durationMs);
277
+ }
278
+ return originalJson.call(c, data, ...args);
279
+ };
280
+ }
281
+
282
+ // Intercept c.text() for text responses
283
+ const originalText = c.text;
284
+ if (typeof originalText === 'function') {
285
+ c.text = function (data: any, ...args: any[]) {
286
+ if (!captured) {
287
+ captured = true;
288
+ const durationMs = performance.now() - startTime;
289
+ traceReturn(callId, routeName, 'hono', durationMs);
290
+ emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, { __text: true }, undefined, durationMs);
291
+ }
292
+ return originalText.call(c, data, ...args);
293
+ };
294
+ }
295
+
296
+ try {
297
+ const result = await handler.call(this, c, next);
298
+
299
+ // Hono handlers can return a Response object directly
300
+ if (result && !captured) {
301
+ captured = true;
302
+ const durationMs = performance.now() - startTime;
303
+ traceReturn(callId, routeName, 'hono', durationMs);
304
+
305
+ // Try to extract JSON from the Response
306
+ if (result instanceof Response) {
307
+ try {
308
+ const cloned = result.clone();
309
+ const ct = cloned.headers.get('content-type') || '';
310
+ if (ct.includes('application/json')) {
311
+ const body = await cloned.json();
312
+ emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, body, undefined, durationMs);
313
+ } else {
314
+ emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, { __response: true }, undefined, durationMs);
315
+ }
316
+ } catch {
317
+ emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, { __response: true }, undefined, durationMs);
318
+ }
319
+ } else {
320
+ emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, result, undefined, durationMs);
321
+ }
322
+ }
323
+
324
+ return result;
325
+ } catch (err) {
326
+ if (!captured) {
327
+ captured = true;
328
+ const durationMs = performance.now() - startTime;
329
+ traceReturn(callId, routeName, 'hono', durationMs, (err instanceof Error ? err : new Error(String(err))).message);
330
+ emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, undefined, err, durationMs);
331
+ }
332
+ throw err;
333
+ }
334
+ };
335
+
336
+ Object.defineProperty(wrapped, 'name', { value: handler.name || routeName, configurable: true });
337
+ Object.defineProperty(wrapped, 'length', { value: handler.length, configurable: true });
338
+
339
+ return wrapped;
340
+ }
341
+
342
+ /**
343
+ * Hono middleware for observability. Use this as an alternative to
344
+ * monkey-patching route methods:
345
+ *
346
+ * import { Hono } from 'hono';
347
+ * import { trickleHonoMiddleware } from 'trickle';
348
+ *
349
+ * const app = new Hono();
350
+ * app.use('*', trickleHonoMiddleware());
351
+ */
352
+ export function trickleHonoMiddleware(
353
+ userOpts?: { enabled?: boolean; environment?: string; sampleRate?: number; maxDepth?: number },
354
+ ): (c: any, next: () => Promise<void>) => Promise<void | Response> {
355
+ const opts: HonoInstrumentOpts = {
356
+ enabled: userOpts?.enabled !== false,
357
+ environment: userOpts?.environment || detectEnvironment(),
358
+ sampleRate: userOpts?.sampleRate ?? 1,
359
+ maxDepth: userOpts?.maxDepth ?? 5,
360
+ };
361
+
362
+ return async function trickleHonoMw(c: any, next: () => Promise<void>): Promise<void | Response> {
363
+ if (!opts.enabled) return next();
364
+ if (opts.sampleRate < 1 && Math.random() > opts.sampleRate) return next();
365
+
366
+ const startTime = performance.now();
367
+ const routeName = `${c.req.method} ${c.req.path}`;
368
+ const callId = traceCall(routeName, 'hono');
369
+
370
+ let input: Record<string, unknown> = {};
371
+ try {
372
+ input = await extractHonoInput(c);
373
+ } catch {}
374
+
375
+ try {
376
+ await next();
377
+
378
+ const durationMs = performance.now() - startTime;
379
+ traceReturn(callId, routeName, 'hono', durationMs);
380
+
381
+ // After next(), capture from c.res if available
382
+ if (c.res) {
383
+ try {
384
+ const ct = c.res.headers?.get('content-type') || '';
385
+ if (ct.includes('application/json')) {
386
+ const cloned = c.res.clone();
387
+ const body = await cloned.json();
388
+ emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, body, undefined, durationMs);
389
+ } else {
390
+ emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, { __response: true }, undefined, durationMs);
391
+ }
392
+ } catch {
393
+ emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, { __response: true }, undefined, durationMs);
394
+ }
395
+ }
396
+ } catch (err) {
397
+ const durationMs = performance.now() - startTime;
398
+ traceReturn(callId, routeName, 'hono', durationMs, (err instanceof Error ? err : new Error(String(err))).message);
399
+ emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, undefined, err, durationMs);
400
+ throw err;
401
+ }
402
+ };
403
+ }
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ import { GlobalOpts, TrickleOpts, WrapOptions } from './types';
5
5
  import { instrumentExpress, trickleMiddleware } from './express';
6
6
  import { instrumentFastify, tricklePlugin } from './fastify';
7
7
  import { instrumentKoa, instrumentKoaRouter } from './koa';
8
+ import { instrumentHono, trickleHonoMiddleware } from './hono';
8
9
 
9
10
  let globalOpts: GlobalOpts = {
10
11
  backendUrl: 'http://localhost:4888',
@@ -135,7 +136,7 @@ export function trickleExpress(
135
136
  }
136
137
 
137
138
  /**
138
- * Auto-instrument a framework app. Supports Express, Fastify, and Koa.
139
+ * Auto-instrument a framework app. Supports Express, Fastify, Koa, and Hono.
139
140
  *
140
141
  * Usage:
141
142
  * import { instrument } from 'trickle';
@@ -164,6 +165,13 @@ export function instrument(
164
165
  maxDepth: opts?.maxDepth ?? 5,
165
166
  };
166
167
 
168
+ // Detect Hono: has .fetch (bound method), .route(), .get(), but NOT .listen() on the app itself
169
+ // Hono apps use serve() from @hono/node-server rather than app.listen()
170
+ if (typeof app.fetch === 'function' && typeof app.get === 'function' && typeof app.route === 'function' && typeof app.fire === 'function') {
171
+ instrumentHono(app, mergedOpts);
172
+ return;
173
+ }
174
+
167
175
  // Detect Fastify: has .route(), .register(), .addHook()
168
176
  if (typeof app.route === 'function' && typeof app.register === 'function' && typeof app.addHook === 'function') {
169
177
  instrumentFastify(app, mergedOpts);
@@ -227,6 +235,7 @@ export { flush } from './transport';
227
235
  export { instrumentExpress, trickleMiddleware } from './express';
228
236
  export { instrumentFastify, tricklePlugin } from './fastify';
229
237
  export { instrumentKoa, instrumentKoaRouter } from './koa';
238
+ export { instrumentHono, trickleHonoMiddleware } from './hono';
230
239
  export { observe, observeFn } from './observe';
231
240
  export type { ObserveOpts } from './observe';
232
241
  export { wrapFunction } from './wrap';
@@ -38,6 +38,7 @@ import { patchFetch } from './fetch-observer';
38
38
  import { instrumentExpress, trickleMiddleware } from './express';
39
39
  import { instrumentFastify } from './fastify';
40
40
  import { instrumentKoa } from './koa';
41
+ import { instrumentHono } from './hono';
41
42
  import { initVarTracer, traceVar } from './trace-var';
42
43
  import { initCallTrace } from './call-trace';
43
44
  import { initLlmObserver } from './llm-observer';
@@ -1483,6 +1484,47 @@ if (enabled) {
1483
1484
  } catch { /* fall through */ }
1484
1485
  }
1485
1486
 
1487
+ // ── Hono auto-detection ──
1488
+ if (request === 'hono' && !expressPatched.has('hono')) {
1489
+ expressPatched.add('hono');
1490
+ try {
1491
+ const honoMod = exports;
1492
+ const HonoClass = honoMod.Hono || (honoMod.default && honoMod.default.Hono);
1493
+ if (HonoClass && typeof HonoClass === 'function') {
1494
+ const OrigHono = HonoClass;
1495
+ const WrappedHono = function (this: any, ...args: any[]): any {
1496
+ const app = new OrigHono(...args);
1497
+ try {
1498
+ instrumentHono(app, { environment });
1499
+ if (debug) {
1500
+ console.log('[trickle/observe] Auto-instrumented Hono app');
1501
+ }
1502
+ } catch (e: unknown) {
1503
+ if (debug) {
1504
+ console.log('[trickle/observe] Hono instrumentation error:', (e as Error).message);
1505
+ }
1506
+ }
1507
+ return app;
1508
+ };
1509
+ WrappedHono.prototype = OrigHono.prototype;
1510
+ for (const key of Object.keys(OrigHono)) {
1511
+ (WrappedHono as any)[key] = (OrigHono as any)[key];
1512
+ }
1513
+
1514
+ if (honoMod.Hono) {
1515
+ honoMod.Hono = WrappedHono;
1516
+ }
1517
+ try {
1518
+ const resolvedPath = M._resolveFilename(request, parent);
1519
+ if (require.cache[resolvedPath]) {
1520
+ const cached = require.cache[resolvedPath]!.exports;
1521
+ if (cached.Hono) cached.Hono = WrappedHono;
1522
+ }
1523
+ } catch { /* non-critical */ }
1524
+ }
1525
+ } catch { /* fall through */ }
1526
+ }
1527
+
1486
1528
  // ── Database auto-detection: patch database drivers to capture SQL queries ──
1487
1529
  if (request === 'pg' && !expressPatched.has('pg')) {
1488
1530
  expressPatched.add('pg');