@futurewindai/wotchi 0.1.0-beta.1 → 0.1.0-beta.5

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