peekinside 1.2.5 → 1.3.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/README.md +5 -2
- package/index.d.ts +41 -12
- package/package.json +10 -1
- package/src/PeekInside.js +70 -14
- package/src/capture/exchange-recorder.js +5 -12
- package/src/capture/http-message.js +17 -0
- package/src/dumps/dump-capture.js +100 -0
- package/src/dumps/dump-record.js +57 -0
- package/src/dumps/dump-value.js +428 -0
- package/src/exceptions/exception-capture.js +5 -4
- package/src/exceptions/request-context.js +118 -10
- package/src/hub/ingest-payload.js +66 -6
- package/src/hub/project-log.js +71 -4
- package/src/hub/server.js +18 -7
- package/src/hub/spool-ingester.js +1 -0
- package/src/hub/store.js +44 -1
- package/src/instrumentation/index.js +48 -3
- package/src/instrumentation/jobs.js +93 -2
- package/src/instrumentation/loggers.js +78 -4
- package/src/integrations/fastify.js +18 -1
- package/src/integrations/hono.js +209 -0
- package/src/integrations/nest.js +277 -0
- package/src/integrations/server-binder.js +9 -6
- package/src/logs/log-capture.js +5 -5
- package/src/logs/log-record.js +2 -2
- package/src/record.js +6 -2
- package/src/timeline/timeline.js +24 -3
- package/src/transport/batch.js +4 -1
- package/src/transport/cloud.js +12 -0
- package/src/transport/local.js +7 -5
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Job runs from the common job libraries, without code changes: node-cron and cron (scheduled
|
|
3
|
-
* tasks, with their cron expression, so PeekInside can tell when one misses its run)
|
|
3
|
+
* tasks, with their cron expression, so PeekInside can tell when one misses its run), BullMQ
|
|
4
4
|
* (queue workers, with the queue, the attempt and the job's data, and the schedule of repeatable
|
|
5
|
-
* jobs)
|
|
5
|
+
* jobs) and @nestjs/schedule (@Cron, @Interval and @Timeout methods, named after their class and
|
|
6
|
+
* method). Each run goes through JobRunner, exactly as PeekInside.job() would run it.
|
|
6
7
|
*
|
|
7
8
|
* A schedule is recorded with its time zone: the one given to the library, else the process's
|
|
8
9
|
* own, which is the one these libraries read a cron expression in without one.
|
|
@@ -20,6 +21,10 @@ import { JobRunner } from '../jobs/job-runner.js';
|
|
|
20
21
|
const kWrapped = Symbol('peekInside.job');
|
|
21
22
|
// node-cron tasks created through named imports are found by looking again now and then.
|
|
22
23
|
const RESCAN_MS = 30_000;
|
|
24
|
+
// @nestjs/schedule's SchedulerType.
|
|
25
|
+
const NEST_CRON = 1;
|
|
26
|
+
const NEST_TIMEOUT = 2;
|
|
27
|
+
const NEST_INTERVAL = 3;
|
|
23
28
|
|
|
24
29
|
/**
|
|
25
30
|
* Patches job libraries.
|
|
@@ -113,6 +118,67 @@ export class JobInstrumentation {
|
|
|
113
118
|
return true;
|
|
114
119
|
}
|
|
115
120
|
|
|
121
|
+
/**
|
|
122
|
+
* @nestjs/schedule: every @Cron, @Interval and @Timeout method runs as a job run, named after the
|
|
123
|
+
* name given to the decorator, else its class and method ("ReportsService.sendDigest"), with its
|
|
124
|
+
* schedule. The scheduler catches what a method throws and only logs it, so a failing method would
|
|
125
|
+
* otherwise look like one that ran fine: the run records the error, then the scheduler logs it as
|
|
126
|
+
* before. Methods are wrapped as the scheduler registers them, when the app initializes.
|
|
127
|
+
*
|
|
128
|
+
* @param {*} explorer - The ScheduleExplorer class, or the app's instance of it
|
|
129
|
+
* @returns {boolean}
|
|
130
|
+
*/
|
|
131
|
+
static nestSchedule(explorer) {
|
|
132
|
+
const proto = typeof explorer === 'function' ? explorer.prototype : explorer && typeof explorer === 'object' ? Object.getPrototypeOf(explorer) : null;
|
|
133
|
+
if (!proto || typeof proto.wrapFunctionInTryCatchBlocks !== 'function') return false;
|
|
134
|
+
if (proto[kWrapped]) return true;
|
|
135
|
+
const original = proto.wrapFunctionInTryCatchBlocks;
|
|
136
|
+
proto.wrapFunctionInTryCatchBlocks = function (methodRef, instance) {
|
|
137
|
+
let job = null;
|
|
138
|
+
try {
|
|
139
|
+
job = typeof methodRef === 'function' ? nestJob(this, methodRef, instance) : null;
|
|
140
|
+
} catch (error) {
|
|
141
|
+
job = null;
|
|
142
|
+
}
|
|
143
|
+
if (!job) return original.apply(this, arguments);
|
|
144
|
+
const run = function () {
|
|
145
|
+
const args = arguments;
|
|
146
|
+
return JobRunner.run(job.name, () => methodRef.apply(this, args), job.options);
|
|
147
|
+
};
|
|
148
|
+
// cron fires the wrapper as a CronJob callback: it is a run already.
|
|
149
|
+
return JobInstrumentation.markRun(original.call(this, run, instance));
|
|
150
|
+
};
|
|
151
|
+
Object.defineProperty(proto, kWrapped, { value: true });
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Whether a function records its own job runs (see markRun()).
|
|
157
|
+
*
|
|
158
|
+
* @param {*} fn
|
|
159
|
+
* @returns {boolean}
|
|
160
|
+
*/
|
|
161
|
+
static isRun(fn) {
|
|
162
|
+
return typeof fn === 'function' && Boolean(fn[kWrapped]);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Marks a function that records its own job runs, so the cron patch doesn't record it again.
|
|
167
|
+
*
|
|
168
|
+
* @param {*} fn
|
|
169
|
+
* @returns {*} fn
|
|
170
|
+
*/
|
|
171
|
+
static markRun(fn) {
|
|
172
|
+
if (typeof fn === 'function' && !fn[kWrapped]) {
|
|
173
|
+
try {
|
|
174
|
+
Object.defineProperty(fn, kWrapped, { value: true });
|
|
175
|
+
} catch (error) {
|
|
176
|
+
// A frozen function is recorded by both, which costs a run, never the job.
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return fn;
|
|
180
|
+
}
|
|
181
|
+
|
|
116
182
|
/**
|
|
117
183
|
* Patches the ES module build of a package installed in the project, when it has one.
|
|
118
184
|
*
|
|
@@ -175,6 +241,31 @@ function wrapCronJobCallback(job, callback) {
|
|
|
175
241
|
return wrapped;
|
|
176
242
|
}
|
|
177
243
|
|
|
244
|
+
// What a @nestjs/schedule method runs as: its name, and its schedule from the decorator's
|
|
245
|
+
// metadata (a cron expression, or "@every <ms>ms" for an interval; a timeout runs once).
|
|
246
|
+
function nestJob(explorer, methodRef, instance) {
|
|
247
|
+
const metadata = explorer.metadataAccessor;
|
|
248
|
+
const read = (method) => (metadata && typeof metadata[method] === 'function' ? metadata[method](methodRef) : undefined);
|
|
249
|
+
const owner = instance && instance.constructor && typeof instance.constructor.name === 'string' ? instance.constructor.name : '';
|
|
250
|
+
const method = typeof methodRef.name === 'string' ? methodRef.name : '';
|
|
251
|
+
const fallback = owner && method ? `${owner}.${method}` : method || owner || 'job';
|
|
252
|
+
const named = (value) => (typeof value === 'string' && value.trim() ? value : fallback);
|
|
253
|
+
const type = read('getSchedulerType');
|
|
254
|
+
if (type === NEST_CRON) {
|
|
255
|
+
const cron = read('getCronMetadata') || {};
|
|
256
|
+
const schedule = typeof cron.cronTime === 'string' && cron.cronTime.trim() ? cron.cronTime : undefined;
|
|
257
|
+
const timezone = typeof cron.timeZone === 'string' && cron.timeZone ? cron.timeZone : schedule && cron.utcOffset === undefined ? localTimeZone() : undefined;
|
|
258
|
+
return { name: named(cron.name), options: { schedule, timezone, trigger: 'cron' } };
|
|
259
|
+
}
|
|
260
|
+
if (type === NEST_INTERVAL) {
|
|
261
|
+
const interval = read('getIntervalMetadata') || {};
|
|
262
|
+
const every = Number.isFinite(interval.timeout) && interval.timeout > 0 ? `@every ${interval.timeout}ms` : undefined;
|
|
263
|
+
return { name: named(read('getSchedulerName')), options: { schedule: every, trigger: 'cron' } };
|
|
264
|
+
}
|
|
265
|
+
if (type === NEST_TIMEOUT) return { name: named(read('getSchedulerName')), options: { trigger: 'cron' } };
|
|
266
|
+
return { name: fallback, options: { trigger: 'cron' } };
|
|
267
|
+
}
|
|
268
|
+
|
|
178
269
|
// The process's time zone (TZ, else the system's).
|
|
179
270
|
function localTimeZone() {
|
|
180
271
|
try {
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Lines written with pino and
|
|
3
|
-
* run) they belong to, and on the Logs page. Only lines the logger actually writes
|
|
4
|
-
* (a level it has turned off is skipped), and what the logger writes is untouched.
|
|
2
|
+
* Lines written with pino, winston and NestJS's Logger, like console lines: on the timeline of the
|
|
3
|
+
* request (or job run) they belong to, and on the Logs page. Only lines the logger actually writes
|
|
4
|
+
* are recorded (a level it has turned off is skipped), and what the logger writes is untouched.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Each is patched on a prototype every logger shares, so loggers created before PeekInside, child
|
|
7
7
|
* loggers and loggers imported as ES modules are all covered.
|
|
8
|
+
*
|
|
9
|
+
* Nest's default logger (ConsoleLogger) writes to process.stdout itself, past the console. Its
|
|
10
|
+
* lines keep the class that wrote them ("[OrdersService] Charging card"), and the redaction the
|
|
11
|
+
* app set up applies to them too. Nest's own startup lines (modules loaded, routes mapped) are
|
|
12
|
+
* left out; its warnings and errors are not.
|
|
8
13
|
*/
|
|
9
14
|
|
|
15
|
+
import { inspect } from 'util';
|
|
10
16
|
import { LogCapture } from '../logs/log-capture.js';
|
|
11
17
|
import { Timeline } from '../timeline/timeline.js';
|
|
12
18
|
|
|
@@ -16,6 +22,10 @@ const PINO_LEVELS = { trace: 'debug', debug: 'debug', info: 'info', warn: 'warn'
|
|
|
16
22
|
const WINSTON_LEVELS = { error: 'error', warn: 'warn', info: 'info', http: 'info', verbose: 'debug', debug: 'debug', silly: 'debug' };
|
|
17
23
|
// Keys of a winston `info` that aren't the line's own data.
|
|
18
24
|
const WINSTON_OWN = new Set(['level', 'message', 'timestamp', 'splat']);
|
|
25
|
+
const NEST_LEVELS = { log: 'info', error: 'error', warn: 'warn', debug: 'debug', verbose: 'debug', fatal: 'error' };
|
|
26
|
+
// Nest's own classes that log while the app starts: their info lines are startup noise.
|
|
27
|
+
const NEST_STARTUP = new Set(['NestFactory', 'InstanceLoader', 'RoutesResolver', 'RouterExplorer', 'NestApplication', 'NestMicroservice', 'WebSocketsController']);
|
|
28
|
+
const INSPECT = { depth: 4, breakLength: Infinity, compact: true };
|
|
19
29
|
|
|
20
30
|
/**
|
|
21
31
|
* Patches logging libraries.
|
|
@@ -88,6 +98,70 @@ export class LoggerInstrumentation {
|
|
|
88
98
|
Object.defineProperty(proto, kPatched, { value: true });
|
|
89
99
|
return true;
|
|
90
100
|
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Patches NestJS's ConsoleLogger, the logger behind Nest's Logger unless the app set its own:
|
|
104
|
+
* every line goes through printMessages(), after the level check.
|
|
105
|
+
*
|
|
106
|
+
* @param {*} target - A ConsoleLogger (or subclass) instance or class
|
|
107
|
+
* @returns {boolean} Whether it is (now) patched
|
|
108
|
+
*/
|
|
109
|
+
static nest(target) {
|
|
110
|
+
let proto = typeof target === 'function' ? target.prototype : target;
|
|
111
|
+
while (proto && !(Object.prototype.hasOwnProperty.call(proto, 'printMessages') && typeof proto.formatPid === 'function')) {
|
|
112
|
+
proto = Object.getPrototypeOf(proto);
|
|
113
|
+
}
|
|
114
|
+
if (!proto || typeof proto.printMessages !== 'function') return false;
|
|
115
|
+
if (proto[kPatched]) return true;
|
|
116
|
+
const original = proto.printMessages;
|
|
117
|
+
proto.printMessages = function (messages) {
|
|
118
|
+
const args = Array.prototype.slice.call(arguments);
|
|
119
|
+
try {
|
|
120
|
+
// Nest 12 resolves lazy messages (functions) while printing: they are resolved once here,
|
|
121
|
+
// and the logger prints the values.
|
|
122
|
+
if (Array.isArray(messages) && typeof this.resolveMessage === 'function') {
|
|
123
|
+
args[0] = messages.map((message) => (typeof message === 'function' ? this.resolveMessage(message) : message));
|
|
124
|
+
}
|
|
125
|
+
nestLine(this, args[0], args[1], args[2], args[4], args[5]);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
// The line is written all the same.
|
|
128
|
+
}
|
|
129
|
+
return original.apply(this, args);
|
|
130
|
+
};
|
|
131
|
+
Object.defineProperty(proto, kPatched, { value: true });
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// One call of ConsoleLogger#printMessages(messages, context, logLevel, writeStreamType, errorStack,
|
|
137
|
+
// params): a line per message, as Nest prints them.
|
|
138
|
+
function nestLine(logger, messages, context, logLevel, errorStack, params) {
|
|
139
|
+
// With forceConsole, Nest prints through the console, which records the lines.
|
|
140
|
+
if (!Array.isArray(messages) || (logger.options && logger.options.forceConsole)) return;
|
|
141
|
+
const level = NEST_LEVELS[logLevel] || 'info';
|
|
142
|
+
const where = typeof context === 'string' ? context : '';
|
|
143
|
+
if (NEST_STARTUP.has(where) && (level === 'info' || level === 'debug')) return;
|
|
144
|
+
const timeline = Timeline.current();
|
|
145
|
+
if (!(timeline && timeline.settings.captureLogs) && !LogCapture.wants(level)) return;
|
|
146
|
+
|
|
147
|
+
const redact = logger.redactor && typeof logger.redact === 'function' ? (value) => logger.redact(value) : (value) => value;
|
|
148
|
+
const fields = {};
|
|
149
|
+
if (params && typeof params === 'object') Object.assign(fields, redact(params));
|
|
150
|
+
if (where) fields.context = where;
|
|
151
|
+
if (typeof errorStack === 'string' && errorStack) fields.stack = errorStack;
|
|
152
|
+
for (const message of messages) {
|
|
153
|
+
if (typeof message === 'function') continue;
|
|
154
|
+
const value = redact(message);
|
|
155
|
+
const own = value instanceof Error ? { ...fields, err: errorFields(value), ...(fields.stack ? {} : { stack: String(value.stack || '') }) } : fields;
|
|
156
|
+
const text = nestText(value);
|
|
157
|
+
capture(level, where ? `[${where}] ${text}` : text, Object.keys(own).length ? own : undefined, 'nest');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function nestText(value) {
|
|
162
|
+
if (typeof value === 'string') return value;
|
|
163
|
+
if (value instanceof Error) return `${value.name}: ${value.message}`;
|
|
164
|
+
return inspect(value, INSPECT);
|
|
91
165
|
}
|
|
92
166
|
|
|
93
167
|
function capture(level, message, fields, source) {
|
|
@@ -10,6 +10,7 @@ import { FrameworkAdapter } from './framework-adapter.js';
|
|
|
10
10
|
* Records requests from a Fastify onRequest hook, and errors from an onError hook.
|
|
11
11
|
*
|
|
12
12
|
* onRequest runs before Fastify reads the body, so bodies are captured as Fastify parses them.
|
|
13
|
+
* It runs once Fastify has routed the request, so the route pattern and parameters are known.
|
|
13
14
|
* Hooks must be added before the app is ready, so attach before fastify.listen().
|
|
14
15
|
*/
|
|
15
16
|
export class FastifyAdapter extends FrameworkAdapter {
|
|
@@ -36,7 +37,10 @@ export class FastifyAdapter extends FrameworkAdapter {
|
|
|
36
37
|
attach(fastify) {
|
|
37
38
|
fastify.addHook('onRequest', (request, reply, done) => {
|
|
38
39
|
ExchangeRecorder.setBodySource(request.raw, () => request.body);
|
|
39
|
-
this.peekInside._intercept(request.raw, reply.raw,
|
|
40
|
+
this.peekInside._intercept(request.raw, reply.raw, () => {
|
|
41
|
+
FastifyAdapter._noteRoute(request);
|
|
42
|
+
done();
|
|
43
|
+
}, 'fastify');
|
|
40
44
|
});
|
|
41
45
|
if (this.peekInside.options.captureExceptions !== false) {
|
|
42
46
|
fastify.addHook('onError', (request, reply, error, done) => {
|
|
@@ -49,6 +53,19 @@ export class FastifyAdapter extends FrameworkAdapter {
|
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
// The route Fastify matched (routerPath before Fastify 4.10); parameters stay on its request.
|
|
57
|
+
static _noteRoute(request) {
|
|
58
|
+
try {
|
|
59
|
+
const context = RequestContext.of(request.raw);
|
|
60
|
+
if (!context) return;
|
|
61
|
+
const options = request.routeOptions;
|
|
62
|
+
const route = options && typeof options === 'object' ? options.url : request.routerPath;
|
|
63
|
+
context.noteRoute({ route: typeof route === 'string' ? route : null, params: () => request.params });
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// The request is recorded without them.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
52
69
|
static _noteError(request, error) {
|
|
53
70
|
try {
|
|
54
71
|
const context = RequestContext.of(request.raw);
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hono integration, for Hono apps served on Node.js by @hono/node-server.
|
|
3
|
+
*
|
|
4
|
+
* node-server hands each request to app.fetch() along with the Node.js request and response
|
|
5
|
+
* (`env.incoming` and `env.outgoing`), so app.fetch is wrapped to record them; the rest of the
|
|
6
|
+
* request runs inside its context. node-server reads app.fetch when serve() is called, so attach
|
|
7
|
+
* before serve().
|
|
8
|
+
*
|
|
9
|
+
* Hono catches what a handler throws and passes it to the app's error handler (app.onError()), or
|
|
10
|
+
* to its default one, which answers 500 and logs it. The error handler is wrapped, whenever the app
|
|
11
|
+
* sets it, to see each error: handled when the app set its own handler, unhandled with Hono's. A
|
|
12
|
+
* sub-app mounted with its own error handler (app.route()) answers its errors itself; they are found
|
|
13
|
+
* on the context (c.error) once the route's handler returns. HTTPExceptions with a 4xx status are
|
|
14
|
+
* how an app answers bad requests, and aren't recorded.
|
|
15
|
+
*
|
|
16
|
+
* Route patterns ("/users/:id") come from the router: each handler it matches for a request is
|
|
17
|
+
* wrapped to note its route when it runs, and to forget it when it hands the request on (next()),
|
|
18
|
+
* so a request's route is the one of the handler that answered it, wherever PeekInside was attached
|
|
19
|
+
* among the app's routes and middleware.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { RequestContext } from '../exceptions/request-context.js';
|
|
23
|
+
import { FrameworkAdapter } from './framework-adapter.js';
|
|
24
|
+
|
|
25
|
+
const kAttached = Symbol('peekInside.hono');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Records a Hono app's requests from app.fetch(), its errors from its error handler, and its routes
|
|
29
|
+
* from its router.
|
|
30
|
+
*/
|
|
31
|
+
export class HonoAdapter extends FrameworkAdapter {
|
|
32
|
+
static framework = 'hono';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {*} app
|
|
36
|
+
* @returns {boolean} Whether `app` looks like a Hono app
|
|
37
|
+
*/
|
|
38
|
+
static matches(app) {
|
|
39
|
+
return Boolean(app) && typeof app.fetch === 'function' && typeof app.route === 'function' && typeof app.onError === 'function' && Array.isArray(app.routes);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Wraps app.fetch, the error handler and the router's match(). An app is wrapped once.
|
|
44
|
+
*
|
|
45
|
+
* @param {Object} app - Hono app
|
|
46
|
+
* @returns {void}
|
|
47
|
+
*/
|
|
48
|
+
attach(app) {
|
|
49
|
+
if (app[kAttached]) return;
|
|
50
|
+
Object.defineProperty(app, kAttached, { value: true });
|
|
51
|
+
if (this.peekInside.options.captureExceptions !== false) wrapErrorHandler(app);
|
|
52
|
+
wrapRouter(app.router);
|
|
53
|
+
wrapFetch(app, this.peekInside);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function wrapFetch(app, peekInside) {
|
|
58
|
+
const original = app.fetch;
|
|
59
|
+
app.fetch = function fetch(request, env) {
|
|
60
|
+
const incoming = env && env.incoming;
|
|
61
|
+
const outgoing = env && env.outgoing;
|
|
62
|
+
// Elsewhere than node-server (app.request() in tests, other runtimes): nothing to record.
|
|
63
|
+
if (!incoming || !outgoing || typeof outgoing.on !== 'function') return original.apply(this, arguments);
|
|
64
|
+
const args = arguments;
|
|
65
|
+
return peekInside._intercept(incoming, outgoing, () => dispatch(original, this, args, incoming), 'hono');
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// What escapes app.fetch() is what Hono itself didn't handle: a value thrown that isn't an Error,
|
|
70
|
+
// or an error handler that threw. node-server answers it with a 500.
|
|
71
|
+
function dispatch(original, self, args, incoming) {
|
|
72
|
+
let result;
|
|
73
|
+
try {
|
|
74
|
+
result = original.apply(self, args);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
noteError(incoming, error, false);
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
if (result && typeof result.then === 'function') {
|
|
80
|
+
return result.then(undefined, (error) => {
|
|
81
|
+
noteError(incoming, error, false);
|
|
82
|
+
throw error;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// app.errorHandler becomes an accessor: setting it (app.onError() does) keeps the wrapper in front.
|
|
89
|
+
function wrapErrorHandler(app) {
|
|
90
|
+
const isDefault = defaultHandlerTest(app);
|
|
91
|
+
let current = app.errorHandler;
|
|
92
|
+
if (typeof current !== 'function') return;
|
|
93
|
+
const wrapper = function (error, c) {
|
|
94
|
+
noteError(c && c.env && c.env.incoming, error, !isDefault(current));
|
|
95
|
+
return current.apply(this, arguments);
|
|
96
|
+
};
|
|
97
|
+
Object.defineProperty(app, 'errorHandler', {
|
|
98
|
+
configurable: true,
|
|
99
|
+
enumerable: true,
|
|
100
|
+
get() {
|
|
101
|
+
return wrapper;
|
|
102
|
+
},
|
|
103
|
+
set(handler) {
|
|
104
|
+
current = handler;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Hono's own error handler is the one a new app of the same class has; when one can't be made,
|
|
110
|
+
// it is known by its name.
|
|
111
|
+
function defaultHandlerTest(app) {
|
|
112
|
+
let fallback = null;
|
|
113
|
+
try {
|
|
114
|
+
const probe = typeof app.constructor === 'function' ? new app.constructor() : null;
|
|
115
|
+
fallback = probe && typeof probe.errorHandler === 'function' ? probe.errorHandler : null;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
fallback = null;
|
|
118
|
+
}
|
|
119
|
+
return (handler) => (fallback ? handler === fallback : Boolean(handler) && handler.name === 'errorHandler');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// The router's match() becomes an accessor too: Hono's routers replace it on first use (and the
|
|
123
|
+
// RegExpRouter then calls the replacement, whose result is wrapped already).
|
|
124
|
+
function wrapRouter(router) {
|
|
125
|
+
if (!router || typeof router.match !== 'function') return;
|
|
126
|
+
let current = router.match;
|
|
127
|
+
const cache = { tuples: new WeakMap(), lists: new WeakMap(), own: new WeakSet() };
|
|
128
|
+
const match = function match() {
|
|
129
|
+
return withRoutes(current.apply(this, arguments), cache);
|
|
130
|
+
};
|
|
131
|
+
Object.defineProperty(router, 'match', {
|
|
132
|
+
configurable: true,
|
|
133
|
+
get() {
|
|
134
|
+
return match;
|
|
135
|
+
},
|
|
136
|
+
set(fn) {
|
|
137
|
+
current = fn;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// A match result is [[[handler, route], params][], stash?]: the same result, its handlers wrapped.
|
|
143
|
+
// Lists and handlers are wrapped once each (the RegExpRouter returns the same list for a route).
|
|
144
|
+
function withRoutes(result, { tuples, lists, own }) {
|
|
145
|
+
if (!Array.isArray(result) || !Array.isArray(result[0]) || own.has(result[0])) return result;
|
|
146
|
+
const handlers = result[0];
|
|
147
|
+
let wrapped = lists.get(handlers);
|
|
148
|
+
if (!wrapped) {
|
|
149
|
+
wrapped = handlers.map((entry) => {
|
|
150
|
+
const tuple = Array.isArray(entry) ? entry[0] : null;
|
|
151
|
+
if (!Array.isArray(tuple) || typeof tuple[0] !== 'function') return entry;
|
|
152
|
+
let routed = tuples.get(tuple);
|
|
153
|
+
if (!routed) {
|
|
154
|
+
routed = [routeHandler(tuple[0], tuple[1]), tuple[1]];
|
|
155
|
+
tuples.set(tuple, routed);
|
|
156
|
+
}
|
|
157
|
+
return [routed, entry[1]];
|
|
158
|
+
});
|
|
159
|
+
lists.set(handlers, wrapped);
|
|
160
|
+
own.add(wrapped);
|
|
161
|
+
}
|
|
162
|
+
const copy = result.slice();
|
|
163
|
+
copy[0] = wrapped;
|
|
164
|
+
return copy;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function routeHandler(handler, route) {
|
|
168
|
+
const pattern = route && typeof route.path === 'string' ? route.path : null;
|
|
169
|
+
return function (c, next) {
|
|
170
|
+
const context = c && c.env ? RequestContext.of(c.env.incoming) : null;
|
|
171
|
+
if (!context) return handler.apply(this, arguments);
|
|
172
|
+
context.noteRoute({ route: pattern, params: () => c.req.param() });
|
|
173
|
+
const args = Array.prototype.slice.call(arguments);
|
|
174
|
+
if (typeof next === 'function') {
|
|
175
|
+
args[1] = function () {
|
|
176
|
+
context.noteRoute({ route: null, params: null });
|
|
177
|
+
return next.apply(this, arguments);
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const result = handler.apply(this, args);
|
|
181
|
+
if (result && typeof result.then === 'function') {
|
|
182
|
+
return result.then((value) => {
|
|
183
|
+
noteCaught(context, c);
|
|
184
|
+
return value;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
noteCaught(context, c);
|
|
188
|
+
return result;
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// A sub-app's own error handler answered an error: Hono leaves it on the context.
|
|
193
|
+
function noteCaught(context, c) {
|
|
194
|
+
try {
|
|
195
|
+
if (c.error && context.capture) context.capture.frameworkError(context, c.error, { mechanism: 'hono', handled: true, open: true });
|
|
196
|
+
} catch (failure) {
|
|
197
|
+
// Recording must never change the response.
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// An error the app's own handler answers stays open: the handler may still throw it on.
|
|
202
|
+
function noteError(incoming, error, handled) {
|
|
203
|
+
try {
|
|
204
|
+
const context = RequestContext.of(incoming) || RequestContext.current();
|
|
205
|
+
if (context && context.capture) context.capture.frameworkError(context, error, { mechanism: 'hono', handled, open: handled });
|
|
206
|
+
} catch (failure) {
|
|
207
|
+
// Recording must never change the error the app sees.
|
|
208
|
+
}
|
|
209
|
+
}
|