@futurewindai/wotchi 0.1.0-beta.1

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.
Files changed (89) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +128 -0
  3. package/SECURITY.md +17 -0
  4. package/dist/cjs/core/client.js +154 -0
  5. package/dist/cjs/core/config.js +114 -0
  6. package/dist/cjs/core/diagnostics.js +14 -0
  7. package/dist/cjs/core/errors.js +11 -0
  8. package/dist/cjs/core/fingerprint.js +37 -0
  9. package/dist/cjs/core/group-store.js +95 -0
  10. package/dist/cjs/core/incident-builder.js +46 -0
  11. package/dist/cjs/core/incident-policy.js +31 -0
  12. package/dist/cjs/core/normalize.js +138 -0
  13. package/dist/cjs/core/notification-queue.js +103 -0
  14. package/dist/cjs/core/process-monitor.js +30 -0
  15. package/dist/cjs/core/redact.js +141 -0
  16. package/dist/cjs/core/rolling-window.js +60 -0
  17. package/dist/cjs/core/stack-frame.js +33 -0
  18. package/dist/cjs/core/types.js +2 -0
  19. package/dist/cjs/index.js +13 -0
  20. package/dist/cjs/integrations/express/error-handler.js +15 -0
  21. package/dist/cjs/integrations/express/index.js +12 -0
  22. package/dist/cjs/integrations/express/request-context.js +19 -0
  23. package/dist/cjs/integrations/nest/exception-filter.js +36 -0
  24. package/dist/cjs/integrations/nest/index.js +9 -0
  25. package/dist/cjs/integrations/nest/register.js +13 -0
  26. package/dist/cjs/integrations/nest/request-context.js +41 -0
  27. package/dist/cjs/integrations/request-context.js +76 -0
  28. package/dist/cjs/notifiers/console.js +33 -0
  29. package/dist/cjs/notifiers/telegram-format.js +52 -0
  30. package/dist/cjs/notifiers/telegram-http.js +150 -0
  31. package/dist/cjs/notifiers/telegram.js +38 -0
  32. package/dist/cjs/package.json +1 -0
  33. package/dist/esm/core/client.js +151 -0
  34. package/dist/esm/core/config.js +110 -0
  35. package/dist/esm/core/diagnostics.js +10 -0
  36. package/dist/esm/core/errors.js +7 -0
  37. package/dist/esm/core/fingerprint.js +33 -0
  38. package/dist/esm/core/group-store.js +92 -0
  39. package/dist/esm/core/incident-builder.js +43 -0
  40. package/dist/esm/core/incident-policy.js +28 -0
  41. package/dist/esm/core/normalize.js +134 -0
  42. package/dist/esm/core/notification-queue.js +100 -0
  43. package/dist/esm/core/process-monitor.js +27 -0
  44. package/dist/esm/core/redact.js +136 -0
  45. package/dist/esm/core/rolling-window.js +57 -0
  46. package/dist/esm/core/stack-frame.js +29 -0
  47. package/dist/esm/core/types.js +1 -0
  48. package/dist/esm/index.js +6 -0
  49. package/dist/esm/integrations/express/error-handler.js +12 -0
  50. package/dist/esm/integrations/express/index.js +5 -0
  51. package/dist/esm/integrations/express/request-context.js +16 -0
  52. package/dist/esm/integrations/nest/exception-filter.js +33 -0
  53. package/dist/esm/integrations/nest/index.js +2 -0
  54. package/dist/esm/integrations/nest/register.js +10 -0
  55. package/dist/esm/integrations/nest/request-context.js +38 -0
  56. package/dist/esm/integrations/request-context.js +72 -0
  57. package/dist/esm/notifiers/console.js +29 -0
  58. package/dist/esm/notifiers/telegram-format.js +49 -0
  59. package/dist/esm/notifiers/telegram-http.js +144 -0
  60. package/dist/esm/notifiers/telegram.js +34 -0
  61. package/dist/types/core/client.d.ts +2 -0
  62. package/dist/types/core/config.d.ts +29 -0
  63. package/dist/types/core/diagnostics.d.ts +7 -0
  64. package/dist/types/core/errors.d.ts +3 -0
  65. package/dist/types/core/fingerprint.d.ts +3 -0
  66. package/dist/types/core/group-store.d.ts +15 -0
  67. package/dist/types/core/incident-builder.d.ts +3 -0
  68. package/dist/types/core/incident-policy.d.ts +15 -0
  69. package/dist/types/core/normalize.d.ts +17 -0
  70. package/dist/types/core/notification-queue.d.ts +16 -0
  71. package/dist/types/core/process-monitor.d.ts +5 -0
  72. package/dist/types/core/redact.d.ts +11 -0
  73. package/dist/types/core/rolling-window.d.ts +12 -0
  74. package/dist/types/core/stack-frame.d.ts +2 -0
  75. package/dist/types/core/types.d.ts +109 -0
  76. package/dist/types/index.d.ts +9 -0
  77. package/dist/types/integrations/express/error-handler.d.ts +4 -0
  78. package/dist/types/integrations/express/index.d.ts +7 -0
  79. package/dist/types/integrations/express/request-context.d.ts +5 -0
  80. package/dist/types/integrations/nest/exception-filter.d.ts +10 -0
  81. package/dist/types/integrations/nest/index.d.ts +4 -0
  82. package/dist/types/integrations/nest/register.d.ts +10 -0
  83. package/dist/types/integrations/nest/request-context.d.ts +5 -0
  84. package/dist/types/integrations/request-context.d.ts +13 -0
  85. package/dist/types/notifiers/console.d.ts +3 -0
  86. package/dist/types/notifiers/telegram-format.d.ts +2 -0
  87. package/dist/types/notifiers/telegram-http.d.ts +21 -0
  88. package/dist/types/notifiers/telegram.d.ts +4 -0
  89. package/package.json +111 -0
@@ -0,0 +1,100 @@
1
+ const DEFAULT_FLUSH_TIMEOUT_MS = 5_000;
2
+ export function createNotificationQueue(options) {
3
+ if (!Number.isSafeInteger(options.maxPendingAlerts) || options.maxPendingAlerts <= 0) {
4
+ throw new RangeError("maxPendingAlerts must be a positive integer");
5
+ }
6
+ if (options.concurrency !== 1) {
7
+ throw new RangeError("concurrency must be 1");
8
+ }
9
+ const pendingJobs = [];
10
+ let drainPromise;
11
+ let resolveDrain;
12
+ let activeJobs = 0;
13
+ let queued = 0;
14
+ let dropped = 0;
15
+ let sent = 0;
16
+ let failures = 0;
17
+ const notifyFailure = (error, notifier) => {
18
+ failures += 1;
19
+ try {
20
+ options.onNotifierError?.(error, notifier);
21
+ }
22
+ catch {
23
+ // Diagnostics callbacks cannot be allowed to affect the host application.
24
+ }
25
+ };
26
+ const resolveDrainWaiters = () => {
27
+ if (activeJobs !== 0 || pendingJobs.length !== 0) {
28
+ return;
29
+ }
30
+ resolveDrain?.();
31
+ drainPromise = undefined;
32
+ resolveDrain = undefined;
33
+ };
34
+ const processJob = async (job) => {
35
+ for (const notifier of job.notifiers) {
36
+ try {
37
+ await notifier.send(job.alert);
38
+ sent += 1;
39
+ }
40
+ catch (error) {
41
+ notifyFailure(error, notifier);
42
+ }
43
+ }
44
+ };
45
+ const pump = () => {
46
+ while (activeJobs < options.concurrency && pendingJobs.length > 0) {
47
+ const job = pendingJobs.shift();
48
+ if (job === undefined) {
49
+ break;
50
+ }
51
+ activeJobs += 1;
52
+ void processJob(job).finally(() => {
53
+ activeJobs -= 1;
54
+ pump();
55
+ resolveDrainWaiters();
56
+ });
57
+ }
58
+ };
59
+ const enqueue = (alert, notifiers) => {
60
+ if (pendingJobs.length >= options.maxPendingAlerts) {
61
+ dropped += 1;
62
+ return false;
63
+ }
64
+ pendingJobs.push({ alert, notifiers: [...notifiers] });
65
+ queued += 1;
66
+ pump();
67
+ return true;
68
+ };
69
+ const flush = (timeoutMs = DEFAULT_FLUSH_TIMEOUT_MS) => {
70
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
71
+ return Promise.reject(new RangeError("timeoutMs must be a positive integer"));
72
+ }
73
+ if (activeJobs === 0 && pendingJobs.length === 0) {
74
+ return Promise.resolve();
75
+ }
76
+ const drain = drainPromise ??
77
+ new Promise((resolve) => {
78
+ resolveDrain = resolve;
79
+ });
80
+ drainPromise = drain;
81
+ return new Promise((resolve, reject) => {
82
+ const timer = setTimeout(() => {
83
+ reject(new Error(`Wotchi notification queue flush timed out after ${timeoutMs}ms`));
84
+ }, timeoutMs);
85
+ void drain.then(() => {
86
+ clearTimeout(timer);
87
+ resolve();
88
+ });
89
+ });
90
+ };
91
+ return {
92
+ enqueue,
93
+ flush,
94
+ pending: () => pendingJobs.length,
95
+ alertsQueued: () => queued,
96
+ alertsDropped: () => dropped,
97
+ alertsSent: () => sent,
98
+ notifierFailures: () => failures,
99
+ };
100
+ }
@@ -0,0 +1,27 @@
1
+ export function registerWotchiProcessMonitor(client) {
2
+ const listener = (error, origin) => {
3
+ try {
4
+ client.captureEvent({
5
+ level: "error",
6
+ kind: "process-monitor",
7
+ message: "Uncaught process exception",
8
+ error,
9
+ context: { processOrigin: origin },
10
+ });
11
+ }
12
+ catch {
13
+ // Process monitoring must never alter the host process crash behavior.
14
+ }
15
+ };
16
+ globalThis.process.on("uncaughtExceptionMonitor", listener);
17
+ let registered = true;
18
+ return {
19
+ unregister() {
20
+ if (!registered) {
21
+ return;
22
+ }
23
+ registered = false;
24
+ globalThis.process.off("uncaughtExceptionMonitor", listener);
25
+ },
26
+ };
27
+ }
@@ -0,0 +1,136 @@
1
+ import { normalizeUnknown } from "./normalize.js";
2
+ export const REDACTED = "[REDACTED]";
3
+ const DEFAULT_KEYS = [
4
+ "authorization",
5
+ "cookie",
6
+ "set-cookie",
7
+ "password",
8
+ "passwd",
9
+ "secret",
10
+ "apiKey",
11
+ "api_key",
12
+ "token",
13
+ "accessToken",
14
+ "access_token",
15
+ "refreshToken",
16
+ "refresh_token",
17
+ "privateKey",
18
+ "private_key",
19
+ "clientSecret",
20
+ "client_secret",
21
+ "cardNumber",
22
+ "cvv",
23
+ "cvc",
24
+ "pin",
25
+ ];
26
+ const normalizeKey = (key) => key.toLowerCase().replace(/[-_]/g, "");
27
+ const defaultKeySet = new Set(DEFAULT_KEYS.map(normalizeKey));
28
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
29
+ const luhnValid = (digits) => {
30
+ let sum = 0;
31
+ let double = false;
32
+ for (let index = digits.length - 1; index >= 0; index -= 1) {
33
+ let digit = Number(digits[index]);
34
+ if (double) {
35
+ digit *= 2;
36
+ if (digit > 9) {
37
+ digit -= 9;
38
+ }
39
+ }
40
+ sum += digit;
41
+ double = !double;
42
+ }
43
+ return sum % 10 === 0;
44
+ };
45
+ const redactCardLikeValues = (value) => value.replace(/\b\d[\d -]{11,25}\d\b/g, (candidate) => {
46
+ const digits = candidate.replace(/[ -]/g, "");
47
+ return digits.length >= 13 && digits.length <= 19 && luhnValid(digits) ? REDACTED : candidate;
48
+ });
49
+ const redactString = (value, maxStringLength) => {
50
+ let result = value;
51
+ result = result.replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi, REDACTED);
52
+ result = result.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, `Bearer ${REDACTED}`);
53
+ result = result.replace(/\b(?:authorization|password|passwd|secret|token|api[-_]?key)\s*[:=]\s*[^\s,;]+/gi, (match) => `${match.slice(0, match.search(/[:=]/))}: ${REDACTED}`);
54
+ result = result.replace(/\b[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g, REDACTED);
55
+ result = result.replace(/\b(?:github_pat|ghp|xox[baprs]-|sk|pk|AKIA)[A-Za-z0-9_-]{8,}\b/gi, REDACTED);
56
+ result = redactCardLikeValues(result);
57
+ return result.slice(0, maxStringLength);
58
+ };
59
+ const normalizedOption = (value, fallback) => Number.isSafeInteger(value) && value !== undefined && value > 0 ? value : fallback;
60
+ const redactNormalized = (value, sensitiveKeys, options, depth, keysVisited) => {
61
+ if (typeof value === "string") {
62
+ return redactString(value, options.maxStringLength);
63
+ }
64
+ if (value === null || typeof value !== "object") {
65
+ return value;
66
+ }
67
+ if (depth >= options.maxDepth) {
68
+ return "[MaxDepth]";
69
+ }
70
+ if (Array.isArray(value)) {
71
+ const result = [];
72
+ for (const item of value) {
73
+ if (keysVisited.value >= options.maxKeys) {
74
+ break;
75
+ }
76
+ keysVisited.value += 1;
77
+ result.push(redactNormalized(item, sensitiveKeys, options, depth + 1, keysVisited));
78
+ }
79
+ return result;
80
+ }
81
+ const result = {};
82
+ for (const [key, child] of Object.entries(value)) {
83
+ if (keysVisited.value >= options.maxKeys) {
84
+ break;
85
+ }
86
+ keysVisited.value += 1;
87
+ const normalizedKey = normalizeKey(key);
88
+ result[key] = sensitiveKeys.has(normalizedKey)
89
+ ? REDACTED
90
+ : typeof child === "string" && normalizedKey === "stack"
91
+ ? redactString(child, options.maxStackLength)
92
+ : redactNormalized(child, sensitiveKeys, options, depth + 1, keysVisited);
93
+ }
94
+ return result;
95
+ };
96
+ const redactionLimits = (options) => ({
97
+ maxDepth: normalizedOption(options.maxDepth, 5),
98
+ maxKeys: normalizedOption(options.maxKeys, 100),
99
+ maxStringLength: normalizedOption(options.maxStringLength, 500),
100
+ maxStackLength: normalizedOption(options.maxStackLength, 4_000),
101
+ });
102
+ const sensitiveKeySet = (options) => {
103
+ const keys = new Set(defaultKeySet);
104
+ for (const key of options.redactKeys ?? []) {
105
+ if (typeof key === "string" && key.trim().length > 0) {
106
+ keys.add(normalizeKey(key.trim()));
107
+ }
108
+ }
109
+ return keys;
110
+ };
111
+ export function redactValue(value, options = {}) {
112
+ const limits = redactionLimits(options);
113
+ const normalized = normalizeUnknown(value, {
114
+ maxDepth: limits.maxDepth,
115
+ maxKeys: limits.maxKeys,
116
+ maxStringLength: limits.maxStringLength,
117
+ maxStackLength: limits.maxStackLength,
118
+ });
119
+ return redactNormalized(normalized, sensitiveKeySet(options), limits, 0, { value: 0 });
120
+ }
121
+ export function redactError(error, options = {}) {
122
+ const redacted = redactValue(error, options);
123
+ if (!isRecord(redacted)) {
124
+ return { name: "UnknownError", message: REDACTED };
125
+ }
126
+ const name = typeof redacted.name === "string" ? redacted.name : "UnknownError";
127
+ const message = typeof redacted.message === "string" ? redacted.message : REDACTED;
128
+ const stack = typeof redacted.stack === "string" ? redacted.stack : undefined;
129
+ const code = typeof redacted.code === "string" ? redacted.code : undefined;
130
+ return {
131
+ name,
132
+ message,
133
+ ...(stack === undefined ? {} : { stack }),
134
+ ...(code === undefined ? {} : { code }),
135
+ };
136
+ }
@@ -0,0 +1,57 @@
1
+ export function createRollingWindow(options) {
2
+ if (!Number.isSafeInteger(options.maxEvents) || options.maxEvents <= 0) {
3
+ throw new RangeError("maxEvents must be a positive integer");
4
+ }
5
+ if (!Number.isSafeInteger(options.windowMs) || options.windowMs <= 0) {
6
+ throw new RangeError("windowMs must be a positive integer");
7
+ }
8
+ const now = options.now ?? Date.now;
9
+ const buffer = new Array(options.maxEvents);
10
+ let head = 0;
11
+ let length = 0;
12
+ const readNow = (timestamp) => {
13
+ const value = timestamp ?? now();
14
+ return Number.isFinite(value) ? value : now();
15
+ };
16
+ const purge = (timestamp) => {
17
+ while (length > 0) {
18
+ const oldest = buffer[head];
19
+ if (oldest === undefined || timestamp - oldest < options.windowMs) {
20
+ break;
21
+ }
22
+ buffer[head] = undefined;
23
+ head = (head + 1) % options.maxEvents;
24
+ length -= 1;
25
+ }
26
+ };
27
+ const add = (timestamp) => {
28
+ const value = readNow(timestamp);
29
+ purge(value);
30
+ const index = (head + length) % options.maxEvents;
31
+ if (length === options.maxEvents) {
32
+ buffer[head] = undefined;
33
+ head = (head + 1) % options.maxEvents;
34
+ length -= 1;
35
+ }
36
+ buffer[index] = value;
37
+ length += 1;
38
+ };
39
+ const timestamps = (timestamp) => {
40
+ const value = readNow(timestamp);
41
+ purge(value);
42
+ const result = [];
43
+ for (let index = 0; index < length; index += 1) {
44
+ const entry = buffer[(head + index) % options.maxEvents];
45
+ if (entry !== undefined) {
46
+ result.push(entry);
47
+ }
48
+ }
49
+ return result;
50
+ };
51
+ return {
52
+ add,
53
+ count: (timestamp) => timestamps(timestamp).length,
54
+ size: (timestamp) => timestamps(timestamp).length,
55
+ timestamps,
56
+ };
57
+ }
@@ -0,0 +1,29 @@
1
+ const INTERNAL_MARKERS = ["node_modules/", "node:internal/", "internal/"];
2
+ const isInternalFrame = (frame) => {
3
+ const lower = frame.toLowerCase();
4
+ return INTERNAL_MARKERS.some((marker) => lower.includes(marker));
5
+ };
6
+ export function normalizeStackFrame(frame) {
7
+ return frame
8
+ .trim()
9
+ .replace(/^at\s+/, "")
10
+ .replace(/:\d+:\d+(?=\)?$)/, "")
11
+ .replace(/\s+/g, " ");
12
+ }
13
+ export function selectApplicationFrame(stack) {
14
+ if (typeof stack !== "string" || stack.length === 0) {
15
+ return undefined;
16
+ }
17
+ for (const line of stack.split("\n")) {
18
+ const trimmed = line.trim();
19
+ if (!trimmed || /^\w*Error(?::|$)/.test(trimmed)) {
20
+ continue;
21
+ }
22
+ const frame = normalizeStackFrame(trimmed);
23
+ if (!frame || isInternalFrame(frame)) {
24
+ continue;
25
+ }
26
+ return frame;
27
+ }
28
+ return undefined;
29
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import { createWotchi } from "./core/client.js";
2
+ import { registerWotchiProcessMonitor } from "./core/process-monitor.js";
3
+ import { consoleNotifier } from "./notifiers/console.js";
4
+ import { telegramNotifier } from "./notifiers/telegram.js";
5
+ export { WotchiConfigurationError } from "./core/errors.js";
6
+ export { consoleNotifier, createWotchi, registerWotchiProcessMonitor, telegramNotifier };
@@ -0,0 +1,12 @@
1
+ import { getExpressRequestContext } from "./request-context.js";
2
+ export function createExpressErrorHandler(client, options) {
3
+ return (error, request, response, next) => {
4
+ try {
5
+ client.captureException(error, getExpressRequestContext(request, response, options));
6
+ }
7
+ catch {
8
+ // A capture failure must never change Express error handling.
9
+ }
10
+ next(error);
11
+ };
12
+ }
@@ -0,0 +1,5 @@
1
+ import { createExpressErrorHandler } from "./error-handler.js";
2
+ export { consoleNotifier, createWotchi, telegramNotifier } from "../../index.js";
3
+ export function wotchiErrorHandler(client, options) {
4
+ return createExpressErrorHandler(client, options);
5
+ }
@@ -0,0 +1,16 @@
1
+ import { buildRequestContext } from "../request-context.js";
2
+ export function getExpressRequestContext(request, response, options) {
3
+ try {
4
+ const route = request.route?.path;
5
+ return buildRequestContext({
6
+ request,
7
+ method: request.method,
8
+ route: route ?? request.path,
9
+ statusCode: response.statusCode,
10
+ ...(options === undefined ? {} : { options }),
11
+ });
12
+ }
13
+ catch {
14
+ return undefined;
15
+ }
16
+ }
@@ -0,0 +1,33 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { Catch } from "@nestjs/common";
11
+ import { BaseExceptionFilter } from "@nestjs/core";
12
+ import { getNestRequestContext } from "./request-context.js";
13
+ let WotchiNestExceptionFilter = class WotchiNestExceptionFilter extends BaseExceptionFilter {
14
+ constructor(client, applicationRef, options) {
15
+ super(applicationRef);
16
+ this.client = client;
17
+ this.options = options;
18
+ }
19
+ catch(exception, host) {
20
+ try {
21
+ this.client.captureException(exception, getNestRequestContext(host, exception, this.options));
22
+ }
23
+ catch {
24
+ // A capture failure must never change NestJS exception handling.
25
+ }
26
+ super.catch(exception, host);
27
+ }
28
+ };
29
+ WotchiNestExceptionFilter = __decorate([
30
+ Catch(),
31
+ __metadata("design:paramtypes", [Object, Object, Object])
32
+ ], WotchiNestExceptionFilter);
33
+ export { WotchiNestExceptionFilter };
@@ -0,0 +1,2 @@
1
+ export { consoleNotifier, createWotchi, telegramNotifier } from "../../index.js";
2
+ export { registerWotchiNest } from "./register.js";
@@ -0,0 +1,10 @@
1
+ import { HttpAdapterHost } from "@nestjs/core";
2
+ import { WotchiNestExceptionFilter } from "./exception-filter.js";
3
+ export function registerWotchiNest(app, client, options) {
4
+ const application = app;
5
+ const directAdapter = application.getHttpAdapter?.();
6
+ const httpAdapter = directAdapter === undefined
7
+ ? application.get(HttpAdapterHost).httpAdapter
8
+ : directAdapter;
9
+ application.useGlobalFilters(new WotchiNestExceptionFilter(client, httpAdapter, options));
10
+ }
@@ -0,0 +1,38 @@
1
+ import { buildRequestContext } from "../request-context.js";
2
+ const isHttpException = (exception) => typeof exception === "object" &&
3
+ exception !== null &&
4
+ typeof exception.getStatus === "function";
5
+ const getStatusCode = (exception, response) => {
6
+ if (isHttpException(exception)) {
7
+ try {
8
+ const status = exception.getStatus();
9
+ return Number.isSafeInteger(status) ? status : undefined;
10
+ }
11
+ catch {
12
+ return undefined;
13
+ }
14
+ }
15
+ return typeof response.statusCode === "number" &&
16
+ Number.isSafeInteger(response.statusCode) &&
17
+ response.statusCode >= 400
18
+ ? response.statusCode
19
+ : 500;
20
+ };
21
+ export function getNestRequestContext(host, exception, options) {
22
+ try {
23
+ const httpHost = host.switchToHttp();
24
+ const request = httpHost.getRequest();
25
+ const response = httpHost.getResponse();
26
+ const route = request.route?.path ?? request.path ?? request.url;
27
+ return buildRequestContext({
28
+ request,
29
+ method: request.method,
30
+ route,
31
+ statusCode: getStatusCode(exception, response),
32
+ ...(options === undefined ? {} : { options }),
33
+ });
34
+ }
35
+ catch {
36
+ return undefined;
37
+ }
38
+ }
@@ -0,0 +1,72 @@
1
+ const MAX_ROUTE_LENGTH = 500;
2
+ const MAX_REQUEST_ID_LENGTH = 200;
3
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
4
+ const HEX_PATTERN = /^[0-9a-f]{8,}$/i;
5
+ const isRecord = (value) => typeof value === "object" && value !== null;
6
+ const dynamicSegment = (segment) => /^\d+$/.test(segment) ||
7
+ UUID_PATTERN.test(segment) ||
8
+ HEX_PATTERN.test(segment) ||
9
+ segment.length >= 24
10
+ ? ":id"
11
+ : segment;
12
+ export function normalizeRoutePath(value) {
13
+ if (typeof value !== "string") {
14
+ return undefined;
15
+ }
16
+ const path = value.split("?", 1)[0]?.trim();
17
+ if (path === undefined || path.length === 0) {
18
+ return undefined;
19
+ }
20
+ return path.split("/").map(dynamicSegment).join("/").slice(0, MAX_ROUTE_LENGTH);
21
+ }
22
+ const normalizeRequestIdProperty = (property) => {
23
+ if (property === undefined) {
24
+ return undefined;
25
+ }
26
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(property)) {
27
+ throw new TypeError("requestIdProperty must be a simple property name");
28
+ }
29
+ return property;
30
+ };
31
+ const readRequestId = (request, property) => {
32
+ if (property === undefined || !isRecord(request)) {
33
+ return undefined;
34
+ }
35
+ try {
36
+ if (!Object.prototype.hasOwnProperty.call(request, property)) {
37
+ return undefined;
38
+ }
39
+ const value = request[property];
40
+ return typeof value === "string" ? value.slice(0, MAX_REQUEST_ID_LENGTH) : undefined;
41
+ }
42
+ catch {
43
+ return undefined;
44
+ }
45
+ };
46
+ export function buildRequestContext(input) {
47
+ const requestIdProperty = normalizeRequestIdProperty(input.options?.requestIdProperty);
48
+ let requestId;
49
+ try {
50
+ requestId = readRequestId(input.request, requestIdProperty);
51
+ }
52
+ catch {
53
+ requestId = undefined;
54
+ }
55
+ const method = typeof input.method === "string" ? input.method.slice(0, 16) : undefined;
56
+ const route = normalizeRoutePath(input.route);
57
+ const statusCode = typeof input.statusCode === "number" && Number.isSafeInteger(input.statusCode)
58
+ ? input.statusCode
59
+ : undefined;
60
+ if (method === undefined &&
61
+ route === undefined &&
62
+ statusCode === undefined &&
63
+ requestId === undefined) {
64
+ return undefined;
65
+ }
66
+ return {
67
+ ...(method === undefined ? {} : { method }),
68
+ ...(route === undefined ? {} : { route }),
69
+ ...(statusCode === undefined ? {} : { statusCode }),
70
+ ...(requestId === undefined ? {} : { requestId }),
71
+ };
72
+ }
@@ -0,0 +1,29 @@
1
+ const MAX_FIELD_LENGTH = 1_000;
2
+ const MAX_ACTIONS = 5;
3
+ const limit = (value, maxLength = MAX_FIELD_LENGTH) => value.slice(0, maxLength);
4
+ export function formatConsoleAlert(alert) {
5
+ const actions = alert.suggestedActions
6
+ .slice(0, MAX_ACTIONS)
7
+ .map((action) => `- ${limit(action, 300)}`)
8
+ .join("\n");
9
+ return [
10
+ limit(alert.title),
11
+ `Service: ${limit(alert.service, 300)}`,
12
+ `Environment: ${limit(alert.environment, 300)}`,
13
+ `Summary: ${limit(alert.summary)}`,
14
+ `Occurrences: ${alert.occurrences}`,
15
+ `First seen: ${limit(alert.firstSeenAt, 100)}`,
16
+ `Last seen: ${limit(alert.lastSeenAt, 100)}`,
17
+ "Suggested checks:",
18
+ actions,
19
+ ].join("\n");
20
+ }
21
+ export function consoleNotifier(options) {
22
+ const write = options?.write ?? ((line) => console.error(line));
23
+ return {
24
+ name: "console",
25
+ async send(alert) {
26
+ write(formatConsoleAlert(alert));
27
+ },
28
+ };
29
+ }
@@ -0,0 +1,49 @@
1
+ const MAX_MESSAGE_LENGTH = 4_096;
2
+ const MAX_ACTIONS = 5;
3
+ const MAX_TITLE_LENGTH = 300;
4
+ const MAX_SERVICE_LENGTH = 300;
5
+ const MAX_ENVIRONMENT_LENGTH = 300;
6
+ const MAX_SUMMARY_LENGTH = 2_000;
7
+ const MAX_ACTION_LENGTH = 600;
8
+ const escapeHtml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
9
+ const limit = (value, maxLength) => value.slice(0, maxLength);
10
+ const render = (title, service, environment, summary, actions, firstSeenAt, lastSeenAt, occurrences) => {
11
+ const actionText = actions.length === 0
12
+ ? "- No suggested checks."
13
+ : actions.map((action) => `- ${escapeHtml(action)}`).join("\n");
14
+ return [
15
+ `<b>${escapeHtml(title)}</b>`,
16
+ `Service: ${escapeHtml(service)}`,
17
+ `Environment: ${escapeHtml(environment)}`,
18
+ `Summary: ${escapeHtml(summary)}`,
19
+ `Occurrences: ${occurrences}`,
20
+ `First seen: ${escapeHtml(firstSeenAt)}`,
21
+ `Last seen: ${escapeHtml(lastSeenAt)}`,
22
+ "Suggested checks:",
23
+ actionText,
24
+ ].join("\n");
25
+ };
26
+ export function formatTelegramAlert(alert) {
27
+ const title = limit(alert.title, MAX_TITLE_LENGTH);
28
+ const service = limit(alert.service, MAX_SERVICE_LENGTH);
29
+ const environment = limit(alert.environment, MAX_ENVIRONMENT_LENGTH);
30
+ const firstSeenAt = limit(alert.firstSeenAt, 100);
31
+ const lastSeenAt = limit(alert.lastSeenAt, 100);
32
+ let summary = limit(alert.summary, MAX_SUMMARY_LENGTH);
33
+ let actions = alert.suggestedActions
34
+ .slice(0, MAX_ACTIONS)
35
+ .map((action) => limit(action, MAX_ACTION_LENGTH));
36
+ let formatted = render(title, service, environment, summary, actions, firstSeenAt, lastSeenAt, alert.occurrences);
37
+ while (formatted.length > MAX_MESSAGE_LENGTH && actions.length > 0) {
38
+ actions = actions.slice(0, -1);
39
+ formatted = render(title, service, environment, summary, actions, firstSeenAt, lastSeenAt, alert.occurrences);
40
+ }
41
+ while (formatted.length > MAX_MESSAGE_LENGTH && summary.length > 0) {
42
+ summary = summary.slice(0, Math.max(0, summary.length - 200));
43
+ formatted = render(title, service, environment, summary, actions, firstSeenAt, lastSeenAt, alert.occurrences);
44
+ }
45
+ if (formatted.length > MAX_MESSAGE_LENGTH) {
46
+ formatted = render(limit(title, 100), limit(service, 100), limit(environment, 100), "Incident details were truncated.", [], firstSeenAt, lastSeenAt, alert.occurrences);
47
+ }
48
+ return formatted;
49
+ }