@futurewindai/wotchi 0.1.0-beta.2 → 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 (70) hide show
  1. package/README.md +102 -25
  2. package/dist/cjs/core/client.js +370 -21
  3. package/dist/cjs/core/config.js +177 -18
  4. package/dist/cjs/core/diagnostics.js +12 -1
  5. package/dist/cjs/core/group-store.js +15 -1
  6. package/dist/cjs/core/incident-builder.js +18 -0
  7. package/dist/cjs/core/incident-policy.js +2 -7
  8. package/dist/cjs/core/limits.js +13 -0
  9. package/dist/cjs/core/normalize.js +10 -5
  10. package/dist/cjs/core/notification-queue.js +20 -3
  11. package/dist/cjs/core/redact.js +53 -7
  12. package/dist/cjs/index.js +3 -1
  13. package/dist/cjs/integrations/express/error-handler.js +2 -1
  14. package/dist/cjs/integrations/express/index.js +2 -1
  15. package/dist/cjs/integrations/express/status-observer.js +6 -0
  16. package/dist/cjs/integrations/nest/exception-filter.js +9 -1
  17. package/dist/cjs/integrations/nest/index.js +2 -1
  18. package/dist/cjs/integrations/request-context.js +83 -1
  19. package/dist/cjs/notifiers/alert-payload.js +20 -0
  20. package/dist/cjs/notifiers/console.js +85 -15
  21. package/dist/cjs/notifiers/telegram-format.js +75 -5
  22. package/dist/cjs/notifiers/webhook-http.js +394 -0
  23. package/dist/cjs/notifiers/webhook.js +28 -0
  24. package/dist/esm/core/client.js +370 -21
  25. package/dist/esm/core/config.js +177 -18
  26. package/dist/esm/core/diagnostics.js +12 -1
  27. package/dist/esm/core/group-store.js +15 -1
  28. package/dist/esm/core/incident-builder.js +18 -0
  29. package/dist/esm/core/incident-policy.js +2 -7
  30. package/dist/esm/core/limits.js +10 -0
  31. package/dist/esm/core/normalize.js +10 -5
  32. package/dist/esm/core/notification-queue.js +20 -3
  33. package/dist/esm/core/redact.js +53 -7
  34. package/dist/esm/index.js +2 -1
  35. package/dist/esm/integrations/express/error-handler.js +2 -1
  36. package/dist/esm/integrations/express/index.js +1 -1
  37. package/dist/esm/integrations/express/status-observer.js +6 -0
  38. package/dist/esm/integrations/nest/exception-filter.js +9 -1
  39. package/dist/esm/integrations/nest/index.js +1 -1
  40. package/dist/esm/integrations/request-context.js +83 -1
  41. package/dist/esm/notifiers/alert-payload.js +16 -0
  42. package/dist/esm/notifiers/console.js +85 -15
  43. package/dist/esm/notifiers/telegram-format.js +75 -5
  44. package/dist/esm/notifiers/webhook-http.js +387 -0
  45. package/dist/esm/notifiers/webhook.js +24 -0
  46. package/dist/types/core/config.d.ts +7 -1
  47. package/dist/types/core/diagnostics.d.ts +5 -1
  48. package/dist/types/core/limits.d.ts +10 -0
  49. package/dist/types/core/notification-queue.d.ts +5 -1
  50. package/dist/types/core/types.d.ts +95 -1
  51. package/dist/types/index.d.ts +4 -2
  52. package/dist/types/integrations/express/index.d.ts +2 -2
  53. package/dist/types/integrations/nest/index.d.ts +2 -2
  54. package/dist/types/integrations/request-context.d.ts +3 -0
  55. package/dist/types/notifiers/alert-payload.d.ts +5 -0
  56. package/dist/types/notifiers/webhook-http.d.ts +37 -0
  57. package/dist/types/notifiers/webhook.d.ts +4 -0
  58. package/dist/types-cjs/core/config.d.cts +7 -1
  59. package/dist/types-cjs/core/diagnostics.d.cts +5 -1
  60. package/dist/types-cjs/core/limits.d.cts +10 -0
  61. package/dist/types-cjs/core/notification-queue.d.cts +5 -1
  62. package/dist/types-cjs/core/types.d.cts +95 -1
  63. package/dist/types-cjs/index.d.cts +4 -2
  64. package/dist/types-cjs/integrations/express/index.d.cts +2 -2
  65. package/dist/types-cjs/integrations/nest/index.d.cts +2 -2
  66. package/dist/types-cjs/integrations/request-context.d.cts +3 -0
  67. package/dist/types-cjs/notifiers/alert-payload.d.cts +5 -0
  68. package/dist/types-cjs/notifiers/webhook-http.d.cts +37 -0
  69. package/dist/types-cjs/notifiers/webhook.d.cts +4 -0
  70. package/package.json +6 -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,26 @@ 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");
83
235
  const privacy = readOptionalRecord(config.privacy, "privacy");
236
+ const links = normalizeLinks(config.links);
84
237
  const concurrency = queue?.concurrency;
85
238
  if (concurrency !== undefined && concurrency !== 1) {
86
239
  return fail("queue.concurrency");
@@ -88,25 +241,31 @@ function validateConfig(config) {
88
241
  const normalized = {
89
242
  service: service.trim(),
90
243
  environment: environment.trim(),
91
- ...(config.release === undefined ? {} : { release: config.release.trim() }),
244
+ ...(config.instance === undefined ? {} : { instance: config.instance.trim().slice(0, 200) }),
245
+ ...(config.release === undefined ? {} : { release: config.release.trim().slice(0, 200) }),
92
246
  enabled: config.enabled ?? true,
247
+ ...(config.filter === undefined ? {} : { filter: freezeHook(config.filter) }),
248
+ ...(config.fingerprint === undefined ? {} : { fingerprint: freezeHook(config.fingerprint) }),
249
+ ...(config.beforeSend === undefined ? {} : { beforeSend: freezeHook(config.beforeSend) }),
250
+ ...(links === undefined ? {} : { links }),
251
+ rules: normalizeRules(config.rules),
93
252
  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),
253
+ windowMs: readPositiveInteger(grouping, "windowMs", 60_000, limits_js_1.MAX_WINDOW_MS),
254
+ alertThreshold: readPositiveInteger(grouping, "alertThreshold", 3, limits_js_1.MAX_ALERT_THRESHOLD),
255
+ cooldownMs: readPositiveInteger(grouping, "cooldownMs", 900_000, limits_js_1.MAX_COOLDOWN_MS),
256
+ maxGroups: readPositiveInteger(grouping, "maxGroups", 200, limits_js_1.MAX_GROUPS),
257
+ maxEventsPerWindow: readPositiveInteger(grouping, "maxEventsPerWindow", 100, limits_js_1.MAX_EVENTS_PER_WINDOW),
99
258
  }),
100
259
  queue: Object.freeze({
101
- maxPendingAlerts: readPositiveInteger(queue, "maxPendingAlerts", 100),
260
+ maxPendingAlerts: readPositiveInteger(queue, "maxPendingAlerts", 100, limits_js_1.MAX_PENDING_ALERTS),
102
261
  concurrency: 1,
103
262
  }),
104
263
  privacy: Object.freeze({
105
264
  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),
265
+ maxDepth: readPositiveInteger(privacy, "maxDepth", 5, limits_js_1.MAX_NORMALIZATION_DEPTH),
266
+ maxKeys: readPositiveInteger(privacy, "maxKeys", 100, limits_js_1.MAX_NORMALIZATION_KEYS),
267
+ maxStringLength: readPositiveInteger(privacy, "maxStringLength", 500, limits_js_1.MAX_NORMALIZATION_STRING_LENGTH),
268
+ maxStackLength: readPositiveInteger(privacy, "maxStackLength", 4_000, limits_js_1.MAX_NORMALIZATION_STACK_LENGTH),
110
269
  }),
111
270
  notifiers: normalizeNotifiers(config.notifiers),
112
271
  };
@@ -3,12 +3,23 @@ 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
+ };
7
14
  }
8
15
  function snapshotDiagnostics(state, values) {
9
16
  return Object.freeze({
10
17
  capturedEvents: state.capturedEvents,
11
18
  captureFailures: state.captureFailures,
19
+ fingerprintCallbackFailures: state.fingerprintCallbackFailures,
20
+ filterFailures: state.filterFailures,
21
+ beforeSendFailures: state.beforeSendFailures,
22
+ eventsSuppressed: state.eventsSuppressed,
12
23
  ...values,
13
24
  });
14
25
  }
@@ -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;
@@ -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,13 @@
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_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_NORMALIZATION_DEPTH = 20;
11
+ exports.MAX_NORMALIZATION_KEYS = 10_000;
12
+ exports.MAX_NORMALIZATION_STRING_LENGTH = 32_768;
13
+ 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,6 +90,9 @@ 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;
@@ -136,3 +140,4 @@ function normalizeError(error, limits) {
136
140
  const message = typeof normalized === "string" ? normalized : UNREADABLE_VALUE;
137
141
  return { name: "UnknownError", message };
138
142
  }
143
+ const limits_js_1 = require("./limits.js");
@@ -1,9 +1,12 @@
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;
5
6
  function createNotificationQueue(options) {
6
- if (!Number.isSafeInteger(options.maxPendingAlerts) || options.maxPendingAlerts <= 0) {
7
+ if (!Number.isSafeInteger(options.maxPendingAlerts) ||
8
+ options.maxPendingAlerts <= 0 ||
9
+ options.maxPendingAlerts > limits_js_1.MAX_PENDING_ALERTS) {
7
10
  throw new RangeError("maxPendingAlerts must be a positive integer");
8
11
  }
9
12
  if (options.concurrency !== 1) {
@@ -35,15 +38,25 @@ function createNotificationQueue(options) {
35
38
  resolveDrain = undefined;
36
39
  };
37
40
  const processJob = async (job) => {
41
+ let jobSent = 0;
42
+ let jobFailures = 0;
38
43
  for (const notifier of job.notifiers) {
39
44
  try {
40
45
  await notifier.send(job.alert);
41
46
  sent += 1;
47
+ jobSent += 1;
42
48
  }
43
49
  catch (error) {
50
+ jobFailures += 1;
44
51
  notifyFailure(error, notifier);
45
52
  }
46
53
  }
54
+ try {
55
+ job.onComplete?.({ notifierFailures: jobFailures, sent: jobSent });
56
+ }
57
+ catch {
58
+ // Job observers cannot be allowed to affect notification processing.
59
+ }
47
60
  };
48
61
  const pump = () => {
49
62
  while (activeJobs < options.concurrency && pendingJobs.length > 0) {
@@ -59,12 +72,16 @@ function createNotificationQueue(options) {
59
72
  });
60
73
  }
61
74
  };
62
- const enqueue = (alert, notifiers) => {
75
+ const enqueue = (alert, notifiers, onComplete) => {
63
76
  if (pendingJobs.length >= options.maxPendingAlerts) {
64
77
  dropped += 1;
65
78
  return false;
66
79
  }
67
- pendingJobs.push({ alert, notifiers: [...notifiers] });
80
+ pendingJobs.push({
81
+ alert,
82
+ notifiers: [...notifiers],
83
+ ...(onComplete === undefined ? {} : { onComplete }),
84
+ });
68
85
  queued += 1;
69
86
  pump();
70
87
  return true;
@@ -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) => {
@@ -77,11 +88,44 @@ const redactConnectionUrl = (candidate) => {
77
88
  return `${core.slice(0, authorityStart)}${redactedAuthority}${remainder}${suffix}`;
78
89
  };
79
90
  const redactConnectionUrls = (value) => value.replace(CONNECTION_URL_PATTERN, redactConnectionUrl);
91
+ const SENSITIVE_QUERY_KEYS = new Set([
92
+ "authorization",
93
+ "cookie",
94
+ "password",
95
+ "passwd",
96
+ "secret",
97
+ "signature",
98
+ "token",
99
+ "accesstoken",
100
+ "refreshtoken",
101
+ "apikey",
102
+ "xapikey",
103
+ "session",
104
+ ]);
105
+ const decodeQueryKey = (value) => {
106
+ try {
107
+ return decodeURIComponent(value.replace(/\+/g, " "));
108
+ }
109
+ catch {
110
+ return value;
111
+ }
112
+ };
113
+ const redactSensitiveQueryValues = (value) => value.replace(/([?&])([^=&#\s]+)=([^&#\s]*)/g, (match, prefix, key) => {
114
+ if (!SENSITIVE_QUERY_KEYS.has(normalizeKey(decodeQueryKey(key)))) {
115
+ return match;
116
+ }
117
+ return prefix + key + "=" + exports.REDACTED;
118
+ });
119
+ const redactWebhookPathSecrets = (value) => value.replace(/(https?:\/\/[^/\s]+\/(?:services|hooks|webhooks?)\/)[^/?#\s]+/gi, (_match, prefix) => prefix + exports.REDACTED);
80
120
  const redactString = (value, maxStringLength) => {
81
121
  let result = redactConnectionUrls(value);
122
+ result = redactWebhookPathSecrets(result);
123
+ result = redactSensitiveQueryValues(result);
82
124
  result = result.replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi, exports.REDACTED);
83
125
  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) => {
126
+ result = result.replace(/\bBasic\s+[A-Za-z0-9+/=]+/gi, `Basic ${exports.REDACTED}`);
127
+ result = result.replace(/\b(?:Cookie|Set-Cookie)\s*:\s*[^\r\n]+/gi, "Cookie: [REDACTED]");
128
+ 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
129
  const separatorIndex = match.search(/[:=]/);
86
130
  return separatorIndex < 0 ? exports.REDACTED : `${match.slice(0, separatorIndex + 1)}${exports.REDACTED}`;
87
131
  });
@@ -90,7 +134,9 @@ const redactString = (value, maxStringLength) => {
90
134
  result = redactCardLikeValues(result);
91
135
  return result.slice(0, maxStringLength);
92
136
  };
93
- const normalizedOption = (value, fallback) => Number.isSafeInteger(value) && value !== undefined && value > 0 ? value : fallback;
137
+ const normalizedOption = (value, fallback, maximum) => Number.isSafeInteger(value) && value !== undefined && value > 0 && value <= maximum
138
+ ? value
139
+ : fallback;
94
140
  const redactNormalized = (value, sensitiveKeys, options, depth, keysVisited) => {
95
141
  if (typeof value === "string") {
96
142
  return redactString(value, options.maxStringLength);
@@ -128,10 +174,10 @@ const redactNormalized = (value, sensitiveKeys, options, depth, keysVisited) =>
128
174
  return result;
129
175
  };
130
176
  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),
177
+ maxDepth: normalizedOption(options.maxDepth, 5, limits_js_1.MAX_NORMALIZATION_DEPTH),
178
+ maxKeys: normalizedOption(options.maxKeys, 100, limits_js_1.MAX_NORMALIZATION_KEYS),
179
+ maxStringLength: normalizedOption(options.maxStringLength, 500, limits_js_1.MAX_NORMALIZATION_STRING_LENGTH),
180
+ maxStackLength: normalizedOption(options.maxStackLength, 4_000, limits_js_1.MAX_NORMALIZATION_STACK_LENGTH),
135
181
  });
136
182
  const sensitiveKeySet = (options) => {
137
183
  const keys = new Set(defaultKeySet);
package/dist/cjs/index.js CHANGED
@@ -1,6 +1,6 @@
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.registerWotchiProcessMonitor = 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");
@@ -9,5 +9,7 @@ const console_js_1 = require("./notifiers/console.js");
9
9
  Object.defineProperty(exports, "consoleNotifier", { enumerable: true, get: function () { return console_js_1.consoleNotifier; } });
10
10
  const telegram_js_1 = require("./notifiers/telegram.js");
11
11
  Object.defineProperty(exports, "telegramNotifier", { enumerable: true, get: function () { return telegram_js_1.telegramNotifier; } });
12
+ const webhook_js_1 = require("./notifiers/webhook.js");
13
+ Object.defineProperty(exports, "webhookNotifier", { enumerable: true, get: function () { return webhook_js_1.webhookNotifier; } });
12
14
  var errors_js_1 = require("./core/errors.js");
13
15
  Object.defineProperty(exports, "WotchiConfigurationError", { enumerable: true, get: function () { return errors_js_1.WotchiConfigurationError; } });
@@ -7,7 +7,8 @@ function createExpressErrorHandler(client, options) {
7
7
  return (error, request, response, next) => {
8
8
  (0, state_js_1.markExpressErrorCaptured)(request);
9
9
  try {
10
- client.captureException(error, (0, request_context_js_1.getExpressRequestContext)(request, response, options));
10
+ const requestContext = (0, request_context_js_1.getExpressRequestContext)(request, response, options);
11
+ client.captureException(error, undefined, requestContext === undefined ? undefined : { request: requestContext });
11
12
  }
12
13
  catch {
13
14
  // A capture failure must never change Express error handling.
@@ -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, []),