@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
@@ -4,17 +4,34 @@ exports.WotchiConfigurationError = void 0;
4
4
  exports.validateConfig = validateConfig;
5
5
  const errors_js_1 = require("./errors.js");
6
6
  Object.defineProperty(exports, "WotchiConfigurationError", { enumerable: true, get: function () { return errors_js_1.WotchiConfigurationError; } });
7
+ const limits_js_1 = require("./limits.js");
7
8
  const MAX_REDACT_KEYS = 100;
9
+ const MAX_RULES = 50;
10
+ const MAX_RULE_STRING_LENGTH = 200;
11
+ const MAX_LINK_TEMPLATE_LENGTH = 1_000;
12
+ const ALLOWED_LINK_PLACEHOLDERS = new Set([
13
+ "service",
14
+ "environment",
15
+ "release",
16
+ "instance",
17
+ "fingerprint",
18
+ "requestId",
19
+ "correlationId",
20
+ "traceId",
21
+ "spanId",
22
+ "route",
23
+ "statusCode",
24
+ ]);
8
25
  const isRecord = (value) => typeof value === "object" && value !== null;
9
26
  const fail = (field) => {
10
27
  throw new errors_js_1.WotchiConfigurationError(`${field} is invalid`);
11
28
  };
12
- const readPositiveInteger = (parent, field, defaultValue) => {
29
+ const readPositiveInteger = (parent, field, defaultValue, maximum) => {
13
30
  const value = parent?.[field];
14
31
  if (value === undefined) {
15
32
  return defaultValue;
16
33
  }
17
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
34
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0 || value > maximum) {
18
35
  return fail(field);
19
36
  }
20
37
  return value;
@@ -28,6 +45,120 @@ const readOptionalRecord = (value, field) => {
28
45
  }
29
46
  return value;
30
47
  };
48
+ const normalizeRuleString = (value, field) => {
49
+ if (value === undefined) {
50
+ return undefined;
51
+ }
52
+ if (typeof value !== "string" || value.trim().length === 0) {
53
+ return fail(field);
54
+ }
55
+ return value.trim().slice(0, MAX_RULE_STRING_LENGTH);
56
+ };
57
+ const normalizeSeverity = (value, field) => {
58
+ if (value === undefined) {
59
+ return undefined;
60
+ }
61
+ if (value !== "low" && value !== "medium" && value !== "high" && value !== "critical") {
62
+ return fail(field);
63
+ }
64
+ return value;
65
+ };
66
+ const freezeHook = (hook) => {
67
+ try {
68
+ return Object.freeze(hook);
69
+ }
70
+ catch {
71
+ return hook;
72
+ }
73
+ };
74
+ const normalizeLinks = (value) => {
75
+ if (value === undefined) {
76
+ return undefined;
77
+ }
78
+ if (!isRecord(value)) {
79
+ return fail("links");
80
+ }
81
+ const normalized = {};
82
+ for (const key of ["log", "trace"]) {
83
+ const raw = value[key];
84
+ if (raw === undefined) {
85
+ continue;
86
+ }
87
+ if (typeof raw !== "string" || raw.length === 0 || raw.length > MAX_LINK_TEMPLATE_LENGTH) {
88
+ return fail(`links.${key}`);
89
+ }
90
+ let parsed;
91
+ try {
92
+ parsed = new URL(raw.replace(/\{\{[^{}]+\}\}/g, "placeholder"));
93
+ }
94
+ catch {
95
+ return fail(`links.${key}`);
96
+ }
97
+ if (parsed.protocol !== "https:" ||
98
+ parsed.username !== "" ||
99
+ parsed.password !== "" ||
100
+ parsed.hash !== "") {
101
+ return fail(`links.${key}`);
102
+ }
103
+ const placeholders = raw.match(/\{\{([^{}]+)\}\}/g) ?? [];
104
+ if (/[{}]/.test(raw.replace(/\{\{[^{}]+\}\}/g, ""))) {
105
+ return fail(`links.${key}`);
106
+ }
107
+ for (const placeholder of placeholders) {
108
+ const name = placeholder.slice(2, -2);
109
+ if (!ALLOWED_LINK_PLACEHOLDERS.has(name)) {
110
+ return fail(`links.${key}`);
111
+ }
112
+ }
113
+ normalized[key] = raw;
114
+ }
115
+ return Object.freeze(normalized);
116
+ };
117
+ const normalizeRules = (value) => {
118
+ if (value === undefined) {
119
+ return Object.freeze([]);
120
+ }
121
+ if (!Array.isArray(value) || value.length > MAX_RULES) {
122
+ return fail("rules");
123
+ }
124
+ const rules = value.map((rawRule, index) => {
125
+ if (!isRecord(rawRule)) {
126
+ return fail(`rules[${index}]`);
127
+ }
128
+ const alertThreshold = rawRule.alertThreshold;
129
+ if (alertThreshold !== undefined &&
130
+ (typeof alertThreshold !== "number" ||
131
+ !Number.isSafeInteger(alertThreshold) ||
132
+ alertThreshold <= 0 ||
133
+ alertThreshold > 1_000_000)) {
134
+ return fail(`rules[${index}].alertThreshold`);
135
+ }
136
+ if (rawRule.ignore !== undefined && typeof rawRule.ignore !== "boolean") {
137
+ return fail(`rules[${index}].ignore`);
138
+ }
139
+ const environment = normalizeRuleString(rawRule.environment, `rules[${index}].environment`);
140
+ const route = normalizeRuleString(rawRule.route, `rules[${index}].route`);
141
+ const severity = normalizeSeverity(rawRule.severity, `rules[${index}].severity`);
142
+ const rule = {};
143
+ if (environment !== undefined) {
144
+ rule.environment = environment;
145
+ }
146
+ if (route !== undefined) {
147
+ rule.route = route;
148
+ }
149
+ if (rawRule.ignore !== undefined) {
150
+ rule.ignore = rawRule.ignore;
151
+ }
152
+ if (alertThreshold !== undefined) {
153
+ rule.alertThreshold = alertThreshold;
154
+ }
155
+ if (severity !== undefined) {
156
+ rule.severity = severity;
157
+ }
158
+ return Object.freeze(rule);
159
+ });
160
+ return Object.freeze(rules);
161
+ };
31
162
  const normalizeRedactKeys = (privacy) => {
32
163
  const value = privacy?.redactKeys;
33
164
  if (value === undefined) {
@@ -50,11 +181,19 @@ const normalizeNotifiers = (value) => {
50
181
  }
51
182
  for (let index = 0; index < value.length; index += 1) {
52
183
  const notifier = value[index];
53
- if (!isRecord(notifier) ||
54
- typeof notifier.name !== "string" ||
55
- notifier.name.trim().length === 0 ||
56
- typeof notifier.send !== "function") {
57
- return fail(`notifiers[${index}]`);
184
+ const valid = (() => {
185
+ try {
186
+ return (isRecord(notifier) &&
187
+ typeof notifier.name === "string" &&
188
+ notifier.name.trim().length > 0 &&
189
+ typeof notifier.send === "function");
190
+ }
191
+ catch {
192
+ return false;
193
+ }
194
+ })();
195
+ if (!valid) {
196
+ return fail("notifiers[" + index + "]");
58
197
  }
59
198
  }
60
199
  return Object.freeze([...value]);
@@ -75,12 +214,28 @@ function validateConfig(config) {
75
214
  (typeof config.release !== "string" || config.release.trim().length === 0)) {
76
215
  return fail("release");
77
216
  }
217
+ if (config.instance !== undefined &&
218
+ (typeof config.instance !== "string" || config.instance.trim().length === 0)) {
219
+ return fail("instance");
220
+ }
78
221
  if (config.enabled !== undefined && typeof config.enabled !== "boolean") {
79
222
  return fail("enabled");
80
223
  }
224
+ if (config.filter !== undefined && typeof config.filter !== "function") {
225
+ return fail("filter");
226
+ }
227
+ if (config.fingerprint !== undefined && typeof config.fingerprint !== "function") {
228
+ return fail("fingerprint");
229
+ }
230
+ if (config.beforeSend !== undefined && typeof config.beforeSend !== "function") {
231
+ return fail("beforeSend");
232
+ }
81
233
  const grouping = readOptionalRecord(config.grouping, "grouping");
82
234
  const queue = readOptionalRecord(config.queue, "queue");
235
+ const notifierCircuitBreaker = readOptionalRecord(queue?.notifierCircuitBreaker, "queue.notifierCircuitBreaker");
83
236
  const privacy = readOptionalRecord(config.privacy, "privacy");
237
+ const overload = readOptionalRecord(config.overload, "overload");
238
+ const links = normalizeLinks(config.links);
84
239
  const concurrency = queue?.concurrency;
85
240
  if (concurrency !== undefined && concurrency !== 1) {
86
241
  return fail("queue.concurrency");
@@ -88,25 +243,45 @@ function validateConfig(config) {
88
243
  const normalized = {
89
244
  service: service.trim(),
90
245
  environment: environment.trim(),
91
- ...(config.release === undefined ? {} : { release: config.release.trim() }),
246
+ ...(config.instance === undefined ? {} : { instance: config.instance.trim().slice(0, 200) }),
247
+ ...(config.release === undefined ? {} : { release: config.release.trim().slice(0, 200) }),
92
248
  enabled: config.enabled ?? true,
249
+ ...(config.filter === undefined ? {} : { filter: freezeHook(config.filter) }),
250
+ ...(config.fingerprint === undefined ? {} : { fingerprint: freezeHook(config.fingerprint) }),
251
+ ...(config.beforeSend === undefined ? {} : { beforeSend: freezeHook(config.beforeSend) }),
252
+ ...(links === undefined ? {} : { links }),
253
+ rules: normalizeRules(config.rules),
93
254
  grouping: Object.freeze({
94
- windowMs: readPositiveInteger(grouping, "windowMs", 60_000),
95
- alertThreshold: readPositiveInteger(grouping, "alertThreshold", 3),
96
- cooldownMs: readPositiveInteger(grouping, "cooldownMs", 900_000),
97
- maxGroups: readPositiveInteger(grouping, "maxGroups", 200),
98
- maxEventsPerWindow: readPositiveInteger(grouping, "maxEventsPerWindow", 100),
255
+ windowMs: readPositiveInteger(grouping, "windowMs", 60_000, limits_js_1.MAX_WINDOW_MS),
256
+ alertThreshold: readPositiveInteger(grouping, "alertThreshold", 3, limits_js_1.MAX_ALERT_THRESHOLD),
257
+ cooldownMs: readPositiveInteger(grouping, "cooldownMs", 900_000, limits_js_1.MAX_COOLDOWN_MS),
258
+ maxGroups: readPositiveInteger(grouping, "maxGroups", 200, limits_js_1.MAX_GROUPS),
259
+ maxEventsPerWindow: readPositiveInteger(grouping, "maxEventsPerWindow", 100, limits_js_1.MAX_EVENTS_PER_WINDOW),
99
260
  }),
100
261
  queue: Object.freeze({
101
- maxPendingAlerts: readPositiveInteger(queue, "maxPendingAlerts", 100),
262
+ maxPendingAlerts: readPositiveInteger(queue, "maxPendingAlerts", 100, limits_js_1.MAX_PENDING_ALERTS),
102
263
  concurrency: 1,
264
+ notifierTimeoutMs: readPositiveInteger(queue, "notifierTimeoutMs", 5_000, limits_js_1.MAX_NOTIFIER_TIMEOUT_MS),
265
+ notifierCircuitBreaker: Object.freeze({
266
+ failureThreshold: readPositiveInteger(notifierCircuitBreaker, "failureThreshold", 3, limits_js_1.MAX_CIRCUIT_BREAKER_FAILURES),
267
+ cooldownMs: readPositiveInteger(notifierCircuitBreaker, "cooldownMs", 30_000, limits_js_1.MAX_CIRCUIT_BREAKER_COOLDOWN_MS),
268
+ }),
103
269
  }),
270
+ ...(overload === undefined
271
+ ? {}
272
+ : {
273
+ overload: Object.freeze({
274
+ maxEventsPerSecond: readPositiveInteger(overload, "maxEventsPerSecond", 1_000, limits_js_1.MAX_CAPTURE_RATE),
275
+ burst: readPositiveInteger(overload, "burst", readPositiveInteger(overload, "maxEventsPerSecond", 1_000, limits_js_1.MAX_CAPTURE_RATE), limits_js_1.MAX_CAPTURE_RATE),
276
+ alertCooldownMs: readPositiveInteger(overload, "alertCooldownMs", 60_000, limits_js_1.MAX_COOLDOWN_MS),
277
+ }),
278
+ }),
104
279
  privacy: Object.freeze({
105
280
  redactKeys: normalizeRedactKeys(privacy),
106
- maxDepth: readPositiveInteger(privacy, "maxDepth", 5),
107
- maxKeys: readPositiveInteger(privacy, "maxKeys", 100),
108
- maxStringLength: readPositiveInteger(privacy, "maxStringLength", 500),
109
- maxStackLength: readPositiveInteger(privacy, "maxStackLength", 4_000),
281
+ maxDepth: readPositiveInteger(privacy, "maxDepth", 5, limits_js_1.MAX_NORMALIZATION_DEPTH),
282
+ maxKeys: readPositiveInteger(privacy, "maxKeys", 100, limits_js_1.MAX_NORMALIZATION_KEYS),
283
+ maxStringLength: readPositiveInteger(privacy, "maxStringLength", 500, limits_js_1.MAX_NORMALIZATION_STRING_LENGTH),
284
+ maxStackLength: readPositiveInteger(privacy, "maxStackLength", 4_000, limits_js_1.MAX_NORMALIZATION_STACK_LENGTH),
110
285
  }),
111
286
  notifiers: normalizeNotifiers(config.notifiers),
112
287
  };
@@ -3,12 +3,27 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createDiagnosticsState = createDiagnosticsState;
4
4
  exports.snapshotDiagnostics = snapshotDiagnostics;
5
5
  function createDiagnosticsState() {
6
- return { capturedEvents: 0, captureFailures: 0 };
6
+ return {
7
+ capturedEvents: 0,
8
+ captureFailures: 0,
9
+ fingerprintCallbackFailures: 0,
10
+ filterFailures: 0,
11
+ beforeSendFailures: 0,
12
+ eventsSuppressed: 0,
13
+ eventsDroppedOverload: 0,
14
+ capturesAfterShutdown: 0,
15
+ };
7
16
  }
8
17
  function snapshotDiagnostics(state, values) {
9
18
  return Object.freeze({
10
19
  capturedEvents: state.capturedEvents,
11
20
  captureFailures: state.captureFailures,
21
+ fingerprintCallbackFailures: state.fingerprintCallbackFailures,
22
+ filterFailures: state.filterFailures,
23
+ beforeSendFailures: state.beforeSendFailures,
24
+ eventsSuppressed: state.eventsSuppressed,
25
+ eventsDroppedOverload: state.eventsDroppedOverload,
26
+ capturesAfterShutdown: state.capturesAfterShutdown,
12
27
  ...values,
13
28
  });
14
29
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createGroupStore = createGroupStore;
4
4
  const normalize_js_1 = require("./normalize.js");
5
+ const limits_js_1 = require("./limits.js");
5
6
  const rolling_window_js_1 = require("./rolling-window.js");
6
7
  const cloneSafeEvent = (event) => {
7
8
  const context = event.context === undefined ? undefined : (0, normalize_js_1.normalizeUnknown)(event.context);
@@ -10,16 +11,29 @@ const cloneSafeEvent = (event) => {
10
11
  timestamp: event.timestamp,
11
12
  service: event.service,
12
13
  environment: event.environment,
14
+ ...(event.instance === undefined ? {} : { instance: event.instance }),
13
15
  ...(event.release === undefined ? {} : { release: event.release }),
16
+ ...(event.correlationId === undefined ? {} : { correlationId: event.correlationId }),
17
+ ...(event.operation === undefined ? {} : { operation: event.operation }),
18
+ ...(event.job === undefined ? {} : { job: event.job }),
19
+ ...(event.tags === undefined ? {} : { tags: { ...event.tags } }),
14
20
  error: { ...event.error },
15
21
  ...(event.request === undefined ? {} : { request: { ...event.request } }),
22
+ ...(event.trace === undefined ? {} : { trace: { ...event.trace } }),
16
23
  ...(context === undefined ? {} : { context: context }),
17
24
  };
18
25
  };
19
26
  function createGroupStore(options) {
20
- if (!Number.isSafeInteger(options.maxGroups) || options.maxGroups <= 0) {
27
+ if (!Number.isSafeInteger(options.maxGroups) ||
28
+ options.maxGroups <= 0 ||
29
+ options.maxGroups > limits_js_1.MAX_GROUPS) {
21
30
  throw new RangeError("maxGroups must be a positive integer");
22
31
  }
32
+ if (!Number.isSafeInteger(options.maxEventsPerWindow) ||
33
+ options.maxEventsPerWindow <= 0 ||
34
+ options.maxEventsPerWindow > limits_js_1.MAX_EVENTS_PER_WINDOW) {
35
+ throw new RangeError("maxEventsPerWindow must be a positive integer");
36
+ }
23
37
  const now = options.now ?? Date.now;
24
38
  const groups = new Map();
25
39
  let evicted = 0;
@@ -36,14 +50,9 @@ function createGroupStore(options) {
36
50
  severity: group.severity,
37
51
  });
38
52
  const evictLeastRecent = () => {
39
- let candidate;
40
- for (const group of groups.values()) {
41
- if (candidate === undefined || group.lastSeenMs < candidate.lastSeenMs) {
42
- candidate = group;
43
- }
44
- }
45
- if (candidate !== undefined) {
46
- groups.delete(candidate.fingerprint);
53
+ const oldestFingerprint = groups.keys().next().value;
54
+ if (typeof oldestFingerprint === "string") {
55
+ groups.delete(oldestFingerprint);
47
56
  evicted += 1;
48
57
  }
49
58
  };
@@ -69,6 +78,10 @@ function createGroupStore(options) {
69
78
  };
70
79
  groups.set(fingerprint, group);
71
80
  }
81
+ else {
82
+ groups.delete(fingerprint);
83
+ groups.set(fingerprint, group);
84
+ }
72
85
  group.lastSeenMs = timestamp;
73
86
  group.totalCount += 1;
74
87
  group.severity = severity;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildIncidentAlert = buildIncidentAlert;
4
+ const stack_frame_js_1 = require("./stack-frame.js");
4
5
  const titleCase = (value) => `${value.slice(0, 1).toUpperCase()}${value.slice(1)}`;
5
6
  const suggestedActions = (group) => {
6
7
  const text = `${group.sample.error.name} ${group.sample.error.message}`.toLowerCase();
@@ -30,6 +31,11 @@ const suggestedActions = (group) => {
30
31
  function buildIncidentAlert(group, decision) {
31
32
  const message = group.sample.error.message;
32
33
  const errorName = group.sample.error.name;
34
+ const applicationFrame = (0, stack_frame_js_1.selectApplicationFrame)(group.sample.error.stack);
35
+ const error = {
36
+ ...group.sample.error,
37
+ ...(applicationFrame === undefined ? {} : { applicationFrame }),
38
+ };
33
39
  return {
34
40
  id: `wotchi-${group.fingerprint}-${group.lastSeenAt}`,
35
41
  fingerprint: group.fingerprint,
@@ -42,5 +48,17 @@ function buildIncidentAlert(group, decision) {
42
48
  occurrences: group.windowCount,
43
49
  service: group.sample.service,
44
50
  environment: group.sample.environment,
51
+ ...(group.sample.instance === undefined ? {} : { instance: group.sample.instance }),
52
+ ...(group.sample.release === undefined ? {} : { release: group.sample.release }),
53
+ ...(group.sample.correlationId === undefined
54
+ ? {}
55
+ : { correlationId: group.sample.correlationId }),
56
+ ...(group.sample.operation === undefined ? {} : { operation: group.sample.operation }),
57
+ ...(group.sample.job === undefined ? {} : { job: group.sample.job }),
58
+ ...(group.sample.tags === undefined ? {} : { tags: group.sample.tags }),
59
+ error,
60
+ ...(group.sample.request === undefined ? {} : { request: group.sample.request }),
61
+ ...(group.sample.trace === undefined ? {} : { trace: group.sample.trace }),
62
+ ...(group.sample.context === undefined ? {} : { context: group.sample.context }),
45
63
  };
46
64
  }
@@ -13,13 +13,8 @@ function evaluateIncidentPolicy(input) {
13
13
  const eventKind = input.eventKind ?? "error";
14
14
  const isCritical = eventKind === "process-monitor";
15
15
  const isHigh = input.group.windowCount >= 20 || hasUnavailableResponse(input.group);
16
- const severity = isCritical
17
- ? "critical"
18
- : isHigh
19
- ? "high"
20
- : eventKind === "manual"
21
- ? (input.manualSeverity ?? "low")
22
- : "medium";
16
+ const severity = input.manualSeverity ??
17
+ (isCritical ? "critical" : isHigh ? "high" : eventKind === "manual" ? "low" : "medium");
23
18
  const eligible = isCritical ||
24
19
  isHigh ||
25
20
  eventKind === "manual" ||
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAX_NORMALIZATION_STACK_LENGTH = exports.MAX_NORMALIZATION_STRING_LENGTH = exports.MAX_NORMALIZATION_KEYS = exports.MAX_NORMALIZATION_DEPTH = exports.MAX_CIRCUIT_BREAKER_FAILURES = exports.MAX_CIRCUIT_BREAKER_COOLDOWN_MS = exports.MAX_NOTIFIER_TIMEOUT_MS = exports.MAX_CAPTURE_RATE = exports.MAX_COOLDOWN_MS = exports.MAX_ALERT_THRESHOLD = exports.MAX_WINDOW_MS = exports.MAX_PENDING_ALERTS = exports.MAX_EVENTS_PER_WINDOW = exports.MAX_GROUPS = void 0;
4
+ exports.MAX_GROUPS = 10_000;
5
+ exports.MAX_EVENTS_PER_WINDOW = 10_000;
6
+ exports.MAX_PENDING_ALERTS = 10_000;
7
+ exports.MAX_WINDOW_MS = 7 * 24 * 60 * 60 * 1_000;
8
+ exports.MAX_ALERT_THRESHOLD = 1_000_000;
9
+ exports.MAX_COOLDOWN_MS = 30 * 24 * 60 * 60 * 1_000;
10
+ exports.MAX_CAPTURE_RATE = 1_000_000;
11
+ exports.MAX_NOTIFIER_TIMEOUT_MS = 60_000;
12
+ exports.MAX_CIRCUIT_BREAKER_COOLDOWN_MS = 24 * 60 * 60 * 1_000;
13
+ exports.MAX_CIRCUIT_BREAKER_FAILURES = 100;
14
+ exports.MAX_NORMALIZATION_DEPTH = 20;
15
+ exports.MAX_NORMALIZATION_KEYS = 10_000;
16
+ exports.MAX_NORMALIZATION_STRING_LENGTH = 32_768;
17
+ exports.MAX_NORMALIZATION_STACK_LENGTH = 32_768;
@@ -6,18 +6,19 @@ const UNREADABLE_VALUE = "[unreadable value]";
6
6
  const MAX_DEPTH_VALUE = "[MaxDepth]";
7
7
  const CIRCULAR_VALUE = "[Circular]";
8
8
  const MAX_KEYS_VALUE = "[MaxKeys]";
9
+ const UNSAFE_OBJECT_KEYS = new Set(["__proto__", "prototype", "constructor"]);
9
10
  const defaultLimits = {
10
11
  maxDepth: 5,
11
12
  maxKeys: 100,
12
13
  maxStringLength: 500,
13
14
  maxStackLength: 4_000,
14
15
  };
15
- const boundedLimit = (value, fallback) => Number.isSafeInteger(value) && value > 0 ? value : fallback;
16
+ const boundedLimit = (value, fallback, maximum) => Number.isSafeInteger(value) && value > 0 && value <= maximum ? value : fallback;
16
17
  const normalizeLimits = (limits) => ({
17
- maxDepth: boundedLimit(limits?.maxDepth ?? defaultLimits.maxDepth, defaultLimits.maxDepth),
18
- maxKeys: boundedLimit(limits?.maxKeys ?? defaultLimits.maxKeys, defaultLimits.maxKeys),
19
- maxStringLength: boundedLimit(limits?.maxStringLength ?? defaultLimits.maxStringLength, defaultLimits.maxStringLength),
20
- maxStackLength: boundedLimit(limits?.maxStackLength ?? defaultLimits.maxStackLength, defaultLimits.maxStackLength),
18
+ maxDepth: boundedLimit(limits?.maxDepth ?? defaultLimits.maxDepth, defaultLimits.maxDepth, limits_js_1.MAX_NORMALIZATION_DEPTH),
19
+ maxKeys: boundedLimit(limits?.maxKeys ?? defaultLimits.maxKeys, defaultLimits.maxKeys, limits_js_1.MAX_NORMALIZATION_KEYS),
20
+ maxStringLength: boundedLimit(limits?.maxStringLength ?? defaultLimits.maxStringLength, defaultLimits.maxStringLength, limits_js_1.MAX_NORMALIZATION_STRING_LENGTH),
21
+ maxStackLength: boundedLimit(limits?.maxStackLength ?? defaultLimits.maxStackLength, defaultLimits.maxStackLength, limits_js_1.MAX_NORMALIZATION_STACK_LENGTH),
21
22
  });
22
23
  const truncate = (value, limit) => value.slice(0, limit);
23
24
  const readProperty = (value, key) => {
@@ -89,12 +90,20 @@ const normalizeValue = (value, depth, state, limits) => {
89
90
  }
90
91
  const result = {};
91
92
  for (const key of keys) {
93
+ if (UNSAFE_OBJECT_KEYS.has(key)) {
94
+ continue;
95
+ }
92
96
  if (state.keysVisited >= limits.maxKeys - 1) {
93
97
  result["[truncated]"] = MAX_KEYS_VALUE;
94
98
  break;
95
99
  }
96
100
  state.keysVisited += 1;
97
- result[truncate(key, limits.maxStringLength)] = normalizeValue(readProperty(value, key), depth + 1, state, limits);
101
+ const child = readProperty(value, key);
102
+ const normalizedKey = key.toLowerCase().replace(/[^a-z0-9]/g, "");
103
+ result[truncate(key, limits.maxStringLength)] =
104
+ normalizedKey === "stack" && typeof child === "string"
105
+ ? truncate(child, limits.maxStackLength)
106
+ : normalizeValue(child, depth + 1, state, limits);
98
107
  }
99
108
  return result;
100
109
  };
@@ -136,3 +145,4 @@ function normalizeError(error, limits) {
136
145
  const message = typeof normalized === "string" ? normalized : UNREADABLE_VALUE;
137
146
  return { name: "UnknownError", message };
138
147
  }
148
+ const limits_js_1 = require("./limits.js");
@@ -1,14 +1,37 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createNotificationQueue = createNotificationQueue;
4
+ const limits_js_1 = require("./limits.js");
4
5
  const DEFAULT_FLUSH_TIMEOUT_MS = 5_000;
6
+ const DEFAULT_NOTIFIER_TIMEOUT_MS = 5_000;
7
+ const DEFAULT_CIRCUIT_FAILURE_THRESHOLD = 3;
8
+ const DEFAULT_CIRCUIT_COOLDOWN_MS = 30_000;
5
9
  function createNotificationQueue(options) {
6
- if (!Number.isSafeInteger(options.maxPendingAlerts) || options.maxPendingAlerts <= 0) {
10
+ if (!Number.isSafeInteger(options.maxPendingAlerts) ||
11
+ options.maxPendingAlerts <= 0 ||
12
+ options.maxPendingAlerts > limits_js_1.MAX_PENDING_ALERTS) {
7
13
  throw new RangeError("maxPendingAlerts must be a positive integer");
8
14
  }
9
15
  if (options.concurrency !== 1) {
10
16
  throw new RangeError("concurrency must be 1");
11
17
  }
18
+ if (options.notifierTimeoutMs !== undefined &&
19
+ (!Number.isSafeInteger(options.notifierTimeoutMs) ||
20
+ options.notifierTimeoutMs <= 0 ||
21
+ options.notifierTimeoutMs > limits_js_1.MAX_NOTIFIER_TIMEOUT_MS)) {
22
+ throw new RangeError("notifierTimeoutMs must be a positive integer");
23
+ }
24
+ if (options.notifierCircuitBreaker !== undefined) {
25
+ const { failureThreshold, cooldownMs } = options.notifierCircuitBreaker;
26
+ if (!Number.isSafeInteger(failureThreshold) ||
27
+ failureThreshold <= 0 ||
28
+ failureThreshold > limits_js_1.MAX_CIRCUIT_BREAKER_FAILURES ||
29
+ !Number.isSafeInteger(cooldownMs) ||
30
+ cooldownMs <= 0 ||
31
+ cooldownMs > limits_js_1.MAX_CIRCUIT_BREAKER_COOLDOWN_MS) {
32
+ throw new RangeError("notifierCircuitBreaker limits are invalid");
33
+ }
34
+ }
12
35
  const pendingJobs = [];
13
36
  let drainPromise;
14
37
  let resolveDrain;
@@ -17,6 +40,15 @@ function createNotificationQueue(options) {
17
40
  let dropped = 0;
18
41
  let sent = 0;
19
42
  let failures = 0;
43
+ let timeouts = 0;
44
+ let circuitOpenSkips = 0;
45
+ let closed = false;
46
+ const notifierStates = new Map();
47
+ const notifierTimeoutMs = options.notifierTimeoutMs ?? DEFAULT_NOTIFIER_TIMEOUT_MS;
48
+ const circuit = options.notifierCircuitBreaker ?? {
49
+ failureThreshold: DEFAULT_CIRCUIT_FAILURE_THRESHOLD,
50
+ cooldownMs: DEFAULT_CIRCUIT_COOLDOWN_MS,
51
+ };
20
52
  const notifyFailure = (error, notifier) => {
21
53
  failures += 1;
22
54
  try {
@@ -35,14 +67,56 @@ function createNotificationQueue(options) {
35
67
  resolveDrain = undefined;
36
68
  };
37
69
  const processJob = async (job) => {
38
- for (const notifier of job.notifiers) {
70
+ const results = await Promise.all(job.notifiers.map(async (notifier) => {
71
+ const state = notifierStates.get(notifier) ?? { failures: 0, openUntil: 0 };
72
+ const timestamp = Date.now();
73
+ if (state.openUntil > timestamp) {
74
+ circuitOpenSkips += 1;
75
+ notifyFailure(new Error(`Notifier circuit is open: ${notifier.name}`), notifier);
76
+ return { sent: 0, failed: 1 };
77
+ }
78
+ if (state.openUntil !== 0) {
79
+ state.openUntil = 0;
80
+ state.failures = 0;
81
+ }
82
+ let timer;
39
83
  try {
40
- await notifier.send(job.alert);
84
+ const timeout = new Promise((_, reject) => {
85
+ timer = setTimeout(() => {
86
+ timeouts += 1;
87
+ reject(new Error(`Notifier timed out after ${notifierTimeoutMs}ms: ${notifier.name}`));
88
+ }, notifierTimeoutMs);
89
+ timer.unref?.();
90
+ });
91
+ await Promise.race([notifier.send(job.alert), timeout]);
92
+ if (timer !== undefined) {
93
+ clearTimeout(timer);
94
+ }
95
+ state.failures = 0;
96
+ notifierStates.set(notifier, state);
41
97
  sent += 1;
98
+ return { sent: 1, failed: 0 };
42
99
  }
43
100
  catch (error) {
101
+ if (timer !== undefined) {
102
+ clearTimeout(timer);
103
+ }
104
+ state.failures += 1;
105
+ if (state.failures >= circuit.failureThreshold) {
106
+ state.openUntil = Date.now() + circuit.cooldownMs;
107
+ }
108
+ notifierStates.set(notifier, state);
44
109
  notifyFailure(error, notifier);
110
+ return { sent: 0, failed: 1 };
45
111
  }
112
+ }));
113
+ const jobSent = results.reduce((total, result) => total + result.sent, 0);
114
+ const jobFailures = results.reduce((total, result) => total + result.failed, 0);
115
+ try {
116
+ job.onComplete?.({ notifierFailures: jobFailures, sent: jobSent });
117
+ }
118
+ catch {
119
+ // Job observers cannot be allowed to affect notification processing.
46
120
  }
47
121
  };
48
122
  const pump = () => {
@@ -59,16 +133,24 @@ function createNotificationQueue(options) {
59
133
  });
60
134
  }
61
135
  };
62
- const enqueue = (alert, notifiers) => {
63
- if (pendingJobs.length >= options.maxPendingAlerts) {
136
+ const enqueue = (alert, notifiers, onComplete) => {
137
+ if (closed || pendingJobs.length >= options.maxPendingAlerts) {
64
138
  dropped += 1;
65
139
  return false;
66
140
  }
67
- pendingJobs.push({ alert, notifiers: [...notifiers] });
141
+ pendingJobs.push({
142
+ alert,
143
+ notifiers: [...notifiers],
144
+ ...(onComplete === undefined ? {} : { onComplete }),
145
+ });
68
146
  queued += 1;
69
147
  pump();
70
148
  return true;
71
149
  };
150
+ const close = (timeoutMs = DEFAULT_FLUSH_TIMEOUT_MS) => {
151
+ closed = true;
152
+ return flush(timeoutMs);
153
+ };
72
154
  const flush = (timeoutMs = DEFAULT_FLUSH_TIMEOUT_MS) => {
73
155
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
74
156
  return Promise.reject(new RangeError("timeoutMs must be a positive integer"));
@@ -94,10 +176,14 @@ function createNotificationQueue(options) {
94
176
  return {
95
177
  enqueue,
96
178
  flush,
179
+ close,
180
+ isClosed: () => closed,
97
181
  pending: () => pendingJobs.length,
98
182
  alertsQueued: () => queued,
99
183
  alertsDropped: () => dropped,
100
184
  alertsSent: () => sent,
101
185
  notifierFailures: () => failures,
186
+ notifierTimeouts: () => timeouts,
187
+ notifierCircuitOpenSkips: () => circuitOpenSkips,
102
188
  };
103
189
  }