@futdevpro/nts-dynamo 1.15.155 → 1.15.157
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 +10 -10
- package/__documentations/2026-08-22-bfr-ctm-018-content-free-endpoint-errors.md +54 -0
- package/__documentations/2026-08-22-bfr-ctm-020-content-free-request-telemetry.md +65 -0
- package/__documentations/2026-08-22-content-free-process-error-boundary.md +21 -1
- package/build/_models/control-models/endpoint-params.control-model.d.ts +6 -0
- package/build/_models/control-models/endpoint-params.control-model.d.ts.map +1 -1
- package/build/_models/control-models/endpoint-params.control-model.js +71 -146
- package/build/_models/control-models/endpoint-params.control-model.js.map +1 -1
- package/build/_models/interfaces/request-telemetry-settings.interface.d.ts +10 -0
- package/build/_models/interfaces/request-telemetry-settings.interface.d.ts.map +1 -0
- package/build/_models/interfaces/request-telemetry-settings.interface.js +3 -0
- package/build/_models/interfaces/request-telemetry-settings.interface.js.map +1 -0
- package/build/_models/interfaces/request-telemetry-snapshot.interface.d.ts +29 -0
- package/build/_models/interfaces/request-telemetry-snapshot.interface.d.ts.map +1 -0
- package/build/_models/interfaces/request-telemetry-snapshot.interface.js +3 -0
- package/build/_models/interfaces/request-telemetry-snapshot.interface.js.map +1 -0
- package/build/_modules/server/safe-diagnostic/safe-diagnostic-stage.type-enum.d.ts +1 -0
- package/build/_modules/server/safe-diagnostic/safe-diagnostic-stage.type-enum.d.ts.map +1 -1
- package/build/_modules/server/safe-diagnostic/safe-diagnostic-stage.type-enum.js +1 -0
- package/build/_modules/server/safe-diagnostic/safe-diagnostic-stage.type-enum.js.map +1 -1
- package/build/_services/core/request-telemetry.control-service.d.ts +51 -0
- package/build/_services/core/request-telemetry.control-service.d.ts.map +1 -0
- package/build/_services/core/request-telemetry.control-service.js +286 -0
- package/build/_services/core/request-telemetry.control-service.js.map +1 -0
- package/build/_services/route/routing-module.service.d.ts.map +1 -1
- package/build/_services/route/routing-module.service.js +13 -10
- package/build/_services/route/routing-module.service.js.map +1 -1
- package/build/_services/server/app.server.d.ts +5 -0
- package/build/_services/server/app.server.d.ts.map +1 -1
- package/build/_services/server/app.server.js +13 -0
- package/build/_services/server/app.server.js.map +1 -1
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -0
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/_models/control-models/endpoint-params.control-model.spec.ts +87 -2
- package/src/_models/control-models/endpoint-params.control-model.ts +93 -182
- package/src/_models/interfaces/request-telemetry-settings.interface.ts +9 -0
- package/src/_models/interfaces/request-telemetry-snapshot.interface.ts +30 -0
- package/src/_modules/server/safe-diagnostic/safe-diagnostic-stage.type-enum.ts +1 -0
- package/src/_services/core/request-telemetry.control-service.spec.ts +217 -0
- package/src/_services/core/request-telemetry.control-service.ts +381 -0
- package/src/_services/route/routing-module.service.ts +27 -18
- package/src/_services/server/app.server.ts +29 -9
- package/src/_services/shared.static-service.spec.ts +57 -57
- package/src/_services/shared.static-service.ts +48 -48
- package/src/index.ts +5 -2
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { Request, Response } from 'express';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
DyFM_AnyError,
|
|
5
|
-
|
|
6
|
-
DyFM_Log, DyFM_Object
|
|
4
|
+
DyFM_AnyError, DyFM_Array, DyFM_Endpoint_SettingsBase, DyFM_Error, DyFM_getConstructionStackLocation,
|
|
5
|
+
DyFM_HttpCallType, DyFM_Log, DyFM_Object
|
|
7
6
|
} from '@futdevpro/fsm-dynamo';
|
|
8
7
|
|
|
9
8
|
import { DyNTS_global_settings } from '../../_collections/global-settings.const';
|
|
10
9
|
import { DyNTS_ClientSafeError_Util } from '../../_collections/client-safe-error.util';
|
|
11
10
|
import { DyNTS_RouteSecurity } from '../../_enums/route-security.enum';
|
|
11
|
+
import {
|
|
12
|
+
DyNTS_SafeDiagnostic_ControlService
|
|
13
|
+
} from '../../_modules/server/safe-diagnostic/safe-diagnostic.control-service';
|
|
14
|
+
import {
|
|
15
|
+
DyNTS_SafeDiagnosticStage_Type
|
|
16
|
+
} from '../../_modules/server/safe-diagnostic/safe-diagnostic-stage.type-enum';
|
|
17
|
+
import { DyNTS_SafeDiagnostic_Interface } from '../../_modules/server/safe-diagnostic/safe-diagnostic.interface';
|
|
12
18
|
import { DyNTS_GlobalService } from '../../_services/core/global.service';
|
|
13
19
|
import { DyNTS_RequestHunter } from '../../_services/core/request-hunter.service';
|
|
14
20
|
|
|
@@ -167,12 +173,11 @@ export class DyNTS_Endpoint_Params<
|
|
|
167
173
|
|
|
168
174
|
this.stackLocation = DyFM_getConstructionStackLocation();
|
|
169
175
|
} catch (error) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
);
|
|
176
|
+
DyNTS_SafeDiagnostic_ControlService.report({
|
|
177
|
+
code: 'DYNTS|ENDPOINT|CONSTRUCTION_FAILURE',
|
|
178
|
+
stage: DyNTS_SafeDiagnosticStage_Type.endpoint,
|
|
179
|
+
error: error,
|
|
180
|
+
});
|
|
176
181
|
|
|
177
182
|
throw error;
|
|
178
183
|
}
|
|
@@ -222,11 +227,14 @@ export class DyNTS_Endpoint_Params<
|
|
|
222
227
|
// jegyzes. Reszletes indoklas: `DyNTS_RequestHunter`.
|
|
223
228
|
const hunterId: number = DyNTS_RequestHunter.getInstance().begin(this.name);
|
|
224
229
|
try {
|
|
225
|
-
let issuer: string;
|
|
226
230
|
try {
|
|
227
231
|
issuer = DyNTS_GlobalService?.getAuthService()?.getIssuerFromRequest(req);
|
|
228
232
|
} catch (error) {
|
|
229
|
-
|
|
233
|
+
DyNTS_SafeDiagnostic_ControlService.report({
|
|
234
|
+
code: 'DYNTS|ENDPOINT|ISSUER_RESOLUTION_FAILURE',
|
|
235
|
+
stage: DyNTS_SafeDiagnosticStage_Type.endpoint,
|
|
236
|
+
error: error,
|
|
237
|
+
});
|
|
230
238
|
issuer = 'unknown-issuer';
|
|
231
239
|
}
|
|
232
240
|
|
|
@@ -294,7 +302,7 @@ export class DyNTS_Endpoint_Params<
|
|
|
294
302
|
}
|
|
295
303
|
}
|
|
296
304
|
} catch (error) {
|
|
297
|
-
this.error(req, res, error, issuer);
|
|
305
|
+
await this.error(req, res, error, issuer);
|
|
298
306
|
} finally {
|
|
299
307
|
DyNTS_RequestHunter.getInstance().end(hunterId);
|
|
300
308
|
}
|
|
@@ -336,100 +344,28 @@ export class DyNTS_Endpoint_Params<
|
|
|
336
344
|
autoSecondTry?: boolean
|
|
337
345
|
): Promise<void> {
|
|
338
346
|
try {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
msg += '\n at ' + this.stackLocation;
|
|
347
|
-
|
|
348
|
-
DyFM_Log.testError(msg);
|
|
349
|
-
|
|
350
|
-
if (
|
|
351
|
-
DyNTS_global_settings.log_settings.highDetailedLogs ||
|
|
352
|
-
!(error instanceof DyFM_Error)
|
|
353
|
-
) {
|
|
354
|
-
DyFM_Log.H_error(
|
|
355
|
-
`Endpoint "${this.endpoint}" caught an error. (${this.name})`,
|
|
356
|
-
'\n ERROR:', error
|
|
357
|
-
);
|
|
358
|
-
} else if (DyNTS_global_settings.log_settings.api_errors) {
|
|
359
|
-
error.logSimple(`Endpoint "${this.endpoint}" caught an error. (${this.name})`);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if ((error as DyFM_Error)?.flag?.includes('DYNAMO')) {
|
|
363
|
-
if (!(error as DyFM_Error).additionalContent) {
|
|
364
|
-
(error as DyFM_Error).additionalContent = {};
|
|
365
|
-
}
|
|
366
|
-
(error as DyFM_Error).additionalContent.endpointInfo = msg;
|
|
367
|
-
}
|
|
347
|
+
const status: number = DyNTS_Endpoint_Params.resolveErrorStatus(error);
|
|
348
|
+
const diagnostic: DyNTS_SafeDiagnostic_Interface = DyNTS_SafeDiagnostic_ControlService.report({
|
|
349
|
+
code: 'DYNTS|ENDPOINT|FAILURE',
|
|
350
|
+
stage: DyNTS_SafeDiagnosticStage_Type.endpoint,
|
|
351
|
+
error: error,
|
|
352
|
+
status: status,
|
|
353
|
+
});
|
|
368
354
|
|
|
369
|
-
// FR-071 (2026-06-06) — rich debug-descriptive request-context.
|
|
370
|
-
// Per memory [[feedback_rich_error_handling]] minden errornak debug-level
|
|
371
|
-
// descriptive-nek kell lennie + persisted to errors table. Az endpoint-
|
|
372
|
-
// params catch-blokkjaban minden errorhoz csatoljuk a request snapshot-ot
|
|
373
|
-
// (URL, method, headers KULCSAI ertek nelkul, query, params, origin,
|
|
374
|
-
// user-agent), igy a TEST environment-en a kliens-oldali fejleszto a
|
|
375
|
-
// response-bol latja MI hianyzott a request-bol, NEM csak az error-code-ot.
|
|
376
|
-
// Header-VALUEK nem szivarognak ki — csak a kulcs-listat es az auth-header
|
|
377
|
-
// alakjat (length + first 7 chars hogy lassek "Bearer "-rel kezd-e).
|
|
378
355
|
try {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const requestSnapshot: Record<string, unknown> = {
|
|
385
|
-
url: req?.originalUrl ?? req?.url,
|
|
386
|
-
method: req?.method,
|
|
387
|
-
origin: req?.headers?.origin ?? null,
|
|
388
|
-
referer: req?.headers?.referer ?? null,
|
|
389
|
-
userAgent: req?.headers?.['user-agent'] ?? null,
|
|
390
|
-
hostHeader: req?.headers?.host ?? null,
|
|
391
|
-
headerKeyCount: headerKeys.length,
|
|
392
|
-
headerKeys: headerKeys.sort(),
|
|
393
|
-
hasAuthorizationKey: headerKeys.includes('authorization'),
|
|
394
|
-
authHeaderShape: authHeaderShape,
|
|
395
|
-
hasSecretKeyKey: headerKeys.includes('secret-key'),
|
|
396
|
-
hasAdminKeyKey: headerKeys.includes('x-admin-key'),
|
|
397
|
-
hasContentTypeKey: headerKeys.includes('content-type'),
|
|
398
|
-
params: req?.params,
|
|
399
|
-
query: req?.query,
|
|
400
|
-
remoteAddress: req?.ip ?? null,
|
|
401
|
-
};
|
|
402
|
-
if (error instanceof DyFM_Error) {
|
|
403
|
-
if (!(error as DyFM_Error).additionalContent) {
|
|
404
|
-
(error as DyFM_Error).additionalContent = {};
|
|
405
|
-
}
|
|
406
|
-
(error as DyFM_Error).additionalContent.requestDebug = requestSnapshot;
|
|
407
|
-
}
|
|
408
|
-
// Plusz: 1-soros debug log a server-side log-buffer-ben.
|
|
409
|
-
DyFM_Log.warn(
|
|
410
|
-
`[FR-071 req-debug] ${req?.method ?? '?'} ${req?.originalUrl ?? req?.url ?? '?'} ` +
|
|
411
|
-
`(endpoint=${this.name}, origin=${req?.headers?.origin ?? 'none'}, ` +
|
|
412
|
-
`auth=${headerKeys.includes('authorization') ? 'yes' : 'NO'}, ` +
|
|
413
|
-
`secretKey=${headerKeys.includes('secret-key') ? 'yes' : 'no'}, ` +
|
|
414
|
-
`adminKey=${headerKeys.includes('x-admin-key') ? 'yes' : 'no'}, ` +
|
|
415
|
-
`errorCode=${(error as DyFM_Error)?._errorCode ?? '(raw)'})`
|
|
356
|
+
await DyNTS_GlobalService.globalErrorHandler?.(
|
|
357
|
+
error,
|
|
358
|
+
req,
|
|
359
|
+
res,
|
|
360
|
+
issuer
|
|
416
361
|
);
|
|
417
|
-
} catch
|
|
418
|
-
|
|
362
|
+
} catch (handlerError) {
|
|
363
|
+
DyNTS_SafeDiagnostic_ControlService.report({
|
|
364
|
+
code: 'DYNTS|ENDPOINT|GLOBAL_HANDLER_FAILURE',
|
|
365
|
+
stage: DyNTS_SafeDiagnosticStage_Type.endpoint,
|
|
366
|
+
error: handlerError,
|
|
367
|
+
});
|
|
419
368
|
}
|
|
420
|
-
|
|
421
|
-
await DyNTS_GlobalService.globalErrorHandler?.(
|
|
422
|
-
error,
|
|
423
|
-
req,
|
|
424
|
-
res,
|
|
425
|
-
issuer
|
|
426
|
-
).catch((err): void => {
|
|
427
|
-
DyFM_Log.warn(
|
|
428
|
-
'DyNTS_GlobalService.globalErrorHandler failed to handle error: ',
|
|
429
|
-
err
|
|
430
|
-
);
|
|
431
|
-
DyFM_Log.warn('It will proceed as normal.');
|
|
432
|
-
});
|
|
433
369
|
|
|
434
370
|
// FR-067 (2026-06-06): non-DyFM_Error wrap.
|
|
435
371
|
// Korabban a plain Error / axios HTTP error nyersen ment kifele
|
|
@@ -442,25 +378,12 @@ export class DyNTS_Endpoint_Params<
|
|
|
442
378
|
// betesszuk a DyFM_Error `___status` mezojebe, igy a kovetkezo blokk
|
|
443
379
|
// mar konzekvensen olvasni tudja.
|
|
444
380
|
if (!(error instanceof DyFM_Error)) {
|
|
445
|
-
const errAny: { statusCode?: unknown; status?: unknown; message?: string } =
|
|
446
|
-
error as { statusCode?: unknown; status?: unknown; message?: string };
|
|
447
|
-
const downstreamRaw: unknown = errAny?.statusCode ?? errAny?.status;
|
|
448
|
-
const downstreamStatus: number | undefined = typeof downstreamRaw === 'number'
|
|
449
|
-
? downstreamRaw
|
|
450
|
-
: (typeof downstreamRaw === 'string' && downstreamRaw !== '' && !isNaN(Number(downstreamRaw)))
|
|
451
|
-
? Number(downstreamRaw)
|
|
452
|
-
: undefined;
|
|
453
|
-
const wrapStatus: number = (
|
|
454
|
-
downstreamStatus !== undefined &&
|
|
455
|
-
downstreamStatus >= 100 &&
|
|
456
|
-
downstreamStatus <= 599
|
|
457
|
-
) ? downstreamStatus : 500;
|
|
458
381
|
error = new DyFM_Error({
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-EPP-${this.name ?? 'UNKNOWN'}`,
|
|
382
|
+
message: 'Unhandled endpoint failure.',
|
|
383
|
+
errorCode: this.getClientErrorCode(),
|
|
462
384
|
userMessage: 'An internal error occurred.\nPlease contact the responsible development team.',
|
|
463
|
-
|
|
385
|
+
additionalContent: { diagnostic: diagnostic },
|
|
386
|
+
status: status,
|
|
464
387
|
});
|
|
465
388
|
}
|
|
466
389
|
|
|
@@ -513,20 +436,7 @@ export class DyNTS_Endpoint_Params<
|
|
|
513
436
|
// `___status`-en kívül a `statusCode` (Express convention) es `status`
|
|
514
437
|
// (axios / general HTTP) mezőket is. Igy a downstream HTTP-hivasok
|
|
515
438
|
// (pl. axios-bol jovo error) helyes HTTP-status-szal továbbítódnak.
|
|
516
|
-
|
|
517
|
-
const resolvedStatusRaw: unknown = (error as DyFM_Error)?.___status
|
|
518
|
-
?? errorAny?.statusCode
|
|
519
|
-
?? errorAny?.status;
|
|
520
|
-
const resolvedStatus: number | null = typeof resolvedStatusRaw === 'number'
|
|
521
|
-
? resolvedStatusRaw
|
|
522
|
-
: (typeof resolvedStatusRaw === 'string' && !isNaN(Number(resolvedStatusRaw)))
|
|
523
|
-
? Number(resolvedStatusRaw)
|
|
524
|
-
: null;
|
|
525
|
-
if (resolvedStatus !== null && resolvedStatus >= 100 && resolvedStatus <= 599) {
|
|
526
|
-
res.status(resolvedStatus);
|
|
527
|
-
} else {
|
|
528
|
-
res.status(500);
|
|
529
|
-
}
|
|
439
|
+
res.status(DyNTS_Endpoint_Params.resolveErrorStatus(error));
|
|
530
440
|
|
|
531
441
|
// Kliens-safe strip ENV-FÜGGETLENÜL (BFR-FDPAUTHSERVICE-002): a korábbi prod-only blokk a
|
|
532
442
|
// test/dev környezetben TELJES DyFM-leaket hagyott (__localStack + ___systemVersion + issuer).
|
|
@@ -537,15 +447,8 @@ export class DyNTS_Endpoint_Params<
|
|
|
537
447
|
res.send(clientSafeError);
|
|
538
448
|
|
|
539
449
|
if (this.logRequest) {
|
|
540
|
-
DyFM_Log.error(
|
|
541
|
-
` <<<===== '${this.name}' error sent: ${(error as DyFM_Error)?._message ?? ''}`
|
|
542
|
-
);
|
|
543
|
-
if (this.logResponseContent) {
|
|
544
|
-
DyFM_Log.error(` error response: ${DyNTS_Endpoint_Params.previewBody(clientSafeError)}`);
|
|
545
|
-
}
|
|
450
|
+
DyFM_Log.error('[DyNTS endpoint error response sent]', diagnostic);
|
|
546
451
|
}
|
|
547
|
-
|
|
548
|
-
DyFM_Log.error('');
|
|
549
452
|
|
|
550
453
|
} catch (errorLvl2) {
|
|
551
454
|
this.multiLevelError(
|
|
@@ -571,33 +474,11 @@ export class DyNTS_Endpoint_Params<
|
|
|
571
474
|
autoSecondTry: boolean
|
|
572
475
|
}
|
|
573
476
|
): void {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
`and the error message was not sent.)` +
|
|
580
|
-
`\nthisEndpointStackLocation: ${this.stackLocation}`;
|
|
581
|
-
|
|
582
|
-
if (errorLvl2 instanceof DyFM_Error) {
|
|
583
|
-
errorLvl2.logSimple(msg);
|
|
584
|
-
} else {
|
|
585
|
-
DyFM_Log.H_error(msg, '\nERROR:', errorLvl2, '\n');
|
|
586
|
-
}
|
|
587
|
-
} else {
|
|
588
|
-
const msg =
|
|
589
|
-
`\n\nDYNAMO MULTILEVEL ERROR:DyNTS_Endpoint_Params: error: ` +
|
|
590
|
-
`(${this.name}, ${this.endpoint})` +
|
|
591
|
-
`\nthisEndpointStackLocation: ${this.stackLocation}` +
|
|
592
|
-
`\n(DYNAMO MULTILEVEL ERROR means, that the ERROR HANDLING is ALSO FAILED, ` +
|
|
593
|
-
`and the error message was not sent.)`;
|
|
594
|
-
|
|
595
|
-
if (errorLvl2 instanceof DyFM_Error) {
|
|
596
|
-
errorLvl2.logSimple(msg);
|
|
597
|
-
} else {
|
|
598
|
-
DyFM_Log.H_error(msg, '\nERROR:', errorLvl2, '\n');
|
|
599
|
-
}
|
|
600
|
-
}
|
|
477
|
+
DyNTS_SafeDiagnostic_ControlService.report({
|
|
478
|
+
code: 'DYNTS|ENDPOINT|ERROR_HANDLER_FAILURE',
|
|
479
|
+
stage: DyNTS_SafeDiagnosticStage_Type.endpoint,
|
|
480
|
+
error: errorLvl2,
|
|
481
|
+
});
|
|
601
482
|
|
|
602
483
|
try {
|
|
603
484
|
JSON.stringify(errorInputs.error);
|
|
@@ -608,8 +489,11 @@ export class DyNTS_Endpoint_Params<
|
|
|
608
489
|
try {
|
|
609
490
|
resolvedError = DyFM_Object.resolveCirculation(errorInputs.error);
|
|
610
491
|
} catch (errorLvl3) {
|
|
611
|
-
|
|
612
|
-
|
|
492
|
+
DyNTS_SafeDiagnostic_ControlService.report({
|
|
493
|
+
code: 'DYNTS|ENDPOINT|CIRCULATION_RESOLUTION_FAILURE',
|
|
494
|
+
stage: DyNTS_SafeDiagnosticStage_Type.endpoint,
|
|
495
|
+
error: errorLvl3,
|
|
496
|
+
});
|
|
613
497
|
}
|
|
614
498
|
|
|
615
499
|
if (resolvedError) {
|
|
@@ -633,19 +517,46 @@ export class DyNTS_Endpoint_Params<
|
|
|
633
517
|
}
|
|
634
518
|
}
|
|
635
519
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** Resolves conventional HTTP status fields without inspecting raw error content. */
|
|
523
|
+
private static resolveErrorStatus(error: unknown): number {
|
|
524
|
+
try {
|
|
525
|
+
if (error === null || (typeof error !== 'object' && typeof error !== 'function')) {
|
|
526
|
+
return 500;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const statusRaw: unknown = Reflect.get(error, '___status')
|
|
530
|
+
?? Reflect.get(error, 'statusCode')
|
|
531
|
+
?? Reflect.get(error, 'status');
|
|
532
|
+
const status: number = typeof statusRaw === 'number' ? statusRaw : Number(statusRaw);
|
|
533
|
+
|
|
534
|
+
return Number.isInteger(status) && status >= 100 && status <= 599 ? status : 500;
|
|
535
|
+
} catch {
|
|
536
|
+
return 500;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** Preserves the public error-code shape while excluding arbitrary endpoint-name characters. */
|
|
541
|
+
private getClientErrorCode(): string {
|
|
542
|
+
const systemCode: string = DyNTS_Endpoint_Params.sanitizeCodePart(
|
|
543
|
+
DyNTS_global_settings.systemShortCodeName,
|
|
544
|
+
'DYNTS'
|
|
545
|
+
);
|
|
546
|
+
const endpointCode: string = DyNTS_Endpoint_Params.sanitizeCodePart(this.name, 'UNKNOWN');
|
|
547
|
+
|
|
548
|
+
return `${systemCode}|DyNTS-EPP-${endpointCode}`;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** Bounds developer-owned identifiers to the established error-code alphabet. */
|
|
552
|
+
private static sanitizeCodePart(value: unknown, fallback: string): string {
|
|
553
|
+
if (typeof value !== 'string') {
|
|
554
|
+
return fallback;
|
|
648
555
|
}
|
|
556
|
+
|
|
557
|
+
const sanitized: string = value.replace(/[^A-Za-z0-9_-]/g, '_').slice(0, 64);
|
|
558
|
+
|
|
559
|
+
return sanitized || fallback;
|
|
649
560
|
}
|
|
650
561
|
|
|
651
562
|
private getPathParamsLogContent(req: Request): string {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Opt-in, content-free request telemetry settings for one Dynamo application. */
|
|
2
|
+
export interface DyNTS_RequestTelemetry_Settings {
|
|
3
|
+
/** Enables correlation headers and event-driven aggregate metrics. Default: false. */
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
/** Maximum distinct declared route templates retained before overflow aggregation. Default: 128. */
|
|
6
|
+
maxRouteCardinality?: number;
|
|
7
|
+
/** Inclusive duration bucket upper bounds in milliseconds. Default: 25, 100, 250, 1000, 5000. */
|
|
8
|
+
durationBucketUpperBoundsMs?: number[];
|
|
9
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** One bounded duration bucket in the content-free request telemetry snapshot. */
|
|
2
|
+
export interface DyNTS_RequestDurationBucket_Interface {
|
|
3
|
+
upperBoundMs: number | null;
|
|
4
|
+
count: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** One declared route-template aggregate. No concrete URL or request value is retained. */
|
|
8
|
+
export interface DyNTS_RequestRouteMetric_Interface {
|
|
9
|
+
method: string;
|
|
10
|
+
routeTemplate: string;
|
|
11
|
+
count: number;
|
|
12
|
+
abortedCount: number;
|
|
13
|
+
serverErrorCount: number;
|
|
14
|
+
totalDurationMs: number;
|
|
15
|
+
maxDurationMs: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Synchronous, immutable-by-copy snapshot of the event-driven request counters. */
|
|
19
|
+
export interface DyNTS_RequestTelemetrySnapshot_Interface {
|
|
20
|
+
contractVersion: 'dynts-request-telemetry/1';
|
|
21
|
+
correlationHeader: 'X-Correlation-Id';
|
|
22
|
+
requestsStarted: number;
|
|
23
|
+
requestsCompleted: number;
|
|
24
|
+
requestsAborted: number;
|
|
25
|
+
inFlight: number;
|
|
26
|
+
droppedRouteTemplates: number;
|
|
27
|
+
statusClasses: Record<string, number>;
|
|
28
|
+
durationBuckets: DyNTS_RequestDurationBucket_Interface[];
|
|
29
|
+
routes: DyNTS_RequestRouteMetric_Interface[];
|
|
30
|
+
}
|
|
@@ -5,6 +5,7 @@ export enum DyNTS_SafeDiagnosticStage_Type {
|
|
|
5
5
|
databaseConnect = 'database-connect',
|
|
6
6
|
databaseRuntime = 'database-runtime',
|
|
7
7
|
eventLoopDiagnostic = 'event-loop-diagnostic',
|
|
8
|
+
endpoint = 'endpoint',
|
|
8
9
|
expressFallback = 'express-fallback',
|
|
9
10
|
globalErrorHandler = 'global-error-handler',
|
|
10
11
|
httpServer = 'http-server',
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { NextFunction, Request, Response } from 'express';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
DyNTS_RequestRouteMetric_Interface,
|
|
6
|
+
DyNTS_RequestTelemetrySnapshot_Interface
|
|
7
|
+
} from '../../_models/interfaces/request-telemetry-snapshot.interface';
|
|
8
|
+
import { DyNTS_RequestTelemetry_ControlService } from './request-telemetry.control-service';
|
|
9
|
+
|
|
10
|
+
class ResponseStub extends EventEmitter {
|
|
11
|
+
public readonly headers: Record<string, string> = {};
|
|
12
|
+
private responseStatusCode: number = 200;
|
|
13
|
+
|
|
14
|
+
/** Returns the response status used by completion aggregation. */
|
|
15
|
+
public get statusCode(): number {
|
|
16
|
+
return this.responseStatusCode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Updates the response status used by completion aggregation. */
|
|
20
|
+
public set statusCode(value: number) {
|
|
21
|
+
this.responseStatusCode = value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Stores one response header for middleware assertions. */
|
|
25
|
+
public setHeader(name: string, value: string): void {
|
|
26
|
+
this.headers[name] = value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Reads one stored response header for middleware composition. */
|
|
30
|
+
public getHeader(name: string): string | undefined {
|
|
31
|
+
return this.headers[name];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class ThrowingHeaderResponseStub extends ResponseStub {
|
|
36
|
+
/** Simulates a response implementation rejecting header mutation. */
|
|
37
|
+
public override setHeader(_name: string, _value: string): void {
|
|
38
|
+
void _name;
|
|
39
|
+
void _value;
|
|
40
|
+
|
|
41
|
+
throw new Error('simulated header failure');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class ThrowingStatusResponseStub extends ResponseStub {
|
|
46
|
+
/** Simulates a late response implementation failure after listener installation. */
|
|
47
|
+
public override get statusCode(): number { // eslint-disable-line @futdevpro/dynamo/no-getter-logic
|
|
48
|
+
throw new Error('simulated status failure');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Accepts setup writes while the read side remains deliberately unavailable. */
|
|
52
|
+
public override set statusCode(_value: number) {}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('| BFR-CTM-020 content-free request telemetry', (): void => {
|
|
56
|
+
const service: DyNTS_RequestTelemetry_ControlService = DyNTS_RequestTelemetry_ControlService.getInstance();
|
|
57
|
+
|
|
58
|
+
beforeEach((): void => service._resetForTesting());
|
|
59
|
+
|
|
60
|
+
it('| replaces caller correlation and counts finish exactly once', (): void => {
|
|
61
|
+
service.configure({ enabled: true, durationBucketUpperBoundsMs: [ 50 ], maxRouteCardinality: 4 });
|
|
62
|
+
const req: Request = createRequest('GET');
|
|
63
|
+
const res: ResponseStub = new ResponseStub();
|
|
64
|
+
const next: NextFunction = createNext();
|
|
65
|
+
|
|
66
|
+
service.createMiddleware()(req, response(res), next);
|
|
67
|
+
service.identifyRoute('/pipeline/:pipelineId')(req, response(res), next);
|
|
68
|
+
res.statusCode = 503;
|
|
69
|
+
res.emit('finish');
|
|
70
|
+
res.emit('close');
|
|
71
|
+
|
|
72
|
+
const snapshot: DyNTS_RequestTelemetrySnapshot_Interface = service.snapshot();
|
|
73
|
+
|
|
74
|
+
expect(res.headers['X-Correlation-Id']).toMatch(/^[0-9a-f-]{36}$/u);
|
|
75
|
+
expect(res.headers['X-Correlation-Id']).not.toBe('caller-value');
|
|
76
|
+
expect(res.headers['Access-Control-Expose-Headers']).toContain('X-Correlation-Id');
|
|
77
|
+
expect(snapshot.requestsStarted).toBe(1);
|
|
78
|
+
expect(snapshot.requestsCompleted).toBe(1);
|
|
79
|
+
expect(snapshot.requestsAborted).toBe(0);
|
|
80
|
+
expect(snapshot.inFlight).toBe(0);
|
|
81
|
+
expect(snapshot.statusClasses['5xx']).toBe(1);
|
|
82
|
+
expect(snapshot.routes).toEqual([ jasmine.objectContaining({
|
|
83
|
+
method: 'GET',
|
|
84
|
+
routeTemplate: '/pipeline/:pipelineId',
|
|
85
|
+
count: 1,
|
|
86
|
+
abortedCount: 0,
|
|
87
|
+
serverErrorCount: 1,
|
|
88
|
+
}) ]);
|
|
89
|
+
expect(service.getCorrelationId(req)).toBeUndefined();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('| records close without finish as an abort and removes both listeners', (): void => {
|
|
93
|
+
service.configure({ enabled: true });
|
|
94
|
+
const req: Request = createRequest('POST');
|
|
95
|
+
const res: ResponseStub = new ResponseStub();
|
|
96
|
+
|
|
97
|
+
service.createMiddleware()(req, response(res), createNext());
|
|
98
|
+
expect(res.listenerCount('finish')).toBe(1);
|
|
99
|
+
expect(res.listenerCount('close')).toBe(1);
|
|
100
|
+
res.emit('close');
|
|
101
|
+
|
|
102
|
+
const snapshot: DyNTS_RequestTelemetrySnapshot_Interface = service.snapshot();
|
|
103
|
+
|
|
104
|
+
expect(snapshot.requestsAborted).toBe(1);
|
|
105
|
+
expect(snapshot.requestsCompleted).toBe(0);
|
|
106
|
+
expect(snapshot.inFlight).toBe(0);
|
|
107
|
+
expect(res.listenerCount('finish')).toBe(0);
|
|
108
|
+
expect(res.listenerCount('close')).toBe(0);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('| caps declared route cardinality and never retains invalid route content', (): void => {
|
|
112
|
+
service.configure({ enabled: true, maxRouteCardinality: 1 });
|
|
113
|
+
completeRequest(service, '/first/:id');
|
|
114
|
+
completeRequest(service, '/second/:id');
|
|
115
|
+
completeRequest(service, '/unsafe?secret=:value');
|
|
116
|
+
|
|
117
|
+
const snapshot: DyNTS_RequestTelemetrySnapshot_Interface = service.snapshot();
|
|
118
|
+
|
|
119
|
+
expect(snapshot.droppedRouteTemplates).toBe(2);
|
|
120
|
+
expect(snapshot.routes.map(
|
|
121
|
+
(route: DyNTS_RequestRouteMetric_Interface): string => route.routeTemplate
|
|
122
|
+
)).toEqual([ '__other__', '/first/:id' ]);
|
|
123
|
+
expect(JSON.stringify(snapshot)).not.toContain('secret');
|
|
124
|
+
expect(snapshot.routes.length).toBe(2);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('| caps distinct templates without collapsing allowed methods on one retained template', (): void => {
|
|
128
|
+
service.configure({ enabled: true, maxRouteCardinality: 1 });
|
|
129
|
+
completeRequest(service, '/pipeline/:id', 'GET');
|
|
130
|
+
completeRequest(service, '/pipeline/:id', 'POST');
|
|
131
|
+
completeRequest(service, '/another/:id', 'GET');
|
|
132
|
+
|
|
133
|
+
const snapshot: DyNTS_RequestTelemetrySnapshot_Interface = service.snapshot();
|
|
134
|
+
|
|
135
|
+
expect(snapshot.droppedRouteTemplates).toBe(1);
|
|
136
|
+
expect(snapshot.routes.map(
|
|
137
|
+
(route: DyNTS_RequestRouteMetric_Interface): string => `${route.method}:${route.routeTemplate}`
|
|
138
|
+
)).toEqual([ 'GET:__other__', 'GET:/pipeline/:id', 'POST:/pipeline/:id' ]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('| is a no-op while disabled', (): void => {
|
|
142
|
+
service.configure({ enabled: false });
|
|
143
|
+
const req: Request = createRequest('GET');
|
|
144
|
+
const res: ResponseStub = new ResponseStub();
|
|
145
|
+
|
|
146
|
+
service.createMiddleware()(req, response(res), createNext());
|
|
147
|
+
|
|
148
|
+
expect(res.headers['X-Correlation-Id']).toBeUndefined();
|
|
149
|
+
expect(res.listenerCount('finish')).toBe(0);
|
|
150
|
+
expect(service.snapshot().requestsStarted).toBe(0);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('| cannot leak header setup failures or create phantom in-flight requests', (): void => {
|
|
154
|
+
service.configure({ enabled: true });
|
|
155
|
+
const req: Request = createRequest('GET');
|
|
156
|
+
const res: ThrowingHeaderResponseStub = new ThrowingHeaderResponseStub();
|
|
157
|
+
|
|
158
|
+
expect((): void => service.createMiddleware()(req, response(res), createNext())).not.toThrow();
|
|
159
|
+
expect(service.snapshot().requestsStarted).toBe(0);
|
|
160
|
+
expect(service.snapshot().inFlight).toBe(0);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('| contains late telemetry failures without changing response lifecycle semantics', (): void => {
|
|
164
|
+
service.configure({ enabled: true });
|
|
165
|
+
const req: Request = createRequest('GET');
|
|
166
|
+
const res: ThrowingStatusResponseStub = new ThrowingStatusResponseStub();
|
|
167
|
+
|
|
168
|
+
service.createMiddleware()(req, response(res), createNext());
|
|
169
|
+
|
|
170
|
+
expect((): boolean => res.emit('finish')).not.toThrow();
|
|
171
|
+
expect(service.snapshot().inFlight).toBe(0);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('| rejects conflicting or unbounded configuration with stable codes', (): void => {
|
|
175
|
+
expect((): void => service.configure({ enabled: true, maxRouteCardinality: 0 }))
|
|
176
|
+
.toThrowError('DYNTS|REQUEST_TELEMETRY|MAX_ROUTE_CARDINALITY_INVALID');
|
|
177
|
+
expect((): void => service.configure({ enabled: true, durationBucketUpperBoundsMs: [ 100, 50 ] }))
|
|
178
|
+
.toThrowError('DYNTS|REQUEST_TELEMETRY|DURATION_BUCKETS_INVALID');
|
|
179
|
+
|
|
180
|
+
service.configure({ enabled: true });
|
|
181
|
+
expect((): void => service.configure({ enabled: true, maxRouteCardinality: 5 }))
|
|
182
|
+
.toThrowError('DYNTS|REQUEST_TELEMETRY|CONFIGURATION_CONFLICT');
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
function completeRequest(
|
|
187
|
+
service: DyNTS_RequestTelemetry_ControlService,
|
|
188
|
+
routeTemplate: string,
|
|
189
|
+
method: string = 'GET'
|
|
190
|
+
): void {
|
|
191
|
+
const req: Request = createRequest(method);
|
|
192
|
+
const res: ResponseStub = new ResponseStub();
|
|
193
|
+
const next: NextFunction = createNext();
|
|
194
|
+
|
|
195
|
+
service.createMiddleware()(req, response(res), next);
|
|
196
|
+
service.identifyRoute(routeTemplate)(req, response(res), next);
|
|
197
|
+
res.emit('finish');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function createRequest(method: string): Request {
|
|
201
|
+
// Express's runtime object is intentionally narrowed to fields the middleware is allowed to read.
|
|
202
|
+
// eslint-disable-next-line @futdevpro/dynamo/no-as-cast
|
|
203
|
+
return { method: method, headers: { 'x-correlation-id': 'caller-value' } } as unknown as Request;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function response(res: ResponseStub): Response {
|
|
207
|
+
// The stub implements the exact response lifecycle/header surface used by this middleware.
|
|
208
|
+
// eslint-disable-next-line @futdevpro/dynamo/no-as-cast
|
|
209
|
+
return res as unknown as Response;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function createNext(): NextFunction {
|
|
213
|
+
/** Deliberately empty Express continuation used by the isolated middleware tests. */
|
|
214
|
+
const next: NextFunction = (): void => undefined;
|
|
215
|
+
|
|
216
|
+
return next;
|
|
217
|
+
}
|