@futurewindai/wotchi 0.1.0-beta.2 → 0.1.0-beta.6

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 (103) hide show
  1. package/README.md +178 -47
  2. package/SECURITY.md +2 -2
  3. package/dist/cjs/core/admission.js +22 -0
  4. package/dist/cjs/core/client.js +416 -21
  5. package/dist/cjs/core/config.js +193 -18
  6. package/dist/cjs/core/diagnostics.js +16 -1
  7. package/dist/cjs/core/group-store.js +22 -9
  8. package/dist/cjs/core/incident-builder.js +18 -0
  9. package/dist/cjs/core/incident-policy.js +2 -7
  10. package/dist/cjs/core/limits.js +17 -0
  11. package/dist/cjs/core/normalize.js +16 -6
  12. package/dist/cjs/core/notification-queue.js +92 -6
  13. package/dist/cjs/core/prometheus.js +129 -0
  14. package/dist/cjs/core/redact.js +57 -8
  15. package/dist/cjs/core/runtime-monitor.js +109 -0
  16. package/dist/cjs/index.js +8 -1
  17. package/dist/cjs/integrations/express/error-handler.js +21 -4
  18. package/dist/cjs/integrations/express/index.js +2 -1
  19. package/dist/cjs/integrations/express/status-observer.js +6 -0
  20. package/dist/cjs/integrations/nest/capture.js +21 -0
  21. package/dist/cjs/integrations/nest/exception-filter.js +64 -8
  22. package/dist/cjs/integrations/nest/filter-wrapper.js +17 -0
  23. package/dist/cjs/integrations/nest/index.js +7 -1
  24. package/dist/cjs/integrations/nest/module.js +44 -0
  25. package/dist/cjs/integrations/nest/register.js +15 -1
  26. package/dist/cjs/integrations/request-context.js +83 -1
  27. package/dist/cjs/notifiers/alert-payload.js +20 -0
  28. package/dist/cjs/notifiers/console.js +85 -15
  29. package/dist/cjs/notifiers/telegram-format.js +75 -5
  30. package/dist/cjs/notifiers/webhook-http.js +394 -0
  31. package/dist/cjs/notifiers/webhook.js +28 -0
  32. package/dist/esm/core/admission.js +19 -0
  33. package/dist/esm/core/client.js +416 -21
  34. package/dist/esm/core/config.js +193 -18
  35. package/dist/esm/core/diagnostics.js +16 -1
  36. package/dist/esm/core/group-store.js +22 -9
  37. package/dist/esm/core/incident-builder.js +18 -0
  38. package/dist/esm/core/incident-policy.js +2 -7
  39. package/dist/esm/core/limits.js +14 -0
  40. package/dist/esm/core/normalize.js +16 -6
  41. package/dist/esm/core/notification-queue.js +92 -6
  42. package/dist/esm/core/prometheus.js +125 -0
  43. package/dist/esm/core/redact.js +57 -8
  44. package/dist/esm/core/runtime-monitor.js +106 -0
  45. package/dist/esm/index.js +4 -1
  46. package/dist/esm/integrations/express/error-handler.js +21 -4
  47. package/dist/esm/integrations/express/index.js +1 -1
  48. package/dist/esm/integrations/express/status-observer.js +6 -0
  49. package/dist/esm/integrations/nest/capture.js +17 -0
  50. package/dist/esm/integrations/nest/exception-filter.js +65 -9
  51. package/dist/esm/integrations/nest/filter-wrapper.js +14 -0
  52. package/dist/esm/integrations/nest/index.js +3 -1
  53. package/dist/esm/integrations/nest/module.js +41 -0
  54. package/dist/esm/integrations/nest/register.js +15 -1
  55. package/dist/esm/integrations/request-context.js +83 -1
  56. package/dist/esm/notifiers/alert-payload.js +16 -0
  57. package/dist/esm/notifiers/console.js +85 -15
  58. package/dist/esm/notifiers/telegram-format.js +75 -5
  59. package/dist/esm/notifiers/webhook-http.js +387 -0
  60. package/dist/esm/notifiers/webhook.js +24 -0
  61. package/dist/types/core/admission.d.ts +10 -0
  62. package/dist/types/core/config.d.ts +17 -1
  63. package/dist/types/core/diagnostics.d.ts +7 -1
  64. package/dist/types/core/incident-policy.d.ts +1 -1
  65. package/dist/types/core/limits.d.ts +14 -0
  66. package/dist/types/core/notification-queue.d.ts +14 -1
  67. package/dist/types/core/prometheus.d.ts +7 -0
  68. package/dist/types/core/runtime-monitor.d.ts +15 -0
  69. package/dist/types/core/types.d.ts +111 -2
  70. package/dist/types/index.d.ts +8 -2
  71. package/dist/types/integrations/express/index.d.ts +2 -2
  72. package/dist/types/integrations/nest/capture.d.ts +4 -0
  73. package/dist/types/integrations/nest/exception-filter.d.ts +6 -2
  74. package/dist/types/integrations/nest/filter-wrapper.d.ts +7 -0
  75. package/dist/types/integrations/nest/index.d.ts +6 -2
  76. package/dist/types/integrations/nest/module.d.ts +9 -0
  77. package/dist/types/integrations/nest/register.d.ts +3 -0
  78. package/dist/types/integrations/request-context.d.ts +3 -0
  79. package/dist/types/notifiers/alert-payload.d.ts +5 -0
  80. package/dist/types/notifiers/webhook-http.d.ts +37 -0
  81. package/dist/types/notifiers/webhook.d.ts +4 -0
  82. package/dist/types-cjs/core/admission.d.cts +10 -0
  83. package/dist/types-cjs/core/config.d.cts +17 -1
  84. package/dist/types-cjs/core/diagnostics.d.cts +7 -1
  85. package/dist/types-cjs/core/incident-policy.d.cts +1 -1
  86. package/dist/types-cjs/core/limits.d.cts +14 -0
  87. package/dist/types-cjs/core/notification-queue.d.cts +14 -1
  88. package/dist/types-cjs/core/prometheus.d.cts +7 -0
  89. package/dist/types-cjs/core/runtime-monitor.d.cts +15 -0
  90. package/dist/types-cjs/core/types.d.cts +111 -2
  91. package/dist/types-cjs/index.d.cts +8 -2
  92. package/dist/types-cjs/integrations/express/index.d.cts +2 -2
  93. package/dist/types-cjs/integrations/nest/capture.d.cts +4 -0
  94. package/dist/types-cjs/integrations/nest/exception-filter.d.cts +6 -2
  95. package/dist/types-cjs/integrations/nest/filter-wrapper.d.cts +7 -0
  96. package/dist/types-cjs/integrations/nest/index.d.cts +6 -2
  97. package/dist/types-cjs/integrations/nest/module.d.cts +9 -0
  98. package/dist/types-cjs/integrations/nest/register.d.cts +3 -0
  99. package/dist/types-cjs/integrations/request-context.d.cts +3 -0
  100. package/dist/types-cjs/notifiers/alert-payload.d.cts +5 -0
  101. package/dist/types-cjs/notifiers/webhook-http.d.cts +37 -0
  102. package/dist/types-cjs/notifiers/webhook.d.cts +4 -0
  103. package/package.json +17 -4
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PROMETHEUS_CONTENT_TYPE = void 0;
4
+ exports.createWotchiPrometheusExporter = createWotchiPrometheusExporter;
5
+ exports.PROMETHEUS_CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8";
6
+ const METRICS = [
7
+ {
8
+ name: "captured_events_total",
9
+ help: "Total events accepted by Wotchi.",
10
+ type: "counter",
11
+ value: (diagnostics) => diagnostics.capturedEvents,
12
+ },
13
+ {
14
+ name: "capture_failures_total",
15
+ help: "Capture failures contained by Wotchi.",
16
+ type: "counter",
17
+ value: (diagnostics) => diagnostics.captureFailures,
18
+ },
19
+ {
20
+ name: "groups_evicted_total",
21
+ help: "Groups evicted from bounded memory.",
22
+ type: "counter",
23
+ value: (diagnostics) => diagnostics.groupsEvicted,
24
+ },
25
+ {
26
+ name: "alerts_queued_total",
27
+ help: "Alerts admitted to the notification queue.",
28
+ type: "counter",
29
+ value: (diagnostics) => diagnostics.alertsQueued,
30
+ },
31
+ {
32
+ name: "alerts_dropped_total",
33
+ help: "Alerts dropped because the notification queue was full.",
34
+ type: "counter",
35
+ value: (diagnostics) => diagnostics.alertsDropped,
36
+ },
37
+ {
38
+ name: "alerts_sent_total",
39
+ help: "Alerts delivered successfully to notifiers.",
40
+ type: "counter",
41
+ value: (diagnostics) => diagnostics.alertsSent,
42
+ },
43
+ {
44
+ name: "notifier_failures_total",
45
+ help: "Notifier delivery failures contained by Wotchi.",
46
+ type: "counter",
47
+ value: (diagnostics) => diagnostics.notifierFailures,
48
+ },
49
+ {
50
+ name: "fingerprint_callback_failures_total",
51
+ help: "Fingerprint callback failures contained by Wotchi.",
52
+ type: "counter",
53
+ value: (diagnostics) => diagnostics.fingerprintCallbackFailures,
54
+ },
55
+ {
56
+ name: "filter_failures_total",
57
+ help: "Event filter failures contained by Wotchi.",
58
+ type: "counter",
59
+ value: (diagnostics) => diagnostics.filterFailures,
60
+ },
61
+ {
62
+ name: "before_send_failures_total",
63
+ help: "Before-send callback failures contained by Wotchi.",
64
+ type: "counter",
65
+ value: (diagnostics) => diagnostics.beforeSendFailures,
66
+ },
67
+ {
68
+ name: "events_suppressed_total",
69
+ help: "Events suppressed by filtering, thresholds, or cooldowns.",
70
+ type: "counter",
71
+ value: (diagnostics) => diagnostics.eventsSuppressed,
72
+ },
73
+ {
74
+ name: "events_dropped_overload_total",
75
+ help: "Events dropped before normalization by the optional overload admission limit.",
76
+ type: "counter",
77
+ value: (diagnostics) => diagnostics.eventsDroppedOverload,
78
+ },
79
+ {
80
+ name: "captures_after_shutdown_total",
81
+ help: "Capture calls ignored after client shutdown.",
82
+ type: "counter",
83
+ value: (diagnostics) => diagnostics.capturesAfterShutdown,
84
+ },
85
+ {
86
+ name: "notifier_timeouts_total",
87
+ help: "Notifier deliveries that exceeded the configured timeout.",
88
+ type: "counter",
89
+ value: (diagnostics) => diagnostics.notifierTimeouts,
90
+ },
91
+ {
92
+ name: "notifier_circuit_open_skips_total",
93
+ help: "Notifier deliveries skipped while the notifier circuit was open.",
94
+ type: "counter",
95
+ value: (diagnostics) => diagnostics.notifierCircuitOpenSkips,
96
+ },
97
+ {
98
+ name: "active_groups",
99
+ help: "Current in-memory incident groups.",
100
+ type: "gauge",
101
+ value: (diagnostics) => diagnostics.activeGroups,
102
+ },
103
+ {
104
+ name: "pending_alerts",
105
+ help: "Current alerts waiting for notifier delivery.",
106
+ type: "gauge",
107
+ value: (diagnostics) => diagnostics.pendingAlerts,
108
+ },
109
+ ];
110
+ const safeMetricValue = (value) => {
111
+ if (!Number.isFinite(value) || value <= 0) {
112
+ return 0;
113
+ }
114
+ return Math.min(Math.floor(value), Number.MAX_SAFE_INTEGER);
115
+ };
116
+ const renderMetrics = (diagnostics) => {
117
+ const lines = [];
118
+ for (const metric of METRICS) {
119
+ lines.push(`# HELP wotchi_${metric.name} ${metric.help}`, `# TYPE wotchi_${metric.name} ${metric.type}`, `wotchi_${metric.name} ${safeMetricValue(metric.value(diagnostics))}`);
120
+ }
121
+ lines.push("");
122
+ return lines.join("\n");
123
+ };
124
+ function createWotchiPrometheusExporter(client) {
125
+ return Object.freeze({
126
+ contentType: exports.PROMETHEUS_CONTENT_TYPE,
127
+ render: () => renderMetrics(client.getDiagnostics()),
128
+ });
129
+ }
@@ -4,6 +4,7 @@ exports.REDACTED = void 0;
4
4
  exports.redactValue = redactValue;
5
5
  exports.redactError = redactError;
6
6
  const normalize_js_1 = require("./normalize.js");
7
+ const limits_js_1 = require("./limits.js");
7
8
  exports.REDACTED = "[REDACTED]";
8
9
  const DEFAULT_KEYS = [
9
10
  "authorization",
@@ -14,6 +15,12 @@ const DEFAULT_KEYS = [
14
15
  "secret",
15
16
  "apiKey",
16
17
  "api_key",
18
+ "xApiKey",
19
+ "x_api_key",
20
+ "dbPassword",
21
+ "db_password",
22
+ "databasePassword",
23
+ "database_password",
17
24
  "token",
18
25
  "accessToken",
19
26
  "access_token",
@@ -23,12 +30,16 @@ const DEFAULT_KEYS = [
23
30
  "private_key",
24
31
  "clientSecret",
25
32
  "client_secret",
33
+ "signature",
34
+ "session",
35
+ "webhookSecret",
36
+ "webhook_secret",
26
37
  "cardNumber",
27
38
  "cvv",
28
39
  "cvc",
29
40
  "pin",
30
41
  ];
31
- const normalizeKey = (key) => key.toLowerCase().replace(/[-_]/g, "");
42
+ const normalizeKey = (key) => key.toLowerCase().replace(/[^a-z0-9]/g, "");
32
43
  const defaultKeySet = new Set(DEFAULT_KEYS.map(normalizeKey));
33
44
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
34
45
  const luhnValid = (digits) => {
@@ -54,6 +65,7 @@ const redactCardLikeValues = (value) => value.replace(/\b\d[\d -]{11,25}\d\b/g,
54
65
  const CONNECTION_URL_PATTERN = /\b(?:postgres(?:ql)?|rediss?|mongodb(?:\+srv)?):\/\/[^\s"'<>]+/gi;
55
66
  const CONNECTION_QUERY_SECRET_PATTERN = /([?&](?:password|passwd|pass|secret|token|api[-_]?key|access[-_]?token|refresh[-_]?token|sslpassword)=)[^&#\s]*/gi;
56
67
  const URL_TRAILING_PUNCTUATION = /[),.;!?]+$/;
68
+ const AUTHORITY_USERINFO_PATTERN = /\b[^:\s/@]+:[^\s/@]+@(?=(?:[a-z0-9-]+(?:\.[a-z0-9-]+)*|\[[0-9a-f:.]+\])(?::\d+)?(?:[/?#\s,;.)]|$))/gi;
57
69
  const redactConnectionUrl = (candidate) => {
58
70
  let core = candidate;
59
71
  let suffix = "";
@@ -77,20 +89,57 @@ const redactConnectionUrl = (candidate) => {
77
89
  return `${core.slice(0, authorityStart)}${redactedAuthority}${remainder}${suffix}`;
78
90
  };
79
91
  const redactConnectionUrls = (value) => value.replace(CONNECTION_URL_PATTERN, redactConnectionUrl);
92
+ const redactAuthorityUserinfo = (value) => value.replace(AUTHORITY_USERINFO_PATTERN, `${exports.REDACTED}@`);
93
+ const SENSITIVE_QUERY_KEYS = new Set([
94
+ "authorization",
95
+ "cookie",
96
+ "password",
97
+ "passwd",
98
+ "secret",
99
+ "signature",
100
+ "token",
101
+ "accesstoken",
102
+ "refreshtoken",
103
+ "apikey",
104
+ "xapikey",
105
+ "session",
106
+ ]);
107
+ const decodeQueryKey = (value) => {
108
+ try {
109
+ return decodeURIComponent(value.replace(/\+/g, " "));
110
+ }
111
+ catch {
112
+ return value;
113
+ }
114
+ };
115
+ const redactSensitiveQueryValues = (value) => value.replace(/([?&])([^=&#\s]+)=([^&#\s]*)/g, (match, prefix, key) => {
116
+ if (!SENSITIVE_QUERY_KEYS.has(normalizeKey(decodeQueryKey(key)))) {
117
+ return match;
118
+ }
119
+ return prefix + key + "=" + exports.REDACTED;
120
+ });
121
+ const redactWebhookPathSecrets = (value) => value.replace(/(https?:\/\/[^/\s]+\/(?:services|hooks|webhooks?)\/)[^/?#\s]+/gi, (_match, prefix) => prefix + exports.REDACTED);
80
122
  const redactString = (value, maxStringLength) => {
81
123
  let result = redactConnectionUrls(value);
124
+ result = redactAuthorityUserinfo(result);
125
+ result = redactWebhookPathSecrets(result);
126
+ result = redactSensitiveQueryValues(result);
82
127
  result = result.replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi, exports.REDACTED);
83
128
  result = result.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, `Bearer ${exports.REDACTED}`);
84
- result = result.replace(/\b(?:authorization|password|passwd|secret|token|api[-_]?key)\s*[:=]\s*[^\s,;]+/gi, (match) => {
129
+ result = result.replace(/\bBasic\s+[A-Za-z0-9+/=]+/gi, `Basic ${exports.REDACTED}`);
130
+ result = result.replace(/\b(?:Cookie|Set-Cookie)\s*:\s*[^\r\n]+/gi, "Cookie: [REDACTED]");
131
+ result = result.replace(/\b(?:authorization|password|passwd|secret|signature|session|token|x[-_]?api[-_]?key|db[-_]?password|database[-_]?password|api[-_]?key)\s*[:=]\s*[^\s,;]+/gi, (match) => {
85
132
  const separatorIndex = match.search(/[:=]/);
86
133
  return separatorIndex < 0 ? exports.REDACTED : `${match.slice(0, separatorIndex + 1)}${exports.REDACTED}`;
87
134
  });
88
- result = result.replace(/\b[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g, exports.REDACTED);
135
+ result = result.replace(/\b[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, exports.REDACTED);
89
136
  result = result.replace(/\b(?:github_pat|ghp|xox[baprs]-|sk|pk|AKIA)[A-Za-z0-9_-]{8,}\b/gi, exports.REDACTED);
90
137
  result = redactCardLikeValues(result);
91
138
  return result.slice(0, maxStringLength);
92
139
  };
93
- const normalizedOption = (value, fallback) => Number.isSafeInteger(value) && value !== undefined && value > 0 ? value : fallback;
140
+ const normalizedOption = (value, fallback, maximum) => Number.isSafeInteger(value) && value !== undefined && value > 0 && value <= maximum
141
+ ? value
142
+ : fallback;
94
143
  const redactNormalized = (value, sensitiveKeys, options, depth, keysVisited) => {
95
144
  if (typeof value === "string") {
96
145
  return redactString(value, options.maxStringLength);
@@ -128,10 +177,10 @@ const redactNormalized = (value, sensitiveKeys, options, depth, keysVisited) =>
128
177
  return result;
129
178
  };
130
179
  const redactionLimits = (options) => ({
131
- maxDepth: normalizedOption(options.maxDepth, 5),
132
- maxKeys: normalizedOption(options.maxKeys, 100),
133
- maxStringLength: normalizedOption(options.maxStringLength, 500),
134
- maxStackLength: normalizedOption(options.maxStackLength, 4_000),
180
+ maxDepth: normalizedOption(options.maxDepth, 5, limits_js_1.MAX_NORMALIZATION_DEPTH),
181
+ maxKeys: normalizedOption(options.maxKeys, 100, limits_js_1.MAX_NORMALIZATION_KEYS),
182
+ maxStringLength: normalizedOption(options.maxStringLength, 500, limits_js_1.MAX_NORMALIZATION_STRING_LENGTH),
183
+ maxStackLength: normalizedOption(options.maxStackLength, 4_000, limits_js_1.MAX_NORMALIZATION_STACK_LENGTH),
135
184
  });
136
185
  const sensitiveKeySet = (options) => {
137
186
  const keys = new Set(defaultKeySet);
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerWotchiRuntimeWatcher = registerWotchiRuntimeWatcher;
4
+ const DEFAULT_INTERVAL_MS = 5_000;
5
+ const MIN_INTERVAL_MS = 100;
6
+ const MAX_INTERVAL_MS = 60_000;
7
+ const MAX_THRESHOLD = Number.MAX_SAFE_INTEGER;
8
+ const positiveNumber = (value, fallback, field) => {
9
+ if (value === undefined) {
10
+ return fallback;
11
+ }
12
+ if (!Number.isFinite(value) || value <= 0 || value > MAX_THRESHOLD) {
13
+ throw new RangeError(`${field} must be a positive finite number`);
14
+ }
15
+ return value;
16
+ };
17
+ function registerWotchiRuntimeWatcher(client, options = {}) {
18
+ const intervalMs = positiveNumber(options.intervalMs, DEFAULT_INTERVAL_MS, "intervalMs");
19
+ if (!Number.isSafeInteger(intervalMs) ||
20
+ intervalMs < MIN_INTERVAL_MS ||
21
+ intervalMs > MAX_INTERVAL_MS) {
22
+ throw new RangeError(`intervalMs must be an integer from ${MIN_INTERVAL_MS} to ${MAX_INTERVAL_MS}`);
23
+ }
24
+ const thresholds = {
25
+ cpuPercent: options.cpuPercent,
26
+ rssBytes: options.rssBytes,
27
+ heapUsedBytes: options.heapUsedBytes,
28
+ eventLoopDelayMs: options.eventLoopDelayMs,
29
+ pendingAlerts: options.pendingAlerts,
30
+ notifierFailures: options.notifierFailures,
31
+ };
32
+ for (const [field, value] of Object.entries(thresholds)) {
33
+ if (value !== undefined) {
34
+ positiveNumber(value, 1, field);
35
+ }
36
+ }
37
+ const alertThreshold = options.alertThreshold ?? 3;
38
+ if (!Number.isSafeInteger(alertThreshold) || alertThreshold <= 0 || alertThreshold > 1_000_000) {
39
+ throw new RangeError("alertThreshold must be a positive integer no greater than 1000000");
40
+ }
41
+ let previousCpu = process.cpuUsage();
42
+ let previousTime = process.hrtime.bigint();
43
+ let previousNotifierFailures = client.getDiagnostics().notifierFailures;
44
+ let expectedAt = Date.now() + intervalMs;
45
+ let registered = true;
46
+ const sample = () => {
47
+ if (!registered) {
48
+ return;
49
+ }
50
+ const timestamp = Date.now();
51
+ const elapsedMicros = Math.max(1, Number(process.hrtime.bigint() - previousTime) / 1_000);
52
+ const cpu = process.cpuUsage(previousCpu);
53
+ previousCpu = process.cpuUsage();
54
+ previousTime = process.hrtime.bigint();
55
+ const memory = process.memoryUsage();
56
+ const eventLoopDelayMs = Math.max(0, timestamp - expectedAt);
57
+ expectedAt = timestamp + intervalMs;
58
+ const cpuPercent = ((cpu.user + cpu.system) / elapsedMicros) * 100;
59
+ const diagnostics = client.getDiagnostics();
60
+ const notifierFailures = Math.max(0, diagnostics.notifierFailures - previousNotifierFailures);
61
+ previousNotifierFailures = diagnostics.notifierFailures;
62
+ const readings = [
63
+ { metric: "cpu-percent", value: cpuPercent, threshold: thresholds.cpuPercent },
64
+ { metric: "rss-bytes", value: memory.rss, threshold: thresholds.rssBytes },
65
+ { metric: "heap-used-bytes", value: memory.heapUsed, threshold: thresholds.heapUsedBytes },
66
+ {
67
+ metric: "event-loop-delay-ms",
68
+ value: eventLoopDelayMs,
69
+ threshold: thresholds.eventLoopDelayMs,
70
+ },
71
+ { metric: "pending-alerts", value: 0, threshold: thresholds.pendingAlerts },
72
+ {
73
+ metric: "notifier-failures",
74
+ value: notifierFailures,
75
+ threshold: thresholds.notifierFailures,
76
+ },
77
+ ];
78
+ for (const reading of readings) {
79
+ const value = reading.metric === "pending-alerts" ? diagnostics.pendingAlerts : reading.value;
80
+ if (reading.threshold === undefined || value < reading.threshold) {
81
+ continue;
82
+ }
83
+ client.captureEvent({
84
+ level: "error",
85
+ kind: "runtime-monitor",
86
+ message: `Wotchi runtime pressure: ${reading.metric}`,
87
+ fingerprint: `wotchi.runtime.${reading.metric}`,
88
+ severity: "high",
89
+ alertThreshold,
90
+ context: {
91
+ metric: reading.metric,
92
+ value: Math.round(value * 100) / 100,
93
+ threshold: reading.threshold,
94
+ },
95
+ });
96
+ }
97
+ };
98
+ const timer = setInterval(sample, intervalMs);
99
+ timer.unref?.();
100
+ return {
101
+ unregister: () => {
102
+ if (!registered) {
103
+ return;
104
+ }
105
+ registered = false;
106
+ clearInterval(timer);
107
+ },
108
+ };
109
+ }
package/dist/cjs/index.js CHANGED
@@ -1,13 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.telegramNotifier = exports.registerWotchiProcessMonitor = exports.createWotchi = exports.consoleNotifier = exports.WotchiConfigurationError = void 0;
3
+ exports.webhookNotifier = exports.telegramNotifier = exports.registerWotchiRuntimeWatcher = exports.registerWotchiProcessMonitor = exports.PROMETHEUS_CONTENT_TYPE = exports.createWotchiPrometheusExporter = exports.createWotchi = exports.consoleNotifier = exports.WotchiConfigurationError = void 0;
4
4
  const client_js_1 = require("./core/client.js");
5
5
  Object.defineProperty(exports, "createWotchi", { enumerable: true, get: function () { return client_js_1.createWotchi; } });
6
6
  const process_monitor_js_1 = require("./core/process-monitor.js");
7
7
  Object.defineProperty(exports, "registerWotchiProcessMonitor", { enumerable: true, get: function () { return process_monitor_js_1.registerWotchiProcessMonitor; } });
8
+ const prometheus_js_1 = require("./core/prometheus.js");
9
+ Object.defineProperty(exports, "createWotchiPrometheusExporter", { enumerable: true, get: function () { return prometheus_js_1.createWotchiPrometheusExporter; } });
10
+ Object.defineProperty(exports, "PROMETHEUS_CONTENT_TYPE", { enumerable: true, get: function () { return prometheus_js_1.PROMETHEUS_CONTENT_TYPE; } });
11
+ const runtime_monitor_js_1 = require("./core/runtime-monitor.js");
12
+ Object.defineProperty(exports, "registerWotchiRuntimeWatcher", { enumerable: true, get: function () { return runtime_monitor_js_1.registerWotchiRuntimeWatcher; } });
8
13
  const console_js_1 = require("./notifiers/console.js");
9
14
  Object.defineProperty(exports, "consoleNotifier", { enumerable: true, get: function () { return console_js_1.consoleNotifier; } });
10
15
  const telegram_js_1 = require("./notifiers/telegram.js");
11
16
  Object.defineProperty(exports, "telegramNotifier", { enumerable: true, get: function () { return telegram_js_1.telegramNotifier; } });
17
+ const webhook_js_1 = require("./notifiers/webhook.js");
18
+ Object.defineProperty(exports, "webhookNotifier", { enumerable: true, get: function () { return webhook_js_1.webhookNotifier; } });
12
19
  var errors_js_1 = require("./core/errors.js");
13
20
  Object.defineProperty(exports, "WotchiConfigurationError", { enumerable: true, get: function () { return errors_js_1.WotchiConfigurationError; } });
@@ -6,11 +6,28 @@ const state_js_1 = require("./state.js");
6
6
  function createExpressErrorHandler(client, options) {
7
7
  return (error, request, response, next) => {
8
8
  (0, state_js_1.markExpressErrorCaptured)(request);
9
- try {
10
- client.captureException(error, (0, request_context_js_1.getExpressRequestContext)(request, response, options));
9
+ let captured = false;
10
+ const captureAfterResponse = () => {
11
+ if (captured) {
12
+ return;
13
+ }
14
+ captured = true;
15
+ response.off("finish", captureAfterResponse);
16
+ response.off("close", captureAfterResponse);
17
+ try {
18
+ const requestContext = (0, request_context_js_1.getExpressRequestContext)(request, response, options);
19
+ client.captureException(error, undefined, requestContext === undefined ? undefined : { request: requestContext });
20
+ }
21
+ catch {
22
+ // A capture failure must never change Express error handling.
23
+ }
24
+ };
25
+ if (response.writableEnded || response.finished) {
26
+ captureAfterResponse();
11
27
  }
12
- catch {
13
- // A capture failure must never change Express error handling.
28
+ else {
29
+ response.once("finish", captureAfterResponse);
30
+ response.once("close", captureAfterResponse);
14
31
  }
15
32
  next(error);
16
33
  };
@@ -1,12 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.wotchiStatusObserver = exports.telegramNotifier = exports.createWotchi = exports.consoleNotifier = void 0;
3
+ exports.wotchiStatusObserver = exports.webhookNotifier = exports.telegramNotifier = exports.createWotchi = exports.consoleNotifier = void 0;
4
4
  exports.wotchiErrorHandler = wotchiErrorHandler;
5
5
  const error_handler_js_1 = require("./error-handler.js");
6
6
  var index_js_1 = require("../../index.js");
7
7
  Object.defineProperty(exports, "consoleNotifier", { enumerable: true, get: function () { return index_js_1.consoleNotifier; } });
8
8
  Object.defineProperty(exports, "createWotchi", { enumerable: true, get: function () { return index_js_1.createWotchi; } });
9
9
  Object.defineProperty(exports, "telegramNotifier", { enumerable: true, get: function () { return index_js_1.telegramNotifier; } });
10
+ Object.defineProperty(exports, "webhookNotifier", { enumerable: true, get: function () { return index_js_1.webhookNotifier; } });
10
11
  var status_observer_js_1 = require("./status-observer.js");
11
12
  Object.defineProperty(exports, "wotchiStatusObserver", { enumerable: true, get: function () { return status_observer_js_1.wotchiStatusObserver; } });
12
13
  function wotchiErrorHandler(client, options) {
@@ -42,6 +42,12 @@ const normalizeOptions = (options) => {
42
42
  ...(options?.requestIdProperty === undefined
43
43
  ? {}
44
44
  : { requestIdProperty: options.requestIdProperty }),
45
+ ...(options?.correlationIdProperty === undefined
46
+ ? {}
47
+ : { correlationIdProperty: options.correlationIdProperty }),
48
+ ...(options?.traceContextProperty === undefined
49
+ ? {}
50
+ : { traceContextProperty: options.traceContextProperty }),
45
51
  statusCodes: normalizeStatuses(options?.statusCodes, DEFAULT_STATUS_CODES),
46
52
  statusClasses: normalizeStatusClasses(options?.statusClasses),
47
53
  ignoreStatusCodes: normalizeStatuses(options?.ignoreStatusCodes, []),
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.captureWotchiNestException = void 0;
4
+ const state_js_1 = require("../express/state.js");
5
+ const request_context_js_1 = require("./request-context.js");
6
+ const captureWotchiNestException = (client, exception, host, options) => {
7
+ try {
8
+ (0, state_js_1.markExpressErrorCaptured)(host.switchToHttp().getRequest());
9
+ }
10
+ catch {
11
+ // Non-HTTP NestJS contexts do not expose an Express request to mark.
12
+ }
13
+ try {
14
+ const requestContext = (0, request_context_js_1.getNestRequestContext)(host, exception, options);
15
+ client.captureException(exception, undefined, requestContext === undefined ? undefined : { request: requestContext });
16
+ }
17
+ catch {
18
+ // A capture failure must never change NestJS exception handling.
19
+ }
20
+ };
21
+ exports.captureWotchiNestException = captureWotchiNestException;
@@ -12,25 +12,81 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.WotchiNestExceptionFilter = void 0;
13
13
  const common_1 = require("@nestjs/common");
14
14
  const core_1 = require("@nestjs/core");
15
- const request_context_js_1 = require("./request-context.js");
15
+ const capture_js_1 = require("./capture.js");
16
+ const FILTER_CATCH_EXCEPTIONS = "__filterCatchExceptions__";
17
+ const matchesException = (filter, exception) => {
18
+ const reflection = Reflect;
19
+ const exceptionTypes = reflection.getMetadata?.(FILTER_CATCH_EXCEPTIONS, filter.constructor);
20
+ if (!Array.isArray(exceptionTypes) || exceptionTypes.length === 0) {
21
+ return true;
22
+ }
23
+ return exceptionTypes.some((exceptionType) => typeof exceptionType === "function" &&
24
+ exception instanceof exceptionType);
25
+ };
16
26
  let WotchiNestExceptionFilter = class WotchiNestExceptionFilter extends core_1.BaseExceptionFilter {
17
- constructor(client, applicationRef, options) {
27
+ constructor(client, applicationRef, options, previousGlobalFilters = []) {
18
28
  super(applicationRef);
19
29
  this.client = client;
20
30
  this.options = options;
31
+ this.previousGlobalFilters = previousGlobalFilters;
21
32
  }
22
33
  catch(exception, host) {
23
- try {
24
- this.client.captureException(exception, (0, request_context_js_1.getNestRequestContext)(host, exception, this.options));
34
+ (0, capture_js_1.captureWotchiNestException)(this.client, exception, host, this.options);
35
+ const existingFilter = this.previousGlobalFilters.find((filter) => matchesException(filter, exception));
36
+ if (existingFilter !== undefined) {
37
+ existingFilter.catch(exception, host);
38
+ return;
39
+ }
40
+ this.respondWithoutLoggingRawUnknownException(exception, host);
41
+ }
42
+ respondWithoutLoggingRawUnknownException(exception, host) {
43
+ if (this.applicationRef === undefined) {
44
+ this.respondThroughExpressAdapter(exception, host);
45
+ return;
46
+ }
47
+ if (exception instanceof common_1.HttpException) {
48
+ super.catch(exception, host);
49
+ return;
50
+ }
51
+ const response = host.getArgByIndex(1);
52
+ if (this.applicationRef.isHeadersSent(response)) {
53
+ this.applicationRef.end(response);
54
+ return;
55
+ }
56
+ if (this.isHttpError(exception)) {
57
+ this.applicationRef.reply(response, { statusCode: exception.statusCode, message: exception.message }, exception.statusCode);
58
+ return;
59
+ }
60
+ this.applicationRef.reply(response, { statusCode: 500, message: "Internal server error" }, 500);
61
+ }
62
+ respondThroughExpressAdapter(exception, host) {
63
+ const response = host.switchToHttp().getResponse();
64
+ if (response.headersSent) {
65
+ response.end();
66
+ return;
67
+ }
68
+ if (exception instanceof common_1.HttpException) {
69
+ const statusCode = exception.getStatus();
70
+ const exceptionResponse = exception.getResponse();
71
+ response
72
+ .status(statusCode)
73
+ .json(typeof exceptionResponse === "object" && exceptionResponse !== null
74
+ ? exceptionResponse
75
+ : { statusCode, message: exceptionResponse });
76
+ return;
25
77
  }
26
- catch {
27
- // A capture failure must never change NestJS exception handling.
78
+ if (this.isHttpError(exception)) {
79
+ response.status(exception.statusCode).json({
80
+ statusCode: exception.statusCode,
81
+ message: exception.message,
82
+ });
83
+ return;
28
84
  }
29
- super.catch(exception, host);
85
+ response.status(500).json({ statusCode: 500, message: "Internal server error" });
30
86
  }
31
87
  };
32
88
  exports.WotchiNestExceptionFilter = WotchiNestExceptionFilter;
33
89
  exports.WotchiNestExceptionFilter = WotchiNestExceptionFilter = __decorate([
34
90
  (0, common_1.Catch)(),
35
- __metadata("design:paramtypes", [Object, Object, Object])
91
+ __metadata("design:paramtypes", [Object, Object, Object, Array])
36
92
  ], WotchiNestExceptionFilter);
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withWotchiNestFilter = withWotchiNestFilter;
4
+ const capture_js_1 = require("./capture.js");
5
+ function withWotchiNestFilter(client, filter, options) {
6
+ const wrapped = Object.create(filter);
7
+ Object.defineProperty(wrapped, "catch", {
8
+ configurable: false,
9
+ enumerable: false,
10
+ value: (exception, host) => {
11
+ (0, capture_js_1.captureWotchiNestException)(client, exception, host, options);
12
+ filter.catch(exception, host);
13
+ },
14
+ writable: false,
15
+ });
16
+ return wrapped;
17
+ }
@@ -1,10 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.registerWotchiNestStatusObserver = exports.registerWotchiNest = exports.telegramNotifier = exports.createWotchi = exports.consoleNotifier = void 0;
3
+ exports.registerWotchiNestStatusObserver = exports.registerWotchiNest = exports.WOTCHI_CLIENT = exports.WotchiModule = exports.withWotchiNestFilter = exports.webhookNotifier = exports.telegramNotifier = exports.createWotchi = exports.consoleNotifier = void 0;
4
4
  var index_js_1 = require("../../index.js");
5
5
  Object.defineProperty(exports, "consoleNotifier", { enumerable: true, get: function () { return index_js_1.consoleNotifier; } });
6
6
  Object.defineProperty(exports, "createWotchi", { enumerable: true, get: function () { return index_js_1.createWotchi; } });
7
7
  Object.defineProperty(exports, "telegramNotifier", { enumerable: true, get: function () { return index_js_1.telegramNotifier; } });
8
+ Object.defineProperty(exports, "webhookNotifier", { enumerable: true, get: function () { return index_js_1.webhookNotifier; } });
9
+ var filter_wrapper_js_1 = require("./filter-wrapper.js");
10
+ Object.defineProperty(exports, "withWotchiNestFilter", { enumerable: true, get: function () { return filter_wrapper_js_1.withWotchiNestFilter; } });
11
+ var module_js_1 = require("./module.js");
12
+ Object.defineProperty(exports, "WotchiModule", { enumerable: true, get: function () { return module_js_1.WotchiModule; } });
13
+ Object.defineProperty(exports, "WOTCHI_CLIENT", { enumerable: true, get: function () { return module_js_1.WOTCHI_CLIENT; } });
8
14
  var register_js_1 = require("./register.js");
9
15
  Object.defineProperty(exports, "registerWotchiNest", { enumerable: true, get: function () { return register_js_1.registerWotchiNest; } });
10
16
  Object.defineProperty(exports, "registerWotchiNestStatusObserver", { enumerable: true, get: function () { return register_js_1.registerWotchiNestStatusObserver; } });
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ 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;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var WotchiModule_1;
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.WotchiModule = exports.WOTCHI_CLIENT = void 0;
11
+ const common_1 = require("@nestjs/common");
12
+ const core_1 = require("@nestjs/core");
13
+ const client_js_1 = require("../../core/client.js");
14
+ const exception_filter_js_1 = require("./exception-filter.js");
15
+ exports.WOTCHI_CLIENT = Symbol("@futurewindai/wotchi/client");
16
+ let WotchiModule = WotchiModule_1 = class WotchiModule {
17
+ static forRoot(config, options) {
18
+ const registerGlobalFilter = options?.registerGlobalFilter ?? true;
19
+ const providers = [
20
+ {
21
+ provide: exports.WOTCHI_CLIENT,
22
+ useFactory: () => (0, client_js_1.createWotchi)(config),
23
+ },
24
+ ];
25
+ if (registerGlobalFilter) {
26
+ providers.push({
27
+ provide: core_1.APP_FILTER,
28
+ useFactory: (client) => new exception_filter_js_1.WotchiNestExceptionFilter(client),
29
+ inject: [exports.WOTCHI_CLIENT],
30
+ });
31
+ }
32
+ return {
33
+ module: WotchiModule_1,
34
+ global: true,
35
+ providers,
36
+ exports: [exports.WOTCHI_CLIENT],
37
+ };
38
+ }
39
+ };
40
+ exports.WotchiModule = WotchiModule;
41
+ exports.WotchiModule = WotchiModule = WotchiModule_1 = __decorate([
42
+ (0, common_1.Global)(),
43
+ (0, common_1.Module)({})
44
+ ], WotchiModule);