@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
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createWotchi = createWotchi;
4
4
  const config_js_1 = require("./config.js");
5
+ const admission_js_1 = require("./admission.js");
5
6
  const diagnostics_js_1 = require("./diagnostics.js");
6
7
  const fingerprint_js_1 = require("./fingerprint.js");
7
8
  const group_store_js_1 = require("./group-store.js");
@@ -13,6 +14,34 @@ const redact_js_1 = require("./redact.js");
13
14
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
14
15
  const asString = (value) => typeof value === "string" ? value : undefined;
15
16
  const asStatusCode = (value) => typeof value === "number" && Number.isSafeInteger(value) ? value : undefined;
17
+ const deepFreeze = (value) => {
18
+ if (typeof value !== "object" || value === null || Object.isFrozen(value)) {
19
+ return value;
20
+ }
21
+ Object.freeze(value);
22
+ for (const child of Object.values(value)) {
23
+ deepFreeze(child);
24
+ }
25
+ return value;
26
+ };
27
+ const safeTrace = (value, privacy) => {
28
+ if (value === undefined) {
29
+ return undefined;
30
+ }
31
+ const redacted = (0, redact_js_1.redactValue)(value, privacy);
32
+ if (!isRecord(redacted)) {
33
+ return undefined;
34
+ }
35
+ const traceId = asString(redacted.traceId);
36
+ const spanId = asString(redacted.spanId);
37
+ if (traceId === undefined && spanId === undefined) {
38
+ return undefined;
39
+ }
40
+ return {
41
+ ...(traceId === undefined ? {} : { traceId: traceId.slice(0, 128) }),
42
+ ...(spanId === undefined ? {} : { spanId: spanId.slice(0, 128) }),
43
+ };
44
+ };
16
45
  const safeRecord = (value, privacy) => {
17
46
  if (value === undefined) {
18
47
  return undefined;
@@ -42,20 +71,48 @@ const safeRequest = (request, privacy) => {
42
71
  const method = asString(redacted.method);
43
72
  const route = asString(redacted.route);
44
73
  const requestId = asString(redacted.requestId);
74
+ const correlationId = asString(redacted.correlationId);
45
75
  const statusCode = asStatusCode(redacted.statusCode);
76
+ const trace = safeTrace(redacted.trace, privacy);
46
77
  if (method === undefined &&
47
78
  route === undefined &&
48
79
  requestId === undefined &&
49
- statusCode === undefined) {
80
+ correlationId === undefined &&
81
+ statusCode === undefined &&
82
+ trace === undefined) {
50
83
  return undefined;
51
84
  }
52
85
  return {
53
86
  ...(method === undefined ? {} : { method }),
54
87
  ...(route === undefined ? {} : { route }),
55
88
  ...(requestId === undefined ? {} : { requestId }),
89
+ ...(correlationId === undefined ? {} : { correlationId }),
56
90
  ...(statusCode === undefined ? {} : { statusCode }),
91
+ ...(trace === undefined ? {} : { trace }),
57
92
  };
58
93
  };
94
+ const safeTags = (value, privacy) => {
95
+ const redacted = (0, redact_js_1.redactValue)(value, { ...privacy, maxKeys: Math.min(privacy.maxKeys, 50) });
96
+ if (!isRecord(redacted)) {
97
+ return undefined;
98
+ }
99
+ const tags = {};
100
+ for (const [key, tagValue] of Object.entries(redacted)) {
101
+ if (key.length > 64 || typeof tagValue !== "string") {
102
+ continue;
103
+ }
104
+ tags[key] = tagValue.slice(0, 200);
105
+ }
106
+ return Object.keys(tags).length === 0 ? undefined : tags;
107
+ };
108
+ const safeShortString = (value, privacy, maxLength) => {
109
+ if (value === undefined) {
110
+ return undefined;
111
+ }
112
+ const redacted = (0, redact_js_1.redactValue)(value, { ...privacy, maxStringLength: maxLength });
113
+ const text = asString(redacted);
114
+ return text === undefined ? undefined : text.slice(0, maxLength);
115
+ };
59
116
  const safeMessage = (value, privacy) => {
60
117
  const normalized = (0, normalize_js_1.normalizeUnknown)(value, privacy);
61
118
  return typeof normalized === "string" ? normalized : "[unreadable value]";
@@ -69,6 +126,150 @@ const normalizeEventAlertThreshold = (value) => {
69
126
  }
70
127
  return value;
71
128
  };
129
+ const safeErrorFromValue = (value, privacy) => {
130
+ const redacted = (0, redact_js_1.redactValue)(value, privacy);
131
+ if (!isRecord(redacted)) {
132
+ return undefined;
133
+ }
134
+ const name = asString(redacted.name);
135
+ const message = asString(redacted.message);
136
+ if (name === undefined || message === undefined) {
137
+ return undefined;
138
+ }
139
+ const stack = asString(redacted.stack);
140
+ const code = asString(redacted.code);
141
+ const applicationFrame = asString(redacted.applicationFrame);
142
+ return {
143
+ name,
144
+ message,
145
+ ...(stack === undefined ? {} : { stack }),
146
+ ...(code === undefined ? {} : { code }),
147
+ ...(applicationFrame === undefined
148
+ ? {}
149
+ : { applicationFrame: applicationFrame.slice(0, 1_000) }),
150
+ };
151
+ };
152
+ const safeAlertFromValue = (value, fallback, privacy) => {
153
+ const redacted = (0, redact_js_1.redactValue)(value, privacy);
154
+ if (!isRecord(redacted)) {
155
+ return undefined;
156
+ }
157
+ const title = asString(redacted.title);
158
+ const fingerprint = asString(redacted.fingerprint);
159
+ const summary = asString(redacted.summary);
160
+ const severity = asString(redacted.severity);
161
+ const firstSeenAt = asString(redacted.firstSeenAt);
162
+ const lastSeenAt = asString(redacted.lastSeenAt);
163
+ const service = asString(redacted.service);
164
+ const environment = asString(redacted.environment);
165
+ const occurrences = asStatusCode(redacted.occurrences);
166
+ if (title === undefined ||
167
+ fingerprint === undefined ||
168
+ summary === undefined ||
169
+ firstSeenAt === undefined ||
170
+ lastSeenAt === undefined ||
171
+ service === undefined ||
172
+ environment === undefined ||
173
+ occurrences === undefined ||
174
+ (severity !== "low" && severity !== "medium" && severity !== "high" && severity !== "critical")) {
175
+ return undefined;
176
+ }
177
+ const rawActions = redacted.suggestedActions;
178
+ if (!Array.isArray(rawActions)) {
179
+ return undefined;
180
+ }
181
+ const suggestedActions = rawActions
182
+ .filter((action) => typeof action === "string")
183
+ .slice(0, 5)
184
+ .map((action) => action.slice(0, 600));
185
+ const error = safeErrorFromValue(redacted.error, privacy);
186
+ const request = safeRequest(redacted.request, privacy);
187
+ const trace = safeTrace(redacted.trace, privacy);
188
+ const context = safeRecord(redacted.context, privacy);
189
+ const tags = safeTags(redacted.tags, privacy);
190
+ const linksValue = safeRecord(redacted.links, privacy);
191
+ const logLink = linksValue === undefined ? undefined : asString(linksValue.log);
192
+ const traceLink = linksValue === undefined ? undefined : asString(linksValue.trace);
193
+ const links = linksValue
194
+ ? {
195
+ ...(logLink === undefined ? {} : { log: logLink.slice(0, 2_048) }),
196
+ ...(traceLink === undefined ? {} : { trace: traceLink.slice(0, 2_048) }),
197
+ }
198
+ : undefined;
199
+ const instance = asString(redacted.instance);
200
+ const release = asString(redacted.release);
201
+ const correlationId = asString(redacted.correlationId);
202
+ const operation = asString(redacted.operation);
203
+ const job = asString(redacted.job);
204
+ return deepFreeze({
205
+ id: asString(redacted.id) ?? fallback.id,
206
+ fingerprint: fingerprint.slice(0, 200),
207
+ title: title.slice(0, 300),
208
+ severity,
209
+ summary: summary.slice(0, 2_000),
210
+ suggestedActions,
211
+ firstSeenAt: firstSeenAt.slice(0, 100),
212
+ lastSeenAt: lastSeenAt.slice(0, 100),
213
+ occurrences: Math.max(0, Math.min(occurrences, Number.MAX_SAFE_INTEGER)),
214
+ service: service.slice(0, 300),
215
+ environment: environment.slice(0, 300),
216
+ ...(instance === undefined ? {} : { instance: instance.slice(0, 200) }),
217
+ ...(release === undefined ? {} : { release: release.slice(0, 200) }),
218
+ ...(correlationId === undefined ? {} : { correlationId: correlationId.slice(0, 200) }),
219
+ ...(operation === undefined ? {} : { operation: operation.slice(0, 200) }),
220
+ ...(job === undefined ? {} : { job: job.slice(0, 200) }),
221
+ ...(tags === undefined ? {} : { tags }),
222
+ ...(error === undefined ? {} : { error }),
223
+ ...(request === undefined ? {} : { request }),
224
+ ...(trace === undefined ? {} : { trace }),
225
+ ...(context === undefined ? {} : { context }),
226
+ ...(links === undefined || Object.keys(links).length === 0 ? {} : { links }),
227
+ });
228
+ };
229
+ const normalizeFingerprintOverride = (override, event) => {
230
+ if (override === undefined) {
231
+ return undefined;
232
+ }
233
+ const value = typeof override === "function" ? override(event) : override;
234
+ if (value === undefined) {
235
+ return undefined;
236
+ }
237
+ if (typeof value !== "string" || value.trim().length === 0 || value.length > 200) {
238
+ throw new TypeError("fingerprint must be a non-empty string no longer than 200 characters");
239
+ }
240
+ return value.trim();
241
+ };
242
+ const renderLinks = (templates, event, fingerprint) => {
243
+ if (templates === undefined) {
244
+ return undefined;
245
+ }
246
+ const values = {
247
+ service: event.service,
248
+ environment: event.environment,
249
+ release: event.release ?? "",
250
+ instance: event.instance ?? "",
251
+ fingerprint,
252
+ requestId: event.request?.requestId ?? "",
253
+ correlationId: event.correlationId ?? event.request?.correlationId ?? "",
254
+ traceId: event.trace?.traceId ?? event.request?.trace?.traceId ?? "",
255
+ spanId: event.trace?.spanId ?? event.request?.trace?.spanId ?? "",
256
+ route: event.request?.route ?? "",
257
+ statusCode: event.request?.statusCode === undefined ? "" : String(event.request.statusCode),
258
+ };
259
+ const render = (template) => {
260
+ const expanded = template.replace(/\{\{([^{}]+)\}\}/g, (_match, key) => encodeURIComponent(values[key] ?? ""));
261
+ const redacted = (0, redact_js_1.redactValue)(expanded, { maxStringLength: 2_048 });
262
+ return typeof redacted === "string" ? redacted.slice(0, 2_048) : "";
263
+ };
264
+ const result = {};
265
+ if (templates.log !== undefined) {
266
+ result.log = render(templates.log);
267
+ }
268
+ if (templates.trace !== undefined && (values.traceId !== "" || values.spanId !== "")) {
269
+ result.trace = render(templates.trace);
270
+ }
271
+ return Object.keys(result).length === 0 ? undefined : result;
272
+ };
72
273
  function createWotchi(config) {
73
274
  const normalized = (0, config_js_1.validateConfig)(config);
74
275
  const now = Date.now;
@@ -82,48 +283,162 @@ function createWotchi(config) {
82
283
  const queue = (0, notification_queue_js_1.createNotificationQueue)({
83
284
  maxPendingAlerts: normalized.queue.maxPendingAlerts,
84
285
  concurrency: normalized.queue.concurrency,
286
+ notifierTimeoutMs: normalized.queue.notifierTimeoutMs,
287
+ notifierCircuitBreaker: normalized.queue.notifierCircuitBreaker,
85
288
  });
289
+ const admission = normalized.overload === undefined
290
+ ? undefined
291
+ : (0, admission_js_1.createCaptureAdmission)({
292
+ maxEventsPerSecond: normalized.overload.maxEventsPerSecond,
293
+ burst: normalized.overload.burst,
294
+ now,
295
+ });
86
296
  const privacy = normalized.privacy;
87
297
  let eventSequence = 0;
88
- const captureSafeEvent = (error, metadata, context, request, eventKind = "error", alertThreshold = normalized.grouping.alertThreshold) => {
298
+ let overloadLastAlertAt = 0;
299
+ const matchingRule = (event) => normalized.rules.find((rule) => (rule.environment === undefined || rule.environment === event.environment) &&
300
+ (rule.route === undefined || rule.route === event.request?.route));
301
+ const captureSafeEvent = (error, metadata, context, request, eventKind = "error", alertThreshold = normalized.grouping.alertThreshold, fingerprintOverride, severityOverride, trace, correlationId, operation, job, tags) => {
89
302
  const timestamp = now();
90
303
  const safeError = (0, redact_js_1.redactError)((0, normalize_js_1.normalizeError)(error, privacy), privacy);
91
304
  const requestContext = safeRequest(request, privacy);
92
305
  const eventContext = mergeContext(metadata, context, privacy);
306
+ const requestTrace = safeTrace(trace ?? requestContext?.trace, privacy);
307
+ const eventCorrelationId = safeShortString(correlationId ?? requestContext?.correlationId, privacy, 200);
308
+ const operationValue = safeShortString(operation ?? eventContext?.operation, privacy, 200);
309
+ const jobValue = safeShortString(job ?? eventContext?.job ?? eventContext?.queue, privacy, 200);
310
+ const eventTags = safeTags(tags, privacy);
93
311
  const safeEvent = {
94
312
  id: `event-${eventSequence + 1}`,
95
313
  timestamp: new Date(timestamp).toISOString(),
96
314
  service: normalized.service,
97
315
  environment: normalized.environment,
316
+ ...(normalized.instance === undefined ? {} : { instance: normalized.instance }),
98
317
  ...(normalized.release === undefined ? {} : { release: normalized.release }),
318
+ ...(eventCorrelationId === undefined ? {} : { correlationId: eventCorrelationId }),
319
+ ...(operationValue === undefined ? {} : { operation: operationValue.slice(0, 200) }),
320
+ ...(jobValue === undefined ? {} : { job: jobValue.slice(0, 200) }),
321
+ ...(eventTags === undefined ? {} : { tags: eventTags }),
99
322
  error: safeError,
100
323
  ...(requestContext === undefined ? {} : { request: requestContext }),
324
+ ...(requestTrace === undefined ? {} : { trace: requestTrace }),
101
325
  ...(eventContext === undefined ? {} : { context: eventContext }),
102
326
  };
103
327
  eventSequence += 1;
104
- const fingerprint = (0, fingerprint_js_1.fingerprintSafeErrorEvent)(safeEvent);
105
- const group = groupStore.record(fingerprint, safeEvent);
106
- const decision = (0, incident_policy_js_1.evaluateIncidentPolicy)({
328
+ const filteredEvent = deepFreeze(safeEvent);
329
+ if (normalized.filter !== undefined) {
330
+ try {
331
+ const accepted = normalized.filter(filteredEvent);
332
+ if (typeof accepted !== "boolean") {
333
+ diagnostics.filterFailures += 1;
334
+ diagnostics.eventsSuppressed += 1;
335
+ return;
336
+ }
337
+ if (!accepted) {
338
+ diagnostics.eventsSuppressed += 1;
339
+ return;
340
+ }
341
+ }
342
+ catch {
343
+ diagnostics.filterFailures += 1;
344
+ diagnostics.eventsSuppressed += 1;
345
+ return;
346
+ }
347
+ }
348
+ const rule = matchingRule(filteredEvent);
349
+ if (rule?.ignore === true) {
350
+ diagnostics.eventsSuppressed += 1;
351
+ return;
352
+ }
353
+ let fingerprint;
354
+ try {
355
+ fingerprint = normalizeFingerprintOverride(fingerprintOverride, filteredEvent);
356
+ }
357
+ catch {
358
+ diagnostics.fingerprintCallbackFailures += 1;
359
+ }
360
+ if (fingerprint === undefined && normalized.fingerprint !== undefined) {
361
+ try {
362
+ fingerprint = normalizeFingerprintOverride(normalized.fingerprint, filteredEvent);
363
+ }
364
+ catch {
365
+ diagnostics.fingerprintCallbackFailures += 1;
366
+ }
367
+ }
368
+ fingerprint ??= (0, fingerprint_js_1.fingerprintSafeErrorEvent)(filteredEvent);
369
+ const group = groupStore.record(fingerprint, filteredEvent, rule?.severity ?? severityOverride ?? "medium");
370
+ const policyInput = {
107
371
  group,
108
372
  now: timestamp,
109
- alertThreshold,
373
+ alertThreshold: rule?.alertThreshold ?? alertThreshold,
110
374
  cooldownMs: normalized.grouping.cooldownMs,
111
375
  eventKind,
112
- });
376
+ };
377
+ const manualSeverity = rule?.severity ?? severityOverride;
378
+ const decision = manualSeverity === undefined
379
+ ? (0, incident_policy_js_1.evaluateIncidentPolicy)(policyInput)
380
+ : (0, incident_policy_js_1.evaluateIncidentPolicy)({ ...policyInput, manualSeverity });
113
381
  if (decision.shouldAlert) {
114
- const alert = (0, incident_builder_js_1.buildIncidentAlert)(group, decision);
115
- if (queue.enqueue(alert, normalized.notifiers)) {
382
+ const builtAlert = (0, incident_builder_js_1.buildIncidentAlert)(group, decision);
383
+ const links = renderLinks(normalized.links, filteredEvent, fingerprint);
384
+ const alert = deepFreeze({
385
+ ...builtAlert,
386
+ ...(links === undefined ? {} : { links }),
387
+ });
388
+ let finalAlert = alert;
389
+ if (normalized.beforeSend !== undefined) {
390
+ let transformed;
391
+ try {
392
+ transformed = normalized.beforeSend(alert);
393
+ }
394
+ catch {
395
+ diagnostics.beforeSendFailures += 1;
396
+ diagnostics.eventsSuppressed += 1;
397
+ return;
398
+ }
399
+ if (transformed === null) {
400
+ diagnostics.eventsSuppressed += 1;
401
+ return;
402
+ }
403
+ if (transformed !== undefined) {
404
+ const sanitized = safeAlertFromValue(transformed, alert, privacy);
405
+ if (sanitized === undefined) {
406
+ diagnostics.beforeSendFailures += 1;
407
+ diagnostics.eventsSuppressed += 1;
408
+ return;
409
+ }
410
+ finalAlert = sanitized;
411
+ }
412
+ }
413
+ if (queue.enqueue(finalAlert, normalized.notifiers)) {
116
414
  groupStore.markAlerted(fingerprint, timestamp);
117
415
  }
118
416
  }
119
417
  diagnostics.capturedEvents += 1;
120
418
  };
121
- const captureException = (error, context) => {
419
+ const emitOverloadSignal = (timestamp) => {
420
+ const cooldownMs = normalized.overload?.alertCooldownMs ?? 0;
421
+ if (cooldownMs > 0 && timestamp - overloadLastAlertAt < cooldownMs) {
422
+ return;
423
+ }
424
+ overloadLastAlertAt = timestamp;
425
+ captureSafeEvent(new Error("Wotchi capture overload: admission limit reached"), undefined, { droppedEvents: diagnostics.eventsDroppedOverload }, undefined, "runtime-monitor", 1, "wotchi.capture.overload", "high", undefined, undefined, "wotchi.overload", undefined, undefined);
426
+ };
427
+ const captureException = (error, context, options) => {
122
428
  if (!normalized.enabled) {
123
429
  return;
124
430
  }
431
+ if (queue.isClosed()) {
432
+ diagnostics.capturesAfterShutdown += 1;
433
+ return;
434
+ }
125
435
  try {
126
- captureSafeEvent(error, undefined, context, undefined);
436
+ if (admission !== undefined && !admission.tryAcquire()) {
437
+ diagnostics.eventsDroppedOverload += 1;
438
+ emitOverloadSignal(now());
439
+ return;
440
+ }
441
+ captureSafeEvent(error, undefined, context, options?.request, "error", normalizeEventAlertThreshold(options?.alertThreshold) ?? normalized.grouping.alertThreshold, options?.fingerprint, options?.severity, options?.trace, options?.correlationId, options?.operation, options?.job, options?.tags);
127
442
  }
128
443
  catch {
129
444
  diagnostics.captureFailures += 1;
@@ -133,6 +448,10 @@ function createWotchi(config) {
133
448
  if (!normalized.enabled) {
134
449
  return;
135
450
  }
451
+ if (queue.isClosed()) {
452
+ diagnostics.capturesAfterShutdown += 1;
453
+ return;
454
+ }
136
455
  try {
137
456
  if (event === null || typeof event !== "object" || event.level !== "error") {
138
457
  throw new TypeError("event.level must be error");
@@ -141,24 +460,100 @@ function createWotchi(config) {
141
460
  throw new TypeError("event.message must be a string");
142
461
  }
143
462
  const alertThreshold = normalizeEventAlertThreshold(event.alertThreshold);
144
- captureSafeEvent(event.error ?? safeMessage(event.message, privacy), event.metadata, event.context, event.request, event.kind ?? "error", alertThreshold ?? normalized.grouping.alertThreshold);
463
+ if (admission !== undefined &&
464
+ event.kind !== "process-monitor" &&
465
+ event.kind !== "runtime-monitor" &&
466
+ !admission.tryAcquire()) {
467
+ diagnostics.eventsDroppedOverload += 1;
468
+ emitOverloadSignal(now());
469
+ return;
470
+ }
471
+ captureSafeEvent(event.error ?? safeMessage(event.message, privacy), event.metadata, event.context, event.request, event.kind ?? "error", alertThreshold ?? normalized.grouping.alertThreshold, event.fingerprint, event.severity, event.trace, event.correlationId, event.operation, event.job, event.tags);
145
472
  }
146
473
  catch {
147
474
  diagnostics.captureFailures += 1;
148
475
  }
149
476
  };
477
+ const readDiagnostics = () => (0, diagnostics_js_1.snapshotDiagnostics)(diagnostics, {
478
+ groupsEvicted: groupStore.groupsEvicted(),
479
+ alertsQueued: queue.alertsQueued(),
480
+ alertsDropped: queue.alertsDropped(),
481
+ alertsSent: queue.alertsSent(),
482
+ notifierFailures: queue.notifierFailures(),
483
+ notifierTimeouts: queue.notifierTimeouts(),
484
+ notifierCircuitOpenSkips: queue.notifierCircuitOpenSkips(),
485
+ activeGroups: groupStore.size(),
486
+ pendingAlerts: queue.pending(),
487
+ });
488
+ const testAlert = async () => {
489
+ const timestamp = new Date(now()).toISOString();
490
+ const alert = {
491
+ id: `wotchi-test-${timestamp}`,
492
+ fingerprint: "wotchi-test-alert",
493
+ title: "Wotchi — Test alert",
494
+ severity: "low",
495
+ summary: "This is a notifier configuration test alert.",
496
+ suggestedActions: ["Confirm this destination received the test alert."],
497
+ firstSeenAt: timestamp,
498
+ lastSeenAt: timestamp,
499
+ occurrences: 1,
500
+ service: normalized.service,
501
+ environment: normalized.environment,
502
+ ...(normalized.instance === undefined ? {} : { instance: normalized.instance }),
503
+ ...(normalized.release === undefined ? {} : { release: normalized.release }),
504
+ };
505
+ let jobResult;
506
+ if (!queue.enqueue(alert, normalized.notifiers, (result) => {
507
+ jobResult = result;
508
+ })) {
509
+ return {
510
+ status: "queue-full",
511
+ configurationAccepted: true,
512
+ queued: false,
513
+ flushed: false,
514
+ delivered: false,
515
+ notifierFailures: 0,
516
+ diagnostics: readDiagnostics(),
517
+ error: "Notification queue is full; test alert was not queued.",
518
+ };
519
+ }
520
+ try {
521
+ await queue.flush();
522
+ }
523
+ catch {
524
+ return {
525
+ status: "timeout",
526
+ configurationAccepted: true,
527
+ queued: true,
528
+ flushed: false,
529
+ delivered: false,
530
+ notifierFailures: jobResult?.notifierFailures ?? 0,
531
+ diagnostics: readDiagnostics(),
532
+ error: "Notification queue flush timed out.",
533
+ };
534
+ }
535
+ const notifierFailures = jobResult?.notifierFailures ?? 0;
536
+ const status = notifierFailures > 0 ? "notifier-failed" : "sent";
537
+ return {
538
+ status,
539
+ configurationAccepted: true,
540
+ queued: true,
541
+ flushed: true,
542
+ delivered: status === "sent",
543
+ notifierFailures,
544
+ diagnostics: readDiagnostics(),
545
+ ...(status === "notifier-failed" ? { error: "One or more notifiers failed." } : {}),
546
+ };
547
+ };
548
+ const shutdown = async (timeoutMs) => {
549
+ await queue.close(timeoutMs);
550
+ };
150
551
  return {
151
552
  captureException,
152
553
  captureEvent,
554
+ testAlert,
153
555
  flush: (timeoutMs) => queue.flush(timeoutMs),
154
- getDiagnostics: () => (0, diagnostics_js_1.snapshotDiagnostics)(diagnostics, {
155
- groupsEvicted: groupStore.groupsEvicted(),
156
- alertsQueued: queue.alertsQueued(),
157
- alertsDropped: queue.alertsDropped(),
158
- alertsSent: queue.alertsSent(),
159
- notifierFailures: queue.notifierFailures(),
160
- activeGroups: groupStore.size(),
161
- pendingAlerts: queue.pending(),
162
- }),
556
+ shutdown,
557
+ getDiagnostics: readDiagnostics,
163
558
  };
164
559
  }