@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
@@ -1,5 +1,6 @@
1
1
  const MAX_ROUTE_LENGTH = 500;
2
2
  const MAX_REQUEST_ID_LENGTH = 200;
3
+ const MAX_CORRELATION_ID_LENGTH = 200;
3
4
  const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
4
5
  const HEX_PATTERN = /^[0-9a-f]{8,}$/i;
5
6
  const isRecord = (value) => typeof value === "object" && value !== null;
@@ -28,6 +29,24 @@ const normalizeRequestIdProperty = (property) => {
28
29
  }
29
30
  return property;
30
31
  };
32
+ const normalizeTraceContextProperty = (property) => {
33
+ if (property === undefined) {
34
+ return undefined;
35
+ }
36
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(property)) {
37
+ throw new TypeError("traceContextProperty must be a simple property name");
38
+ }
39
+ return property;
40
+ };
41
+ const normalizeCorrelationIdProperty = (property) => {
42
+ if (property === undefined) {
43
+ return undefined;
44
+ }
45
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(property)) {
46
+ throw new TypeError("correlationIdProperty must be a simple property name");
47
+ }
48
+ return property;
49
+ };
31
50
  const readRequestId = (request, property) => {
32
51
  if (property === undefined || !isRecord(request)) {
33
52
  return undefined;
@@ -43,8 +62,51 @@ const readRequestId = (request, property) => {
43
62
  return undefined;
44
63
  }
45
64
  };
65
+ const readTraceContext = (request, property) => {
66
+ if (property === undefined || !isRecord(request)) {
67
+ return undefined;
68
+ }
69
+ try {
70
+ if (!Object.prototype.hasOwnProperty.call(request, property)) {
71
+ return undefined;
72
+ }
73
+ const value = request[property];
74
+ if (!isRecord(value)) {
75
+ return undefined;
76
+ }
77
+ const traceId = typeof value.traceId === "string" ? value.traceId.slice(0, 128) : undefined;
78
+ const spanId = typeof value.spanId === "string" ? value.spanId.slice(0, 128) : undefined;
79
+ if (traceId === undefined && spanId === undefined) {
80
+ return undefined;
81
+ }
82
+ return {
83
+ ...(traceId === undefined ? {} : { traceId }),
84
+ ...(spanId === undefined ? {} : { spanId }),
85
+ };
86
+ }
87
+ catch {
88
+ return undefined;
89
+ }
90
+ };
91
+ const readCorrelationId = (request, property) => {
92
+ if (property === undefined || !isRecord(request)) {
93
+ return undefined;
94
+ }
95
+ try {
96
+ if (!Object.prototype.hasOwnProperty.call(request, property)) {
97
+ return undefined;
98
+ }
99
+ const value = request[property];
100
+ return typeof value === "string" ? value.slice(0, MAX_CORRELATION_ID_LENGTH) : undefined;
101
+ }
102
+ catch {
103
+ return undefined;
104
+ }
105
+ };
46
106
  export function buildRequestContext(input) {
47
107
  const requestIdProperty = normalizeRequestIdProperty(input.options?.requestIdProperty);
108
+ const correlationIdProperty = normalizeCorrelationIdProperty(input.options?.correlationIdProperty);
109
+ const traceContextProperty = normalizeTraceContextProperty(input.options?.traceContextProperty);
48
110
  let requestId;
49
111
  try {
50
112
  requestId = readRequestId(input.request, requestIdProperty);
@@ -52,6 +114,22 @@ export function buildRequestContext(input) {
52
114
  catch {
53
115
  requestId = undefined;
54
116
  }
117
+ let correlationId;
118
+ try {
119
+ correlationId = readCorrelationId(input.request, correlationIdProperty);
120
+ }
121
+ catch {
122
+ correlationId = undefined;
123
+ }
124
+ let trace;
125
+ try {
126
+ const explicitTrace = input.trace;
127
+ trace = readTraceContext({ trace: explicitTrace }, "trace");
128
+ trace ??= readTraceContext(input.request, traceContextProperty);
129
+ }
130
+ catch {
131
+ trace = undefined;
132
+ }
55
133
  const method = typeof input.method === "string" ? input.method.slice(0, 16) : undefined;
56
134
  const route = normalizeRoutePath(input.route);
57
135
  const statusCode = typeof input.statusCode === "number" && Number.isSafeInteger(input.statusCode)
@@ -60,7 +138,9 @@ export function buildRequestContext(input) {
60
138
  if (method === undefined &&
61
139
  route === undefined &&
62
140
  statusCode === undefined &&
63
- requestId === undefined) {
141
+ requestId === undefined &&
142
+ correlationId === undefined &&
143
+ trace === undefined) {
64
144
  return undefined;
65
145
  }
66
146
  return {
@@ -68,5 +148,7 @@ export function buildRequestContext(input) {
68
148
  ...(route === undefined ? {} : { route }),
69
149
  ...(statusCode === undefined ? {} : { statusCode }),
70
150
  ...(requestId === undefined ? {} : { requestId }),
151
+ ...(correlationId === undefined ? {} : { correlationId }),
152
+ ...(trace === undefined ? {} : { trace }),
71
153
  };
72
154
  }
@@ -0,0 +1,16 @@
1
+ import { redactValue } from "../core/redact.js";
2
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3
+ export function toBoundedPayload(value) {
4
+ const redacted = redactValue(value, {
5
+ maxDepth: 6,
6
+ maxKeys: 100,
7
+ maxStringLength: 1_000,
8
+ maxStackLength: 4_000,
9
+ });
10
+ return isRecord(redacted)
11
+ ? redacted
12
+ : { title: "Wotchi alert", summary: "Incident details were unavailable." };
13
+ }
14
+ export function toBoundedAlertPayload(alert) {
15
+ return toBoundedPayload(alert);
16
+ }
@@ -1,4 +1,5 @@
1
1
  import { redactValue } from "../core/redact.js";
2
+ import { toBoundedAlertPayload } from "./alert-payload.js";
2
3
  const MAX_FIELD_LENGTH = 1_000;
3
4
  const MAX_ACTIONS = 5;
4
5
  const limit = (value, maxLength = MAX_FIELD_LENGTH) => value.slice(0, maxLength);
@@ -6,26 +7,94 @@ const safeText = (value, maxLength = MAX_FIELD_LENGTH) => {
6
7
  const redacted = redactValue(value, { maxStringLength: maxLength });
7
8
  return typeof redacted === "string" ? limit(redacted, maxLength) : "[unreadable value]";
8
9
  };
9
- const boundedAlert = (alert) => ({
10
- title: safeText(alert.title),
11
- fingerprint: safeText(alert.fingerprint),
12
- severity: alert.severity,
13
- summary: safeText(alert.summary),
14
- suggestedActions: alert.suggestedActions
15
- .slice(0, MAX_ACTIONS)
16
- .map((action) => safeText(action, 300)),
17
- firstSeenAt: safeText(alert.firstSeenAt, 100),
18
- lastSeenAt: safeText(alert.lastSeenAt, 100),
19
- occurrences: Number.isSafeInteger(alert.occurrences) && alert.occurrences >= 0 ? alert.occurrences : 0,
20
- service: safeText(alert.service, 300),
21
- environment: safeText(alert.environment, 300),
22
- });
10
+ const safeJson = (value) => {
11
+ try {
12
+ return JSON.stringify(value);
13
+ }
14
+ catch {
15
+ return "[unreadable value]";
16
+ }
17
+ };
18
+ const boundedAlert = (alert) => {
19
+ const payload = toBoundedAlertPayload(alert);
20
+ return {
21
+ ...payload,
22
+ title: safeText(typeof payload.title === "string" ? payload.title : alert.title),
23
+ fingerprint: safeText(typeof payload.fingerprint === "string" ? payload.fingerprint : alert.fingerprint),
24
+ severity: alert.severity,
25
+ summary: safeText(typeof payload.summary === "string" ? payload.summary : alert.summary),
26
+ suggestedActions: alert.suggestedActions
27
+ .slice(0, MAX_ACTIONS)
28
+ .map((action) => safeText(action, 300)),
29
+ firstSeenAt: safeText(alert.firstSeenAt, 100),
30
+ lastSeenAt: safeText(alert.lastSeenAt, 100),
31
+ occurrences: Number.isSafeInteger(alert.occurrences) && alert.occurrences >= 0 ? alert.occurrences : 0,
32
+ service: safeText(alert.service, 300),
33
+ environment: safeText(alert.environment, 300),
34
+ };
35
+ };
36
+ const textDetails = (alert) => {
37
+ const lines = [];
38
+ if (alert.release !== undefined) {
39
+ lines.push(`Release: ${safeText(alert.release, 200)}`);
40
+ }
41
+ if (alert.instance !== undefined) {
42
+ lines.push(`Instance: ${safeText(alert.instance, 200)}`);
43
+ }
44
+ if (alert.operation !== undefined) {
45
+ lines.push(`Operation: ${safeText(alert.operation, 200)}`);
46
+ }
47
+ if (alert.job !== undefined) {
48
+ lines.push(`Job: ${safeText(alert.job, 200)}`);
49
+ }
50
+ if (alert.correlationId !== undefined) {
51
+ lines.push(`Correlation ID: ${safeText(alert.correlationId, 200)}`);
52
+ }
53
+ if (alert.tags !== undefined) {
54
+ lines.push(`Tags: ${safeText(safeJson(alert.tags), 600)}`);
55
+ }
56
+ if (alert.error !== undefined) {
57
+ lines.push(`Error: ${safeText(alert.error.name, 300)} — ${safeText(alert.error.message, 600)}`);
58
+ if (alert.error.applicationFrame !== undefined) {
59
+ lines.push(`Application frame: ${safeText(alert.error.applicationFrame, 500)}`);
60
+ }
61
+ }
62
+ if (alert.request !== undefined) {
63
+ const request = [alert.request.method, alert.request.route].filter((value) => typeof value === "string");
64
+ if (alert.request.statusCode !== undefined) {
65
+ request.push(`status ${alert.request.statusCode}`);
66
+ }
67
+ if (request.length > 0) {
68
+ lines.push(`Request: ${request.join(" ")}`);
69
+ }
70
+ if (alert.request.requestId !== undefined) {
71
+ lines.push(`Request ID: ${safeText(alert.request.requestId, 200)}`);
72
+ }
73
+ if (alert.request.correlationId !== undefined) {
74
+ lines.push(`Correlation ID: ${safeText(alert.request.correlationId, 200)}`);
75
+ }
76
+ }
77
+ const trace = alert.trace ?? alert.request?.trace;
78
+ if (trace?.traceId !== undefined || trace?.spanId !== undefined) {
79
+ lines.push(`Trace: ${safeText(trace.traceId ?? "", 128)}${trace.spanId === undefined ? "" : ` span ${safeText(trace.spanId, 128)}`}`);
80
+ }
81
+ if (alert.context !== undefined) {
82
+ lines.push(`Context: ${safeText(safeJson(alert.context), 1_000)}`);
83
+ }
84
+ if (alert.links !== undefined) {
85
+ lines.push(`Links: ${safeText(safeJson(alert.links), 600)}`);
86
+ }
87
+ return lines;
88
+ };
23
89
  export function formatConsoleAlert(alert, format = "text") {
24
90
  const bounded = boundedAlert(alert);
25
91
  if (format === "json") {
26
92
  return JSON.stringify(bounded);
27
93
  }
28
- const actions = bounded.suggestedActions.map((action) => `- ${action}`).join("\n");
94
+ const actions = alert.suggestedActions
95
+ .slice(0, MAX_ACTIONS)
96
+ .map((action) => `- ${safeText(action, 300)}`)
97
+ .join("\n");
29
98
  return [
30
99
  bounded.title,
31
100
  `Service: ${bounded.service}`,
@@ -34,6 +103,7 @@ export function formatConsoleAlert(alert, format = "text") {
34
103
  `Occurrences: ${bounded.occurrences}`,
35
104
  `First seen: ${bounded.firstSeenAt}`,
36
105
  `Last seen: ${bounded.lastSeenAt}`,
106
+ ...textDetails(alert),
37
107
  "Suggested checks:",
38
108
  actions,
39
109
  ].join("\n");
@@ -1,3 +1,4 @@
1
+ import { redactValue } from "../core/redact.js";
1
2
  const MAX_MESSAGE_LENGTH = 4_096;
2
3
  const MAX_ACTIONS = 5;
3
4
  const MAX_TITLE_LENGTH = 300;
@@ -5,9 +6,76 @@ const MAX_SERVICE_LENGTH = 300;
5
6
  const MAX_ENVIRONMENT_LENGTH = 300;
6
7
  const MAX_SUMMARY_LENGTH = 2_000;
7
8
  const MAX_ACTION_LENGTH = 600;
9
+ const MAX_DETAIL_LENGTH = 900;
8
10
  const escapeHtml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
9
11
  const limit = (value, maxLength) => value.slice(0, maxLength);
10
- const render = (title, service, environment, summary, actions, firstSeenAt, lastSeenAt, occurrences) => {
12
+ const safeText = (value, maxLength) => {
13
+ const redacted = redactValue(value, { maxStringLength: maxLength });
14
+ return typeof redacted === "string" ? limit(redacted, maxLength) : "[unreadable value]";
15
+ };
16
+ const safeJson = (value) => {
17
+ try {
18
+ return JSON.stringify(value);
19
+ }
20
+ catch {
21
+ return "[unreadable value]";
22
+ }
23
+ };
24
+ const details = (alert) => {
25
+ const lines = [];
26
+ if (alert.release !== undefined) {
27
+ lines.push(`Release: ${safeText(alert.release, 200)}`);
28
+ }
29
+ if (alert.instance !== undefined) {
30
+ lines.push(`Instance: ${safeText(alert.instance, 200)}`);
31
+ }
32
+ if (alert.operation !== undefined) {
33
+ lines.push(`Operation: ${safeText(alert.operation, 200)}`);
34
+ }
35
+ if (alert.job !== undefined) {
36
+ lines.push(`Job: ${safeText(alert.job, 200)}`);
37
+ }
38
+ if (alert.correlationId !== undefined) {
39
+ lines.push(`Correlation ID: ${safeText(alert.correlationId, 200)}`);
40
+ }
41
+ if (alert.tags !== undefined) {
42
+ lines.push(`Tags: ${safeText(safeJson(alert.tags), MAX_DETAIL_LENGTH)}`);
43
+ }
44
+ if (alert.error !== undefined) {
45
+ lines.push(`Error: ${safeText(alert.error.name, 250)} — ${safeText(alert.error.message, 500)}`);
46
+ if (alert.error.applicationFrame !== undefined) {
47
+ lines.push(`Application frame: ${safeText(alert.error.applicationFrame, 500)}`);
48
+ }
49
+ }
50
+ if (alert.request !== undefined) {
51
+ const request = [alert.request.method, alert.request.route].filter((value) => typeof value === "string");
52
+ if (alert.request.statusCode !== undefined) {
53
+ request.push(`status ${alert.request.statusCode}`);
54
+ }
55
+ if (request.length > 0) {
56
+ lines.push(`Request: ${request.join(" ")}`);
57
+ }
58
+ if (alert.request.requestId !== undefined) {
59
+ lines.push(`Request ID: ${safeText(alert.request.requestId, 200)}`);
60
+ }
61
+ if (alert.request.correlationId !== undefined) {
62
+ lines.push(`Correlation ID: ${safeText(alert.request.correlationId, 200)}`);
63
+ }
64
+ }
65
+ const trace = alert.trace ?? alert.request?.trace;
66
+ if (trace?.traceId !== undefined || trace?.spanId !== undefined) {
67
+ lines.push(`Trace: ${safeText(trace.traceId ?? "", 128)}${trace.spanId === undefined ? "" : ` span ${safeText(trace.spanId, 128)}`}`);
68
+ }
69
+ if (alert.context !== undefined) {
70
+ const redacted = redactValue(alert.context, { maxDepth: 4, maxKeys: 40, maxStringLength: 300 });
71
+ lines.push(`Context: ${safeText(safeJson(redacted), MAX_DETAIL_LENGTH)}`);
72
+ }
73
+ if (alert.links !== undefined) {
74
+ lines.push(`Links: ${safeText(safeJson(alert.links), MAX_DETAIL_LENGTH)}`);
75
+ }
76
+ return lines.map((line) => limit(line, MAX_DETAIL_LENGTH));
77
+ };
78
+ const render = (title, service, environment, summary, actions, context, firstSeenAt, lastSeenAt, occurrences) => {
11
79
  const actionText = actions.length === 0
12
80
  ? "- No suggested checks."
13
81
  : actions.map((action) => `- ${escapeHtml(action)}`).join("\n");
@@ -19,6 +87,7 @@ const render = (title, service, environment, summary, actions, firstSeenAt, last
19
87
  `Occurrences: ${occurrences}`,
20
88
  `First seen: ${escapeHtml(firstSeenAt)}`,
21
89
  `Last seen: ${escapeHtml(lastSeenAt)}`,
90
+ ...context.map((line) => escapeHtml(line)),
22
91
  "Suggested checks:",
23
92
  actionText,
24
93
  ].join("\n");
@@ -33,17 +102,18 @@ export function formatTelegramAlert(alert) {
33
102
  let actions = alert.suggestedActions
34
103
  .slice(0, MAX_ACTIONS)
35
104
  .map((action) => limit(action, MAX_ACTION_LENGTH));
36
- let formatted = render(title, service, environment, summary, actions, firstSeenAt, lastSeenAt, alert.occurrences);
105
+ const context = details(alert);
106
+ let formatted = render(title, service, environment, summary, actions, context, firstSeenAt, lastSeenAt, alert.occurrences);
37
107
  while (formatted.length > MAX_MESSAGE_LENGTH && actions.length > 0) {
38
108
  actions = actions.slice(0, -1);
39
- formatted = render(title, service, environment, summary, actions, firstSeenAt, lastSeenAt, alert.occurrences);
109
+ formatted = render(title, service, environment, summary, actions, context, firstSeenAt, lastSeenAt, alert.occurrences);
40
110
  }
41
111
  while (formatted.length > MAX_MESSAGE_LENGTH && summary.length > 0) {
42
112
  summary = summary.slice(0, Math.max(0, summary.length - 200));
43
- formatted = render(title, service, environment, summary, actions, firstSeenAt, lastSeenAt, alert.occurrences);
113
+ formatted = render(title, service, environment, summary, actions, context, firstSeenAt, lastSeenAt, alert.occurrences);
44
114
  }
45
115
  if (formatted.length > MAX_MESSAGE_LENGTH) {
46
- formatted = render(limit(title, 100), limit(service, 100), limit(environment, 100), "Incident details were truncated.", [], firstSeenAt, lastSeenAt, alert.occurrences);
116
+ formatted = render(limit(title, 100), limit(service, 100), limit(environment, 100), "Incident details were truncated.", [], [], firstSeenAt, lastSeenAt, alert.occurrences);
47
117
  }
48
118
  return formatted;
49
119
  }