@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.
- package/README.md +102 -25
- package/dist/cjs/core/client.js +370 -21
- package/dist/cjs/core/config.js +177 -18
- package/dist/cjs/core/diagnostics.js +12 -1
- package/dist/cjs/core/group-store.js +15 -1
- package/dist/cjs/core/incident-builder.js +18 -0
- package/dist/cjs/core/incident-policy.js +2 -7
- package/dist/cjs/core/limits.js +13 -0
- package/dist/cjs/core/normalize.js +10 -5
- package/dist/cjs/core/notification-queue.js +20 -3
- package/dist/cjs/core/redact.js +53 -7
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/integrations/express/error-handler.js +2 -1
- package/dist/cjs/integrations/express/index.js +2 -1
- package/dist/cjs/integrations/express/status-observer.js +6 -0
- package/dist/cjs/integrations/nest/exception-filter.js +9 -1
- package/dist/cjs/integrations/nest/index.js +2 -1
- package/dist/cjs/integrations/request-context.js +83 -1
- package/dist/cjs/notifiers/alert-payload.js +20 -0
- package/dist/cjs/notifiers/console.js +85 -15
- package/dist/cjs/notifiers/telegram-format.js +75 -5
- package/dist/cjs/notifiers/webhook-http.js +394 -0
- package/dist/cjs/notifiers/webhook.js +28 -0
- package/dist/esm/core/client.js +370 -21
- package/dist/esm/core/config.js +177 -18
- package/dist/esm/core/diagnostics.js +12 -1
- package/dist/esm/core/group-store.js +15 -1
- package/dist/esm/core/incident-builder.js +18 -0
- package/dist/esm/core/incident-policy.js +2 -7
- package/dist/esm/core/limits.js +10 -0
- package/dist/esm/core/normalize.js +10 -5
- package/dist/esm/core/notification-queue.js +20 -3
- package/dist/esm/core/redact.js +53 -7
- package/dist/esm/index.js +2 -1
- package/dist/esm/integrations/express/error-handler.js +2 -1
- package/dist/esm/integrations/express/index.js +1 -1
- package/dist/esm/integrations/express/status-observer.js +6 -0
- package/dist/esm/integrations/nest/exception-filter.js +9 -1
- package/dist/esm/integrations/nest/index.js +1 -1
- package/dist/esm/integrations/request-context.js +83 -1
- package/dist/esm/notifiers/alert-payload.js +16 -0
- package/dist/esm/notifiers/console.js +85 -15
- package/dist/esm/notifiers/telegram-format.js +75 -5
- package/dist/esm/notifiers/webhook-http.js +387 -0
- package/dist/esm/notifiers/webhook.js +24 -0
- package/dist/types/core/config.d.ts +7 -1
- package/dist/types/core/diagnostics.d.ts +5 -1
- package/dist/types/core/limits.d.ts +10 -0
- package/dist/types/core/notification-queue.d.ts +5 -1
- package/dist/types/core/types.d.ts +95 -1
- package/dist/types/index.d.ts +4 -2
- package/dist/types/integrations/express/index.d.ts +2 -2
- package/dist/types/integrations/nest/index.d.ts +2 -2
- package/dist/types/integrations/request-context.d.ts +3 -0
- package/dist/types/notifiers/alert-payload.d.ts +5 -0
- package/dist/types/notifiers/webhook-http.d.ts +37 -0
- package/dist/types/notifiers/webhook.d.ts +4 -0
- package/dist/types-cjs/core/config.d.cts +7 -1
- package/dist/types-cjs/core/diagnostics.d.cts +5 -1
- package/dist/types-cjs/core/limits.d.cts +10 -0
- package/dist/types-cjs/core/notification-queue.d.cts +5 -1
- package/dist/types-cjs/core/types.d.cts +95 -1
- package/dist/types-cjs/index.d.cts +4 -2
- package/dist/types-cjs/integrations/express/index.d.cts +2 -2
- package/dist/types-cjs/integrations/nest/index.d.cts +2 -2
- package/dist/types-cjs/integrations/request-context.d.cts +3 -0
- package/dist/types-cjs/notifiers/alert-payload.d.cts +5 -0
- package/dist/types-cjs/notifiers/webhook-http.d.cts +37 -0
- package/dist/types-cjs/notifiers/webhook.d.cts +4 -0
- package/package.json +6 -4
package/dist/esm/core/config.js
CHANGED
|
@@ -1,16 +1,33 @@
|
|
|
1
1
|
import { WotchiConfigurationError } from "./errors.js";
|
|
2
|
+
import { MAX_ALERT_THRESHOLD, MAX_COOLDOWN_MS, MAX_EVENTS_PER_WINDOW, MAX_GROUPS, MAX_NORMALIZATION_DEPTH, MAX_NORMALIZATION_KEYS, MAX_NORMALIZATION_STACK_LENGTH, MAX_NORMALIZATION_STRING_LENGTH, MAX_PENDING_ALERTS, MAX_WINDOW_MS, } from "./limits.js";
|
|
2
3
|
export { WotchiConfigurationError };
|
|
3
4
|
const MAX_REDACT_KEYS = 100;
|
|
5
|
+
const MAX_RULES = 50;
|
|
6
|
+
const MAX_RULE_STRING_LENGTH = 200;
|
|
7
|
+
const MAX_LINK_TEMPLATE_LENGTH = 1_000;
|
|
8
|
+
const ALLOWED_LINK_PLACEHOLDERS = new Set([
|
|
9
|
+
"service",
|
|
10
|
+
"environment",
|
|
11
|
+
"release",
|
|
12
|
+
"instance",
|
|
13
|
+
"fingerprint",
|
|
14
|
+
"requestId",
|
|
15
|
+
"correlationId",
|
|
16
|
+
"traceId",
|
|
17
|
+
"spanId",
|
|
18
|
+
"route",
|
|
19
|
+
"statusCode",
|
|
20
|
+
]);
|
|
4
21
|
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
5
22
|
const fail = (field) => {
|
|
6
23
|
throw new WotchiConfigurationError(`${field} is invalid`);
|
|
7
24
|
};
|
|
8
|
-
const readPositiveInteger = (parent, field, defaultValue) => {
|
|
25
|
+
const readPositiveInteger = (parent, field, defaultValue, maximum) => {
|
|
9
26
|
const value = parent?.[field];
|
|
10
27
|
if (value === undefined) {
|
|
11
28
|
return defaultValue;
|
|
12
29
|
}
|
|
13
|
-
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
|
|
30
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0 || value > maximum) {
|
|
14
31
|
return fail(field);
|
|
15
32
|
}
|
|
16
33
|
return value;
|
|
@@ -24,6 +41,120 @@ const readOptionalRecord = (value, field) => {
|
|
|
24
41
|
}
|
|
25
42
|
return value;
|
|
26
43
|
};
|
|
44
|
+
const normalizeRuleString = (value, field) => {
|
|
45
|
+
if (value === undefined) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
49
|
+
return fail(field);
|
|
50
|
+
}
|
|
51
|
+
return value.trim().slice(0, MAX_RULE_STRING_LENGTH);
|
|
52
|
+
};
|
|
53
|
+
const normalizeSeverity = (value, field) => {
|
|
54
|
+
if (value === undefined) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
if (value !== "low" && value !== "medium" && value !== "high" && value !== "critical") {
|
|
58
|
+
return fail(field);
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
};
|
|
62
|
+
const freezeHook = (hook) => {
|
|
63
|
+
try {
|
|
64
|
+
return Object.freeze(hook);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return hook;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const normalizeLinks = (value) => {
|
|
71
|
+
if (value === undefined) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
if (!isRecord(value)) {
|
|
75
|
+
return fail("links");
|
|
76
|
+
}
|
|
77
|
+
const normalized = {};
|
|
78
|
+
for (const key of ["log", "trace"]) {
|
|
79
|
+
const raw = value[key];
|
|
80
|
+
if (raw === undefined) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (typeof raw !== "string" || raw.length === 0 || raw.length > MAX_LINK_TEMPLATE_LENGTH) {
|
|
84
|
+
return fail(`links.${key}`);
|
|
85
|
+
}
|
|
86
|
+
let parsed;
|
|
87
|
+
try {
|
|
88
|
+
parsed = new URL(raw.replace(/\{\{[^{}]+\}\}/g, "placeholder"));
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return fail(`links.${key}`);
|
|
92
|
+
}
|
|
93
|
+
if (parsed.protocol !== "https:" ||
|
|
94
|
+
parsed.username !== "" ||
|
|
95
|
+
parsed.password !== "" ||
|
|
96
|
+
parsed.hash !== "") {
|
|
97
|
+
return fail(`links.${key}`);
|
|
98
|
+
}
|
|
99
|
+
const placeholders = raw.match(/\{\{([^{}]+)\}\}/g) ?? [];
|
|
100
|
+
if (/[{}]/.test(raw.replace(/\{\{[^{}]+\}\}/g, ""))) {
|
|
101
|
+
return fail(`links.${key}`);
|
|
102
|
+
}
|
|
103
|
+
for (const placeholder of placeholders) {
|
|
104
|
+
const name = placeholder.slice(2, -2);
|
|
105
|
+
if (!ALLOWED_LINK_PLACEHOLDERS.has(name)) {
|
|
106
|
+
return fail(`links.${key}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
normalized[key] = raw;
|
|
110
|
+
}
|
|
111
|
+
return Object.freeze(normalized);
|
|
112
|
+
};
|
|
113
|
+
const normalizeRules = (value) => {
|
|
114
|
+
if (value === undefined) {
|
|
115
|
+
return Object.freeze([]);
|
|
116
|
+
}
|
|
117
|
+
if (!Array.isArray(value) || value.length > MAX_RULES) {
|
|
118
|
+
return fail("rules");
|
|
119
|
+
}
|
|
120
|
+
const rules = value.map((rawRule, index) => {
|
|
121
|
+
if (!isRecord(rawRule)) {
|
|
122
|
+
return fail(`rules[${index}]`);
|
|
123
|
+
}
|
|
124
|
+
const alertThreshold = rawRule.alertThreshold;
|
|
125
|
+
if (alertThreshold !== undefined &&
|
|
126
|
+
(typeof alertThreshold !== "number" ||
|
|
127
|
+
!Number.isSafeInteger(alertThreshold) ||
|
|
128
|
+
alertThreshold <= 0 ||
|
|
129
|
+
alertThreshold > 1_000_000)) {
|
|
130
|
+
return fail(`rules[${index}].alertThreshold`);
|
|
131
|
+
}
|
|
132
|
+
if (rawRule.ignore !== undefined && typeof rawRule.ignore !== "boolean") {
|
|
133
|
+
return fail(`rules[${index}].ignore`);
|
|
134
|
+
}
|
|
135
|
+
const environment = normalizeRuleString(rawRule.environment, `rules[${index}].environment`);
|
|
136
|
+
const route = normalizeRuleString(rawRule.route, `rules[${index}].route`);
|
|
137
|
+
const severity = normalizeSeverity(rawRule.severity, `rules[${index}].severity`);
|
|
138
|
+
const rule = {};
|
|
139
|
+
if (environment !== undefined) {
|
|
140
|
+
rule.environment = environment;
|
|
141
|
+
}
|
|
142
|
+
if (route !== undefined) {
|
|
143
|
+
rule.route = route;
|
|
144
|
+
}
|
|
145
|
+
if (rawRule.ignore !== undefined) {
|
|
146
|
+
rule.ignore = rawRule.ignore;
|
|
147
|
+
}
|
|
148
|
+
if (alertThreshold !== undefined) {
|
|
149
|
+
rule.alertThreshold = alertThreshold;
|
|
150
|
+
}
|
|
151
|
+
if (severity !== undefined) {
|
|
152
|
+
rule.severity = severity;
|
|
153
|
+
}
|
|
154
|
+
return Object.freeze(rule);
|
|
155
|
+
});
|
|
156
|
+
return Object.freeze(rules);
|
|
157
|
+
};
|
|
27
158
|
const normalizeRedactKeys = (privacy) => {
|
|
28
159
|
const value = privacy?.redactKeys;
|
|
29
160
|
if (value === undefined) {
|
|
@@ -46,11 +177,19 @@ const normalizeNotifiers = (value) => {
|
|
|
46
177
|
}
|
|
47
178
|
for (let index = 0; index < value.length; index += 1) {
|
|
48
179
|
const notifier = value[index];
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
180
|
+
const valid = (() => {
|
|
181
|
+
try {
|
|
182
|
+
return (isRecord(notifier) &&
|
|
183
|
+
typeof notifier.name === "string" &&
|
|
184
|
+
notifier.name.trim().length > 0 &&
|
|
185
|
+
typeof notifier.send === "function");
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
})();
|
|
191
|
+
if (!valid) {
|
|
192
|
+
return fail("notifiers[" + index + "]");
|
|
54
193
|
}
|
|
55
194
|
}
|
|
56
195
|
return Object.freeze([...value]);
|
|
@@ -71,12 +210,26 @@ export function validateConfig(config) {
|
|
|
71
210
|
(typeof config.release !== "string" || config.release.trim().length === 0)) {
|
|
72
211
|
return fail("release");
|
|
73
212
|
}
|
|
213
|
+
if (config.instance !== undefined &&
|
|
214
|
+
(typeof config.instance !== "string" || config.instance.trim().length === 0)) {
|
|
215
|
+
return fail("instance");
|
|
216
|
+
}
|
|
74
217
|
if (config.enabled !== undefined && typeof config.enabled !== "boolean") {
|
|
75
218
|
return fail("enabled");
|
|
76
219
|
}
|
|
220
|
+
if (config.filter !== undefined && typeof config.filter !== "function") {
|
|
221
|
+
return fail("filter");
|
|
222
|
+
}
|
|
223
|
+
if (config.fingerprint !== undefined && typeof config.fingerprint !== "function") {
|
|
224
|
+
return fail("fingerprint");
|
|
225
|
+
}
|
|
226
|
+
if (config.beforeSend !== undefined && typeof config.beforeSend !== "function") {
|
|
227
|
+
return fail("beforeSend");
|
|
228
|
+
}
|
|
77
229
|
const grouping = readOptionalRecord(config.grouping, "grouping");
|
|
78
230
|
const queue = readOptionalRecord(config.queue, "queue");
|
|
79
231
|
const privacy = readOptionalRecord(config.privacy, "privacy");
|
|
232
|
+
const links = normalizeLinks(config.links);
|
|
80
233
|
const concurrency = queue?.concurrency;
|
|
81
234
|
if (concurrency !== undefined && concurrency !== 1) {
|
|
82
235
|
return fail("queue.concurrency");
|
|
@@ -84,25 +237,31 @@ export function validateConfig(config) {
|
|
|
84
237
|
const normalized = {
|
|
85
238
|
service: service.trim(),
|
|
86
239
|
environment: environment.trim(),
|
|
87
|
-
...(config.
|
|
240
|
+
...(config.instance === undefined ? {} : { instance: config.instance.trim().slice(0, 200) }),
|
|
241
|
+
...(config.release === undefined ? {} : { release: config.release.trim().slice(0, 200) }),
|
|
88
242
|
enabled: config.enabled ?? true,
|
|
243
|
+
...(config.filter === undefined ? {} : { filter: freezeHook(config.filter) }),
|
|
244
|
+
...(config.fingerprint === undefined ? {} : { fingerprint: freezeHook(config.fingerprint) }),
|
|
245
|
+
...(config.beforeSend === undefined ? {} : { beforeSend: freezeHook(config.beforeSend) }),
|
|
246
|
+
...(links === undefined ? {} : { links }),
|
|
247
|
+
rules: normalizeRules(config.rules),
|
|
89
248
|
grouping: Object.freeze({
|
|
90
|
-
windowMs: readPositiveInteger(grouping, "windowMs", 60_000),
|
|
91
|
-
alertThreshold: readPositiveInteger(grouping, "alertThreshold", 3),
|
|
92
|
-
cooldownMs: readPositiveInteger(grouping, "cooldownMs", 900_000),
|
|
93
|
-
maxGroups: readPositiveInteger(grouping, "maxGroups", 200),
|
|
94
|
-
maxEventsPerWindow: readPositiveInteger(grouping, "maxEventsPerWindow", 100),
|
|
249
|
+
windowMs: readPositiveInteger(grouping, "windowMs", 60_000, MAX_WINDOW_MS),
|
|
250
|
+
alertThreshold: readPositiveInteger(grouping, "alertThreshold", 3, MAX_ALERT_THRESHOLD),
|
|
251
|
+
cooldownMs: readPositiveInteger(grouping, "cooldownMs", 900_000, MAX_COOLDOWN_MS),
|
|
252
|
+
maxGroups: readPositiveInteger(grouping, "maxGroups", 200, MAX_GROUPS),
|
|
253
|
+
maxEventsPerWindow: readPositiveInteger(grouping, "maxEventsPerWindow", 100, MAX_EVENTS_PER_WINDOW),
|
|
95
254
|
}),
|
|
96
255
|
queue: Object.freeze({
|
|
97
|
-
maxPendingAlerts: readPositiveInteger(queue, "maxPendingAlerts", 100),
|
|
256
|
+
maxPendingAlerts: readPositiveInteger(queue, "maxPendingAlerts", 100, MAX_PENDING_ALERTS),
|
|
98
257
|
concurrency: 1,
|
|
99
258
|
}),
|
|
100
259
|
privacy: Object.freeze({
|
|
101
260
|
redactKeys: normalizeRedactKeys(privacy),
|
|
102
|
-
maxDepth: readPositiveInteger(privacy, "maxDepth", 5),
|
|
103
|
-
maxKeys: readPositiveInteger(privacy, "maxKeys", 100),
|
|
104
|
-
maxStringLength: readPositiveInteger(privacy, "maxStringLength", 500),
|
|
105
|
-
maxStackLength: readPositiveInteger(privacy, "maxStackLength", 4_000),
|
|
261
|
+
maxDepth: readPositiveInteger(privacy, "maxDepth", 5, MAX_NORMALIZATION_DEPTH),
|
|
262
|
+
maxKeys: readPositiveInteger(privacy, "maxKeys", 100, MAX_NORMALIZATION_KEYS),
|
|
263
|
+
maxStringLength: readPositiveInteger(privacy, "maxStringLength", 500, MAX_NORMALIZATION_STRING_LENGTH),
|
|
264
|
+
maxStackLength: readPositiveInteger(privacy, "maxStackLength", 4_000, MAX_NORMALIZATION_STACK_LENGTH),
|
|
106
265
|
}),
|
|
107
266
|
notifiers: normalizeNotifiers(config.notifiers),
|
|
108
267
|
};
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
export function createDiagnosticsState() {
|
|
2
|
-
return {
|
|
2
|
+
return {
|
|
3
|
+
capturedEvents: 0,
|
|
4
|
+
captureFailures: 0,
|
|
5
|
+
fingerprintCallbackFailures: 0,
|
|
6
|
+
filterFailures: 0,
|
|
7
|
+
beforeSendFailures: 0,
|
|
8
|
+
eventsSuppressed: 0,
|
|
9
|
+
};
|
|
3
10
|
}
|
|
4
11
|
export function snapshotDiagnostics(state, values) {
|
|
5
12
|
return Object.freeze({
|
|
6
13
|
capturedEvents: state.capturedEvents,
|
|
7
14
|
captureFailures: state.captureFailures,
|
|
15
|
+
fingerprintCallbackFailures: state.fingerprintCallbackFailures,
|
|
16
|
+
filterFailures: state.filterFailures,
|
|
17
|
+
beforeSendFailures: state.beforeSendFailures,
|
|
18
|
+
eventsSuppressed: state.eventsSuppressed,
|
|
8
19
|
...values,
|
|
9
20
|
});
|
|
10
21
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeUnknown } from "./normalize.js";
|
|
2
|
+
import { MAX_EVENTS_PER_WINDOW, MAX_GROUPS } from "./limits.js";
|
|
2
3
|
import { createRollingWindow } from "./rolling-window.js";
|
|
3
4
|
const cloneSafeEvent = (event) => {
|
|
4
5
|
const context = event.context === undefined ? undefined : normalizeUnknown(event.context);
|
|
@@ -7,16 +8,29 @@ const cloneSafeEvent = (event) => {
|
|
|
7
8
|
timestamp: event.timestamp,
|
|
8
9
|
service: event.service,
|
|
9
10
|
environment: event.environment,
|
|
11
|
+
...(event.instance === undefined ? {} : { instance: event.instance }),
|
|
10
12
|
...(event.release === undefined ? {} : { release: event.release }),
|
|
13
|
+
...(event.correlationId === undefined ? {} : { correlationId: event.correlationId }),
|
|
14
|
+
...(event.operation === undefined ? {} : { operation: event.operation }),
|
|
15
|
+
...(event.job === undefined ? {} : { job: event.job }),
|
|
16
|
+
...(event.tags === undefined ? {} : { tags: { ...event.tags } }),
|
|
11
17
|
error: { ...event.error },
|
|
12
18
|
...(event.request === undefined ? {} : { request: { ...event.request } }),
|
|
19
|
+
...(event.trace === undefined ? {} : { trace: { ...event.trace } }),
|
|
13
20
|
...(context === undefined ? {} : { context: context }),
|
|
14
21
|
};
|
|
15
22
|
};
|
|
16
23
|
export function createGroupStore(options) {
|
|
17
|
-
if (!Number.isSafeInteger(options.maxGroups) ||
|
|
24
|
+
if (!Number.isSafeInteger(options.maxGroups) ||
|
|
25
|
+
options.maxGroups <= 0 ||
|
|
26
|
+
options.maxGroups > MAX_GROUPS) {
|
|
18
27
|
throw new RangeError("maxGroups must be a positive integer");
|
|
19
28
|
}
|
|
29
|
+
if (!Number.isSafeInteger(options.maxEventsPerWindow) ||
|
|
30
|
+
options.maxEventsPerWindow <= 0 ||
|
|
31
|
+
options.maxEventsPerWindow > MAX_EVENTS_PER_WINDOW) {
|
|
32
|
+
throw new RangeError("maxEventsPerWindow must be a positive integer");
|
|
33
|
+
}
|
|
20
34
|
const now = options.now ?? Date.now;
|
|
21
35
|
const groups = new Map();
|
|
22
36
|
let evicted = 0;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { selectApplicationFrame } from "./stack-frame.js";
|
|
1
2
|
const titleCase = (value) => `${value.slice(0, 1).toUpperCase()}${value.slice(1)}`;
|
|
2
3
|
const suggestedActions = (group) => {
|
|
3
4
|
const text = `${group.sample.error.name} ${group.sample.error.message}`.toLowerCase();
|
|
@@ -27,6 +28,11 @@ const suggestedActions = (group) => {
|
|
|
27
28
|
export function buildIncidentAlert(group, decision) {
|
|
28
29
|
const message = group.sample.error.message;
|
|
29
30
|
const errorName = group.sample.error.name;
|
|
31
|
+
const applicationFrame = selectApplicationFrame(group.sample.error.stack);
|
|
32
|
+
const error = {
|
|
33
|
+
...group.sample.error,
|
|
34
|
+
...(applicationFrame === undefined ? {} : { applicationFrame }),
|
|
35
|
+
};
|
|
30
36
|
return {
|
|
31
37
|
id: `wotchi-${group.fingerprint}-${group.lastSeenAt}`,
|
|
32
38
|
fingerprint: group.fingerprint,
|
|
@@ -39,5 +45,17 @@ export function buildIncidentAlert(group, decision) {
|
|
|
39
45
|
occurrences: group.windowCount,
|
|
40
46
|
service: group.sample.service,
|
|
41
47
|
environment: group.sample.environment,
|
|
48
|
+
...(group.sample.instance === undefined ? {} : { instance: group.sample.instance }),
|
|
49
|
+
...(group.sample.release === undefined ? {} : { release: group.sample.release }),
|
|
50
|
+
...(group.sample.correlationId === undefined
|
|
51
|
+
? {}
|
|
52
|
+
: { correlationId: group.sample.correlationId }),
|
|
53
|
+
...(group.sample.operation === undefined ? {} : { operation: group.sample.operation }),
|
|
54
|
+
...(group.sample.job === undefined ? {} : { job: group.sample.job }),
|
|
55
|
+
...(group.sample.tags === undefined ? {} : { tags: group.sample.tags }),
|
|
56
|
+
error,
|
|
57
|
+
...(group.sample.request === undefined ? {} : { request: group.sample.request }),
|
|
58
|
+
...(group.sample.trace === undefined ? {} : { trace: group.sample.trace }),
|
|
59
|
+
...(group.sample.context === undefined ? {} : { context: group.sample.context }),
|
|
42
60
|
};
|
|
43
61
|
}
|
|
@@ -10,13 +10,8 @@ export function evaluateIncidentPolicy(input) {
|
|
|
10
10
|
const eventKind = input.eventKind ?? "error";
|
|
11
11
|
const isCritical = eventKind === "process-monitor";
|
|
12
12
|
const isHigh = input.group.windowCount >= 20 || hasUnavailableResponse(input.group);
|
|
13
|
-
const severity =
|
|
14
|
-
? "critical"
|
|
15
|
-
: isHigh
|
|
16
|
-
? "high"
|
|
17
|
-
: eventKind === "manual"
|
|
18
|
-
? (input.manualSeverity ?? "low")
|
|
19
|
-
: "medium";
|
|
13
|
+
const severity = input.manualSeverity ??
|
|
14
|
+
(isCritical ? "critical" : isHigh ? "high" : eventKind === "manual" ? "low" : "medium");
|
|
20
15
|
const eligible = isCritical ||
|
|
21
16
|
isHigh ||
|
|
22
17
|
eventKind === "manual" ||
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const MAX_GROUPS = 10_000;
|
|
2
|
+
export const MAX_EVENTS_PER_WINDOW = 10_000;
|
|
3
|
+
export const MAX_PENDING_ALERTS = 10_000;
|
|
4
|
+
export const MAX_WINDOW_MS = 7 * 24 * 60 * 60 * 1_000;
|
|
5
|
+
export const MAX_ALERT_THRESHOLD = 1_000_000;
|
|
6
|
+
export const MAX_COOLDOWN_MS = 30 * 24 * 60 * 60 * 1_000;
|
|
7
|
+
export const MAX_NORMALIZATION_DEPTH = 20;
|
|
8
|
+
export const MAX_NORMALIZATION_KEYS = 10_000;
|
|
9
|
+
export const MAX_NORMALIZATION_STRING_LENGTH = 32_768;
|
|
10
|
+
export const MAX_NORMALIZATION_STACK_LENGTH = 32_768;
|
|
@@ -2,18 +2,19 @@ const UNREADABLE_VALUE = "[unreadable value]";
|
|
|
2
2
|
const MAX_DEPTH_VALUE = "[MaxDepth]";
|
|
3
3
|
const CIRCULAR_VALUE = "[Circular]";
|
|
4
4
|
const MAX_KEYS_VALUE = "[MaxKeys]";
|
|
5
|
+
const UNSAFE_OBJECT_KEYS = new Set(["__proto__", "prototype", "constructor"]);
|
|
5
6
|
const defaultLimits = {
|
|
6
7
|
maxDepth: 5,
|
|
7
8
|
maxKeys: 100,
|
|
8
9
|
maxStringLength: 500,
|
|
9
10
|
maxStackLength: 4_000,
|
|
10
11
|
};
|
|
11
|
-
const boundedLimit = (value, fallback) => Number.isSafeInteger(value) && value > 0 ? value : fallback;
|
|
12
|
+
const boundedLimit = (value, fallback, maximum) => Number.isSafeInteger(value) && value > 0 && value <= maximum ? value : fallback;
|
|
12
13
|
const normalizeLimits = (limits) => ({
|
|
13
|
-
maxDepth: boundedLimit(limits?.maxDepth ?? defaultLimits.maxDepth, defaultLimits.maxDepth),
|
|
14
|
-
maxKeys: boundedLimit(limits?.maxKeys ?? defaultLimits.maxKeys, defaultLimits.maxKeys),
|
|
15
|
-
maxStringLength: boundedLimit(limits?.maxStringLength ?? defaultLimits.maxStringLength, defaultLimits.maxStringLength),
|
|
16
|
-
maxStackLength: boundedLimit(limits?.maxStackLength ?? defaultLimits.maxStackLength, defaultLimits.maxStackLength),
|
|
14
|
+
maxDepth: boundedLimit(limits?.maxDepth ?? defaultLimits.maxDepth, defaultLimits.maxDepth, MAX_NORMALIZATION_DEPTH),
|
|
15
|
+
maxKeys: boundedLimit(limits?.maxKeys ?? defaultLimits.maxKeys, defaultLimits.maxKeys, MAX_NORMALIZATION_KEYS),
|
|
16
|
+
maxStringLength: boundedLimit(limits?.maxStringLength ?? defaultLimits.maxStringLength, defaultLimits.maxStringLength, MAX_NORMALIZATION_STRING_LENGTH),
|
|
17
|
+
maxStackLength: boundedLimit(limits?.maxStackLength ?? defaultLimits.maxStackLength, defaultLimits.maxStackLength, MAX_NORMALIZATION_STACK_LENGTH),
|
|
17
18
|
});
|
|
18
19
|
const truncate = (value, limit) => value.slice(0, limit);
|
|
19
20
|
const readProperty = (value, key) => {
|
|
@@ -85,6 +86,9 @@ const normalizeValue = (value, depth, state, limits) => {
|
|
|
85
86
|
}
|
|
86
87
|
const result = {};
|
|
87
88
|
for (const key of keys) {
|
|
89
|
+
if (UNSAFE_OBJECT_KEYS.has(key)) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
88
92
|
if (state.keysVisited >= limits.maxKeys - 1) {
|
|
89
93
|
result["[truncated]"] = MAX_KEYS_VALUE;
|
|
90
94
|
break;
|
|
@@ -132,3 +136,4 @@ export function normalizeError(error, limits) {
|
|
|
132
136
|
const message = typeof normalized === "string" ? normalized : UNREADABLE_VALUE;
|
|
133
137
|
return { name: "UnknownError", message };
|
|
134
138
|
}
|
|
139
|
+
import { MAX_NORMALIZATION_DEPTH, MAX_NORMALIZATION_KEYS, MAX_NORMALIZATION_STACK_LENGTH, MAX_NORMALIZATION_STRING_LENGTH, } from "./limits.js";
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { MAX_PENDING_ALERTS } from "./limits.js";
|
|
1
2
|
const DEFAULT_FLUSH_TIMEOUT_MS = 5_000;
|
|
2
3
|
export function createNotificationQueue(options) {
|
|
3
|
-
if (!Number.isSafeInteger(options.maxPendingAlerts) ||
|
|
4
|
+
if (!Number.isSafeInteger(options.maxPendingAlerts) ||
|
|
5
|
+
options.maxPendingAlerts <= 0 ||
|
|
6
|
+
options.maxPendingAlerts > MAX_PENDING_ALERTS) {
|
|
4
7
|
throw new RangeError("maxPendingAlerts must be a positive integer");
|
|
5
8
|
}
|
|
6
9
|
if (options.concurrency !== 1) {
|
|
@@ -32,15 +35,25 @@ export function createNotificationQueue(options) {
|
|
|
32
35
|
resolveDrain = undefined;
|
|
33
36
|
};
|
|
34
37
|
const processJob = async (job) => {
|
|
38
|
+
let jobSent = 0;
|
|
39
|
+
let jobFailures = 0;
|
|
35
40
|
for (const notifier of job.notifiers) {
|
|
36
41
|
try {
|
|
37
42
|
await notifier.send(job.alert);
|
|
38
43
|
sent += 1;
|
|
44
|
+
jobSent += 1;
|
|
39
45
|
}
|
|
40
46
|
catch (error) {
|
|
47
|
+
jobFailures += 1;
|
|
41
48
|
notifyFailure(error, notifier);
|
|
42
49
|
}
|
|
43
50
|
}
|
|
51
|
+
try {
|
|
52
|
+
job.onComplete?.({ notifierFailures: jobFailures, sent: jobSent });
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Job observers cannot be allowed to affect notification processing.
|
|
56
|
+
}
|
|
44
57
|
};
|
|
45
58
|
const pump = () => {
|
|
46
59
|
while (activeJobs < options.concurrency && pendingJobs.length > 0) {
|
|
@@ -56,12 +69,16 @@ export function createNotificationQueue(options) {
|
|
|
56
69
|
});
|
|
57
70
|
}
|
|
58
71
|
};
|
|
59
|
-
const enqueue = (alert, notifiers) => {
|
|
72
|
+
const enqueue = (alert, notifiers, onComplete) => {
|
|
60
73
|
if (pendingJobs.length >= options.maxPendingAlerts) {
|
|
61
74
|
dropped += 1;
|
|
62
75
|
return false;
|
|
63
76
|
}
|
|
64
|
-
pendingJobs.push({
|
|
77
|
+
pendingJobs.push({
|
|
78
|
+
alert,
|
|
79
|
+
notifiers: [...notifiers],
|
|
80
|
+
...(onComplete === undefined ? {} : { onComplete }),
|
|
81
|
+
});
|
|
65
82
|
queued += 1;
|
|
66
83
|
pump();
|
|
67
84
|
return true;
|
package/dist/esm/core/redact.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeUnknown } from "./normalize.js";
|
|
2
|
+
import { MAX_NORMALIZATION_DEPTH, MAX_NORMALIZATION_KEYS, MAX_NORMALIZATION_STACK_LENGTH, MAX_NORMALIZATION_STRING_LENGTH, } from "./limits.js";
|
|
2
3
|
export const REDACTED = "[REDACTED]";
|
|
3
4
|
const DEFAULT_KEYS = [
|
|
4
5
|
"authorization",
|
|
@@ -9,6 +10,12 @@ const DEFAULT_KEYS = [
|
|
|
9
10
|
"secret",
|
|
10
11
|
"apiKey",
|
|
11
12
|
"api_key",
|
|
13
|
+
"xApiKey",
|
|
14
|
+
"x_api_key",
|
|
15
|
+
"dbPassword",
|
|
16
|
+
"db_password",
|
|
17
|
+
"databasePassword",
|
|
18
|
+
"database_password",
|
|
12
19
|
"token",
|
|
13
20
|
"accessToken",
|
|
14
21
|
"access_token",
|
|
@@ -18,12 +25,16 @@ const DEFAULT_KEYS = [
|
|
|
18
25
|
"private_key",
|
|
19
26
|
"clientSecret",
|
|
20
27
|
"client_secret",
|
|
28
|
+
"signature",
|
|
29
|
+
"session",
|
|
30
|
+
"webhookSecret",
|
|
31
|
+
"webhook_secret",
|
|
21
32
|
"cardNumber",
|
|
22
33
|
"cvv",
|
|
23
34
|
"cvc",
|
|
24
35
|
"pin",
|
|
25
36
|
];
|
|
26
|
-
const normalizeKey = (key) => key.toLowerCase().replace(/[-
|
|
37
|
+
const normalizeKey = (key) => key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
27
38
|
const defaultKeySet = new Set(DEFAULT_KEYS.map(normalizeKey));
|
|
28
39
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
29
40
|
const luhnValid = (digits) => {
|
|
@@ -72,11 +83,44 @@ const redactConnectionUrl = (candidate) => {
|
|
|
72
83
|
return `${core.slice(0, authorityStart)}${redactedAuthority}${remainder}${suffix}`;
|
|
73
84
|
};
|
|
74
85
|
const redactConnectionUrls = (value) => value.replace(CONNECTION_URL_PATTERN, redactConnectionUrl);
|
|
86
|
+
const SENSITIVE_QUERY_KEYS = new Set([
|
|
87
|
+
"authorization",
|
|
88
|
+
"cookie",
|
|
89
|
+
"password",
|
|
90
|
+
"passwd",
|
|
91
|
+
"secret",
|
|
92
|
+
"signature",
|
|
93
|
+
"token",
|
|
94
|
+
"accesstoken",
|
|
95
|
+
"refreshtoken",
|
|
96
|
+
"apikey",
|
|
97
|
+
"xapikey",
|
|
98
|
+
"session",
|
|
99
|
+
]);
|
|
100
|
+
const decodeQueryKey = (value) => {
|
|
101
|
+
try {
|
|
102
|
+
return decodeURIComponent(value.replace(/\+/g, " "));
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const redactSensitiveQueryValues = (value) => value.replace(/([?&])([^=&#\s]+)=([^&#\s]*)/g, (match, prefix, key) => {
|
|
109
|
+
if (!SENSITIVE_QUERY_KEYS.has(normalizeKey(decodeQueryKey(key)))) {
|
|
110
|
+
return match;
|
|
111
|
+
}
|
|
112
|
+
return prefix + key + "=" + REDACTED;
|
|
113
|
+
});
|
|
114
|
+
const redactWebhookPathSecrets = (value) => value.replace(/(https?:\/\/[^/\s]+\/(?:services|hooks|webhooks?)\/)[^/?#\s]+/gi, (_match, prefix) => prefix + REDACTED);
|
|
75
115
|
const redactString = (value, maxStringLength) => {
|
|
76
116
|
let result = redactConnectionUrls(value);
|
|
117
|
+
result = redactWebhookPathSecrets(result);
|
|
118
|
+
result = redactSensitiveQueryValues(result);
|
|
77
119
|
result = result.replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi, REDACTED);
|
|
78
120
|
result = result.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, `Bearer ${REDACTED}`);
|
|
79
|
-
result = result.replace(/\
|
|
121
|
+
result = result.replace(/\bBasic\s+[A-Za-z0-9+/=]+/gi, `Basic ${REDACTED}`);
|
|
122
|
+
result = result.replace(/\b(?:Cookie|Set-Cookie)\s*:\s*[^\r\n]+/gi, "Cookie: [REDACTED]");
|
|
123
|
+
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) => {
|
|
80
124
|
const separatorIndex = match.search(/[:=]/);
|
|
81
125
|
return separatorIndex < 0 ? REDACTED : `${match.slice(0, separatorIndex + 1)}${REDACTED}`;
|
|
82
126
|
});
|
|
@@ -85,7 +129,9 @@ const redactString = (value, maxStringLength) => {
|
|
|
85
129
|
result = redactCardLikeValues(result);
|
|
86
130
|
return result.slice(0, maxStringLength);
|
|
87
131
|
};
|
|
88
|
-
const normalizedOption = (value, fallback) => Number.isSafeInteger(value) && value !== undefined && value > 0
|
|
132
|
+
const normalizedOption = (value, fallback, maximum) => Number.isSafeInteger(value) && value !== undefined && value > 0 && value <= maximum
|
|
133
|
+
? value
|
|
134
|
+
: fallback;
|
|
89
135
|
const redactNormalized = (value, sensitiveKeys, options, depth, keysVisited) => {
|
|
90
136
|
if (typeof value === "string") {
|
|
91
137
|
return redactString(value, options.maxStringLength);
|
|
@@ -123,10 +169,10 @@ const redactNormalized = (value, sensitiveKeys, options, depth, keysVisited) =>
|
|
|
123
169
|
return result;
|
|
124
170
|
};
|
|
125
171
|
const redactionLimits = (options) => ({
|
|
126
|
-
maxDepth: normalizedOption(options.maxDepth, 5),
|
|
127
|
-
maxKeys: normalizedOption(options.maxKeys, 100),
|
|
128
|
-
maxStringLength: normalizedOption(options.maxStringLength, 500),
|
|
129
|
-
maxStackLength: normalizedOption(options.maxStackLength, 4_000),
|
|
172
|
+
maxDepth: normalizedOption(options.maxDepth, 5, MAX_NORMALIZATION_DEPTH),
|
|
173
|
+
maxKeys: normalizedOption(options.maxKeys, 100, MAX_NORMALIZATION_KEYS),
|
|
174
|
+
maxStringLength: normalizedOption(options.maxStringLength, 500, MAX_NORMALIZATION_STRING_LENGTH),
|
|
175
|
+
maxStackLength: normalizedOption(options.maxStackLength, 4_000, MAX_NORMALIZATION_STACK_LENGTH),
|
|
130
176
|
});
|
|
131
177
|
const sensitiveKeySet = (options) => {
|
|
132
178
|
const keys = new Set(defaultKeySet);
|
package/dist/esm/index.js
CHANGED
|
@@ -2,5 +2,6 @@ import { createWotchi } from "./core/client.js";
|
|
|
2
2
|
import { registerWotchiProcessMonitor } from "./core/process-monitor.js";
|
|
3
3
|
import { consoleNotifier } from "./notifiers/console.js";
|
|
4
4
|
import { telegramNotifier } from "./notifiers/telegram.js";
|
|
5
|
+
import { webhookNotifier } from "./notifiers/webhook.js";
|
|
5
6
|
export { WotchiConfigurationError } from "./core/errors.js";
|
|
6
|
-
export { consoleNotifier, createWotchi, registerWotchiProcessMonitor, telegramNotifier };
|
|
7
|
+
export { consoleNotifier, createWotchi, registerWotchiProcessMonitor, telegramNotifier, webhookNotifier, };
|
|
@@ -4,7 +4,8 @@ export function createExpressErrorHandler(client, options) {
|
|
|
4
4
|
return (error, request, response, next) => {
|
|
5
5
|
markExpressErrorCaptured(request);
|
|
6
6
|
try {
|
|
7
|
-
|
|
7
|
+
const requestContext = getExpressRequestContext(request, response, options);
|
|
8
|
+
client.captureException(error, undefined, requestContext === undefined ? undefined : { request: requestContext });
|
|
8
9
|
}
|
|
9
10
|
catch {
|
|
10
11
|
// A capture failure must never change Express error handling.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createExpressErrorHandler } from "./error-handler.js";
|
|
2
|
-
export { consoleNotifier, createWotchi, telegramNotifier } from "../../index.js";
|
|
2
|
+
export { consoleNotifier, createWotchi, telegramNotifier, webhookNotifier } from "../../index.js";
|
|
3
3
|
export { wotchiStatusObserver } from "./status-observer.js";
|
|
4
4
|
export function wotchiErrorHandler(client, options) {
|
|
5
5
|
return createExpressErrorHandler(client, options);
|
|
@@ -39,6 +39,12 @@ const normalizeOptions = (options) => {
|
|
|
39
39
|
...(options?.requestIdProperty === undefined
|
|
40
40
|
? {}
|
|
41
41
|
: { requestIdProperty: options.requestIdProperty }),
|
|
42
|
+
...(options?.correlationIdProperty === undefined
|
|
43
|
+
? {}
|
|
44
|
+
: { correlationIdProperty: options.correlationIdProperty }),
|
|
45
|
+
...(options?.traceContextProperty === undefined
|
|
46
|
+
? {}
|
|
47
|
+
: { traceContextProperty: options.traceContextProperty }),
|
|
42
48
|
statusCodes: normalizeStatuses(options?.statusCodes, DEFAULT_STATUS_CODES),
|
|
43
49
|
statusClasses: normalizeStatusClasses(options?.statusClasses),
|
|
44
50
|
ignoreStatusCodes: normalizeStatuses(options?.ignoreStatusCodes, []),
|
|
@@ -9,6 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
9
9
|
};
|
|
10
10
|
import { Catch } from "@nestjs/common";
|
|
11
11
|
import { BaseExceptionFilter } from "@nestjs/core";
|
|
12
|
+
import { markExpressErrorCaptured } from "../express/state.js";
|
|
12
13
|
import { getNestRequestContext } from "./request-context.js";
|
|
13
14
|
let WotchiNestExceptionFilter = class WotchiNestExceptionFilter extends BaseExceptionFilter {
|
|
14
15
|
constructor(client, applicationRef, options) {
|
|
@@ -18,7 +19,14 @@ let WotchiNestExceptionFilter = class WotchiNestExceptionFilter extends BaseExce
|
|
|
18
19
|
}
|
|
19
20
|
catch(exception, host) {
|
|
20
21
|
try {
|
|
21
|
-
|
|
22
|
+
markExpressErrorCaptured(host.switchToHttp().getRequest());
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Non-HTTP NestJS contexts do not expose an Express request to mark.
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const requestContext = getNestRequestContext(host, exception, this.options);
|
|
29
|
+
this.client.captureException(exception, undefined, requestContext === undefined ? undefined : { request: requestContext });
|
|
22
30
|
}
|
|
23
31
|
catch {
|
|
24
32
|
// A capture failure must never change NestJS exception handling.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { consoleNotifier, createWotchi, telegramNotifier } from "../../index.js";
|
|
1
|
+
export { consoleNotifier, createWotchi, telegramNotifier, webhookNotifier } from "../../index.js";
|
|
2
2
|
export { registerWotchiNest, registerWotchiNestStatusObserver } from "./register.js";
|