@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
@@ -1,12 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- 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; } });
11
+ var status_observer_js_1 = require("./status-observer.js");
12
+ Object.defineProperty(exports, "wotchiStatusObserver", { enumerable: true, get: function () { return status_observer_js_1.wotchiStatusObserver; } });
10
13
  function wotchiErrorHandler(client, options) {
11
14
  return (0, error_handler_js_1.createExpressErrorHandler)(client, options);
12
15
  }
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.markExpressErrorCaptured = markExpressErrorCaptured;
4
+ exports.wasExpressErrorCaptured = wasExpressErrorCaptured;
5
+ const WOTCHI_ERROR_CAPTURED = Symbol("wotchi.errorCaptured");
6
+ const asMarkedRequest = (request) => typeof request === "object" && request !== null ? request : undefined;
7
+ function markExpressErrorCaptured(request) {
8
+ const markedRequest = asMarkedRequest(request);
9
+ if (markedRequest === undefined) {
10
+ return;
11
+ }
12
+ try {
13
+ markedRequest[WOTCHI_ERROR_CAPTURED] = true;
14
+ }
15
+ catch {
16
+ // A request object can be frozen by host middleware; the observer remains best-effort.
17
+ }
18
+ }
19
+ function wasExpressErrorCaptured(request) {
20
+ const markedRequest = asMarkedRequest(request);
21
+ if (markedRequest === undefined) {
22
+ return false;
23
+ }
24
+ try {
25
+ return markedRequest[WOTCHI_ERROR_CAPTURED] === true;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.wotchiStatusObserver = wotchiStatusObserver;
4
+ const request_context_js_1 = require("./request-context.js");
5
+ const state_js_1 = require("./state.js");
6
+ const DEFAULT_STATUS_CODES = [401, 403, 429];
7
+ const DEFAULT_STATUS_CLASSES = ["5xx"];
8
+ const MAX_STATUS_CODES = 100;
9
+ const normalizeStatuses = (values, defaults) => {
10
+ const selected = values ?? defaults;
11
+ if (selected.length > MAX_STATUS_CODES) {
12
+ throw new TypeError("statusCodes must contain at most 100 values");
13
+ }
14
+ for (const status of selected) {
15
+ if (!Number.isSafeInteger(status) || status < 100 || status > 599) {
16
+ throw new TypeError("statusCodes must contain HTTP status codes from 100 through 599");
17
+ }
18
+ }
19
+ return new Set(selected);
20
+ };
21
+ const normalizeStatusClasses = (values) => {
22
+ const selected = values ?? DEFAULT_STATUS_CLASSES;
23
+ for (const statusClass of selected) {
24
+ if (statusClass !== "4xx" && statusClass !== "5xx") {
25
+ throw new TypeError('statusClasses must contain only "4xx" or "5xx"');
26
+ }
27
+ }
28
+ return new Set(selected);
29
+ };
30
+ const normalizeAlertThreshold = (value) => {
31
+ if (value === undefined) {
32
+ return undefined;
33
+ }
34
+ if (!Number.isSafeInteger(value) || value <= 0 || value > 1_000_000) {
35
+ throw new TypeError("alertThreshold must be a positive integer no greater than 1000000");
36
+ }
37
+ return value;
38
+ };
39
+ const normalizeOptions = (options) => {
40
+ const alertThreshold = normalizeAlertThreshold(options?.alertThreshold);
41
+ return {
42
+ ...(options?.requestIdProperty === undefined
43
+ ? {}
44
+ : { requestIdProperty: options.requestIdProperty }),
45
+ ...(options?.correlationIdProperty === undefined
46
+ ? {}
47
+ : { correlationIdProperty: options.correlationIdProperty }),
48
+ ...(options?.traceContextProperty === undefined
49
+ ? {}
50
+ : { traceContextProperty: options.traceContextProperty }),
51
+ statusCodes: normalizeStatuses(options?.statusCodes, DEFAULT_STATUS_CODES),
52
+ statusClasses: normalizeStatusClasses(options?.statusClasses),
53
+ ignoreStatusCodes: normalizeStatuses(options?.ignoreStatusCodes, []),
54
+ ...(alertThreshold === undefined ? {} : { alertThreshold }),
55
+ };
56
+ };
57
+ const isObservedStatus = (statusCode, options) => !options.ignoreStatusCodes.has(statusCode) &&
58
+ (options.statusCodes.has(statusCode) ||
59
+ (statusCode >= 400 && statusCode < 500 && options.statusClasses.has("4xx")) ||
60
+ (statusCode >= 500 && statusCode < 600 && options.statusClasses.has("5xx")));
61
+ function wotchiStatusObserver(client, options) {
62
+ const normalizedOptions = normalizeOptions(options);
63
+ return (request, response, next) => {
64
+ response.once("finish", () => {
65
+ const statusCode = response.statusCode;
66
+ if ((0, state_js_1.wasExpressErrorCaptured)(request) || !isObservedStatus(statusCode, normalizedOptions)) {
67
+ return;
68
+ }
69
+ try {
70
+ const requestContext = (0, request_context_js_1.getExpressRequestContext)(request, response, normalizedOptions);
71
+ client.captureEvent({
72
+ level: "error",
73
+ message: `HTTP ${statusCode} response`,
74
+ ...(normalizedOptions.alertThreshold === undefined
75
+ ? {}
76
+ : { alertThreshold: normalizedOptions.alertThreshold }),
77
+ ...(requestContext === undefined ? {} : { request: requestContext }),
78
+ });
79
+ }
80
+ catch {
81
+ // Status observation must never change the host response lifecycle.
82
+ }
83
+ });
84
+ next();
85
+ };
86
+ }
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.WotchiNestExceptionFilter = void 0;
13
13
  const common_1 = require("@nestjs/common");
14
14
  const core_1 = require("@nestjs/core");
15
+ const state_js_1 = require("../express/state.js");
15
16
  const request_context_js_1 = require("./request-context.js");
16
17
  let WotchiNestExceptionFilter = class WotchiNestExceptionFilter extends core_1.BaseExceptionFilter {
17
18
  constructor(client, applicationRef, options) {
@@ -21,7 +22,14 @@ let WotchiNestExceptionFilter = class WotchiNestExceptionFilter extends core_1.B
21
22
  }
22
23
  catch(exception, host) {
23
24
  try {
24
- this.client.captureException(exception, (0, request_context_js_1.getNestRequestContext)(host, exception, this.options));
25
+ (0, state_js_1.markExpressErrorCaptured)(host.switchToHttp().getRequest());
26
+ }
27
+ catch {
28
+ // Non-HTTP NestJS contexts do not expose an Express request to mark.
29
+ }
30
+ try {
31
+ const requestContext = (0, request_context_js_1.getNestRequestContext)(host, exception, this.options);
32
+ this.client.captureException(exception, undefined, requestContext === undefined ? undefined : { request: requestContext });
25
33
  }
26
34
  catch {
27
35
  // A capture failure must never change NestJS exception handling.
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.registerWotchiNest = exports.telegramNotifier = exports.createWotchi = exports.consoleNotifier = void 0;
3
+ exports.registerWotchiNestStatusObserver = exports.registerWotchiNest = exports.webhookNotifier = exports.telegramNotifier = exports.createWotchi = exports.consoleNotifier = void 0;
4
4
  var index_js_1 = require("../../index.js");
5
5
  Object.defineProperty(exports, "consoleNotifier", { enumerable: true, get: function () { return index_js_1.consoleNotifier; } });
6
6
  Object.defineProperty(exports, "createWotchi", { enumerable: true, get: function () { return index_js_1.createWotchi; } });
7
7
  Object.defineProperty(exports, "telegramNotifier", { enumerable: true, get: function () { return index_js_1.telegramNotifier; } });
8
+ Object.defineProperty(exports, "webhookNotifier", { enumerable: true, get: function () { return index_js_1.webhookNotifier; } });
8
9
  var register_js_1 = require("./register.js");
9
10
  Object.defineProperty(exports, "registerWotchiNest", { enumerable: true, get: function () { return register_js_1.registerWotchiNest; } });
11
+ Object.defineProperty(exports, "registerWotchiNestStatusObserver", { enumerable: true, get: function () { return register_js_1.registerWotchiNestStatusObserver; } });
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerWotchiNest = registerWotchiNest;
4
+ exports.registerWotchiNestStatusObserver = registerWotchiNestStatusObserver;
4
5
  const core_1 = require("@nestjs/core");
5
6
  const exception_filter_js_1 = require("./exception-filter.js");
7
+ const status_observer_js_1 = require("../express/status-observer.js");
6
8
  function registerWotchiNest(app, client, options) {
7
9
  const application = app;
8
10
  const directAdapter = application.getHttpAdapter?.();
@@ -11,3 +13,10 @@ function registerWotchiNest(app, client, options) {
11
13
  : directAdapter;
12
14
  application.useGlobalFilters(new exception_filter_js_1.WotchiNestExceptionFilter(client, httpAdapter, options));
13
15
  }
16
+ function registerWotchiNestStatusObserver(app, client, options) {
17
+ const application = app;
18
+ if (typeof application.use !== "function") {
19
+ throw new TypeError("Nest application does not expose use(); an Express-based adapter is required");
20
+ }
21
+ application.use((0, status_observer_js_1.wotchiStatusObserver)(client, options));
22
+ }
@@ -4,6 +4,7 @@ exports.normalizeRoutePath = normalizeRoutePath;
4
4
  exports.buildRequestContext = buildRequestContext;
5
5
  const MAX_ROUTE_LENGTH = 500;
6
6
  const MAX_REQUEST_ID_LENGTH = 200;
7
+ const MAX_CORRELATION_ID_LENGTH = 200;
7
8
  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;
8
9
  const HEX_PATTERN = /^[0-9a-f]{8,}$/i;
9
10
  const isRecord = (value) => typeof value === "object" && value !== null;
@@ -32,6 +33,24 @@ const normalizeRequestIdProperty = (property) => {
32
33
  }
33
34
  return property;
34
35
  };
36
+ const normalizeTraceContextProperty = (property) => {
37
+ if (property === undefined) {
38
+ return undefined;
39
+ }
40
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(property)) {
41
+ throw new TypeError("traceContextProperty must be a simple property name");
42
+ }
43
+ return property;
44
+ };
45
+ const normalizeCorrelationIdProperty = (property) => {
46
+ if (property === undefined) {
47
+ return undefined;
48
+ }
49
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(property)) {
50
+ throw new TypeError("correlationIdProperty must be a simple property name");
51
+ }
52
+ return property;
53
+ };
35
54
  const readRequestId = (request, property) => {
36
55
  if (property === undefined || !isRecord(request)) {
37
56
  return undefined;
@@ -47,8 +66,51 @@ const readRequestId = (request, property) => {
47
66
  return undefined;
48
67
  }
49
68
  };
69
+ const readTraceContext = (request, property) => {
70
+ if (property === undefined || !isRecord(request)) {
71
+ return undefined;
72
+ }
73
+ try {
74
+ if (!Object.prototype.hasOwnProperty.call(request, property)) {
75
+ return undefined;
76
+ }
77
+ const value = request[property];
78
+ if (!isRecord(value)) {
79
+ return undefined;
80
+ }
81
+ const traceId = typeof value.traceId === "string" ? value.traceId.slice(0, 128) : undefined;
82
+ const spanId = typeof value.spanId === "string" ? value.spanId.slice(0, 128) : undefined;
83
+ if (traceId === undefined && spanId === undefined) {
84
+ return undefined;
85
+ }
86
+ return {
87
+ ...(traceId === undefined ? {} : { traceId }),
88
+ ...(spanId === undefined ? {} : { spanId }),
89
+ };
90
+ }
91
+ catch {
92
+ return undefined;
93
+ }
94
+ };
95
+ const readCorrelationId = (request, property) => {
96
+ if (property === undefined || !isRecord(request)) {
97
+ return undefined;
98
+ }
99
+ try {
100
+ if (!Object.prototype.hasOwnProperty.call(request, property)) {
101
+ return undefined;
102
+ }
103
+ const value = request[property];
104
+ return typeof value === "string" ? value.slice(0, MAX_CORRELATION_ID_LENGTH) : undefined;
105
+ }
106
+ catch {
107
+ return undefined;
108
+ }
109
+ };
50
110
  function buildRequestContext(input) {
51
111
  const requestIdProperty = normalizeRequestIdProperty(input.options?.requestIdProperty);
112
+ const correlationIdProperty = normalizeCorrelationIdProperty(input.options?.correlationIdProperty);
113
+ const traceContextProperty = normalizeTraceContextProperty(input.options?.traceContextProperty);
52
114
  let requestId;
53
115
  try {
54
116
  requestId = readRequestId(input.request, requestIdProperty);
@@ -56,6 +118,22 @@ function buildRequestContext(input) {
56
118
  catch {
57
119
  requestId = undefined;
58
120
  }
121
+ let correlationId;
122
+ try {
123
+ correlationId = readCorrelationId(input.request, correlationIdProperty);
124
+ }
125
+ catch {
126
+ correlationId = undefined;
127
+ }
128
+ let trace;
129
+ try {
130
+ const explicitTrace = input.trace;
131
+ trace = readTraceContext({ trace: explicitTrace }, "trace");
132
+ trace ??= readTraceContext(input.request, traceContextProperty);
133
+ }
134
+ catch {
135
+ trace = undefined;
136
+ }
59
137
  const method = typeof input.method === "string" ? input.method.slice(0, 16) : undefined;
60
138
  const route = normalizeRoutePath(input.route);
61
139
  const statusCode = typeof input.statusCode === "number" && Number.isSafeInteger(input.statusCode)
@@ -64,7 +142,9 @@ function buildRequestContext(input) {
64
142
  if (method === undefined &&
65
143
  route === undefined &&
66
144
  statusCode === undefined &&
67
- requestId === undefined) {
145
+ requestId === undefined &&
146
+ correlationId === undefined &&
147
+ trace === undefined) {
68
148
  return undefined;
69
149
  }
70
150
  return {
@@ -72,5 +152,7 @@ function buildRequestContext(input) {
72
152
  ...(route === undefined ? {} : { route }),
73
153
  ...(statusCode === undefined ? {} : { statusCode }),
74
154
  ...(requestId === undefined ? {} : { requestId }),
155
+ ...(correlationId === undefined ? {} : { correlationId }),
156
+ ...(trace === undefined ? {} : { trace }),
75
157
  };
76
158
  }
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toBoundedPayload = toBoundedPayload;
4
+ exports.toBoundedAlertPayload = toBoundedAlertPayload;
5
+ const redact_js_1 = require("../core/redact.js");
6
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
7
+ function toBoundedPayload(value) {
8
+ const redacted = (0, redact_js_1.redactValue)(value, {
9
+ maxDepth: 6,
10
+ maxKeys: 100,
11
+ maxStringLength: 1_000,
12
+ maxStackLength: 4_000,
13
+ });
14
+ return isRecord(redacted)
15
+ ? redacted
16
+ : { title: "Wotchi alert", summary: "Incident details were unavailable." };
17
+ }
18
+ function toBoundedAlertPayload(alert) {
19
+ return toBoundedPayload(alert);
20
+ }
@@ -2,22 +2,112 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.formatConsoleAlert = formatConsoleAlert;
4
4
  exports.consoleNotifier = consoleNotifier;
5
+ const redact_js_1 = require("../core/redact.js");
6
+ const alert_payload_js_1 = require("./alert-payload.js");
5
7
  const MAX_FIELD_LENGTH = 1_000;
6
8
  const MAX_ACTIONS = 5;
7
9
  const limit = (value, maxLength = MAX_FIELD_LENGTH) => value.slice(0, maxLength);
8
- function formatConsoleAlert(alert) {
10
+ const safeText = (value, maxLength = MAX_FIELD_LENGTH) => {
11
+ const redacted = (0, redact_js_1.redactValue)(value, { maxStringLength: maxLength });
12
+ return typeof redacted === "string" ? limit(redacted, maxLength) : "[unreadable value]";
13
+ };
14
+ const safeJson = (value) => {
15
+ try {
16
+ return JSON.stringify(value);
17
+ }
18
+ catch {
19
+ return "[unreadable value]";
20
+ }
21
+ };
22
+ const boundedAlert = (alert) => {
23
+ const payload = (0, alert_payload_js_1.toBoundedAlertPayload)(alert);
24
+ return {
25
+ ...payload,
26
+ title: safeText(typeof payload.title === "string" ? payload.title : alert.title),
27
+ fingerprint: safeText(typeof payload.fingerprint === "string" ? payload.fingerprint : alert.fingerprint),
28
+ severity: alert.severity,
29
+ summary: safeText(typeof payload.summary === "string" ? payload.summary : alert.summary),
30
+ suggestedActions: alert.suggestedActions
31
+ .slice(0, MAX_ACTIONS)
32
+ .map((action) => safeText(action, 300)),
33
+ firstSeenAt: safeText(alert.firstSeenAt, 100),
34
+ lastSeenAt: safeText(alert.lastSeenAt, 100),
35
+ occurrences: Number.isSafeInteger(alert.occurrences) && alert.occurrences >= 0 ? alert.occurrences : 0,
36
+ service: safeText(alert.service, 300),
37
+ environment: safeText(alert.environment, 300),
38
+ };
39
+ };
40
+ const textDetails = (alert) => {
41
+ const lines = [];
42
+ if (alert.release !== undefined) {
43
+ lines.push(`Release: ${safeText(alert.release, 200)}`);
44
+ }
45
+ if (alert.instance !== undefined) {
46
+ lines.push(`Instance: ${safeText(alert.instance, 200)}`);
47
+ }
48
+ if (alert.operation !== undefined) {
49
+ lines.push(`Operation: ${safeText(alert.operation, 200)}`);
50
+ }
51
+ if (alert.job !== undefined) {
52
+ lines.push(`Job: ${safeText(alert.job, 200)}`);
53
+ }
54
+ if (alert.correlationId !== undefined) {
55
+ lines.push(`Correlation ID: ${safeText(alert.correlationId, 200)}`);
56
+ }
57
+ if (alert.tags !== undefined) {
58
+ lines.push(`Tags: ${safeText(safeJson(alert.tags), 600)}`);
59
+ }
60
+ if (alert.error !== undefined) {
61
+ lines.push(`Error: ${safeText(alert.error.name, 300)} — ${safeText(alert.error.message, 600)}`);
62
+ if (alert.error.applicationFrame !== undefined) {
63
+ lines.push(`Application frame: ${safeText(alert.error.applicationFrame, 500)}`);
64
+ }
65
+ }
66
+ if (alert.request !== undefined) {
67
+ const request = [alert.request.method, alert.request.route].filter((value) => typeof value === "string");
68
+ if (alert.request.statusCode !== undefined) {
69
+ request.push(`status ${alert.request.statusCode}`);
70
+ }
71
+ if (request.length > 0) {
72
+ lines.push(`Request: ${request.join(" ")}`);
73
+ }
74
+ if (alert.request.requestId !== undefined) {
75
+ lines.push(`Request ID: ${safeText(alert.request.requestId, 200)}`);
76
+ }
77
+ if (alert.request.correlationId !== undefined) {
78
+ lines.push(`Correlation ID: ${safeText(alert.request.correlationId, 200)}`);
79
+ }
80
+ }
81
+ const trace = alert.trace ?? alert.request?.trace;
82
+ if (trace?.traceId !== undefined || trace?.spanId !== undefined) {
83
+ lines.push(`Trace: ${safeText(trace.traceId ?? "", 128)}${trace.spanId === undefined ? "" : ` span ${safeText(trace.spanId, 128)}`}`);
84
+ }
85
+ if (alert.context !== undefined) {
86
+ lines.push(`Context: ${safeText(safeJson(alert.context), 1_000)}`);
87
+ }
88
+ if (alert.links !== undefined) {
89
+ lines.push(`Links: ${safeText(safeJson(alert.links), 600)}`);
90
+ }
91
+ return lines;
92
+ };
93
+ function formatConsoleAlert(alert, format = "text") {
94
+ const bounded = boundedAlert(alert);
95
+ if (format === "json") {
96
+ return JSON.stringify(bounded);
97
+ }
9
98
  const actions = alert.suggestedActions
10
99
  .slice(0, MAX_ACTIONS)
11
- .map((action) => `- ${limit(action, 300)}`)
100
+ .map((action) => `- ${safeText(action, 300)}`)
12
101
  .join("\n");
13
102
  return [
14
- limit(alert.title),
15
- `Service: ${limit(alert.service, 300)}`,
16
- `Environment: ${limit(alert.environment, 300)}`,
17
- `Summary: ${limit(alert.summary)}`,
18
- `Occurrences: ${alert.occurrences}`,
19
- `First seen: ${limit(alert.firstSeenAt, 100)}`,
20
- `Last seen: ${limit(alert.lastSeenAt, 100)}`,
103
+ bounded.title,
104
+ `Service: ${bounded.service}`,
105
+ `Environment: ${bounded.environment}`,
106
+ `Summary: ${bounded.summary}`,
107
+ `Occurrences: ${bounded.occurrences}`,
108
+ `First seen: ${bounded.firstSeenAt}`,
109
+ `Last seen: ${bounded.lastSeenAt}`,
110
+ ...textDetails(alert),
21
111
  "Suggested checks:",
22
112
  actions,
23
113
  ].join("\n");
@@ -27,7 +117,7 @@ function consoleNotifier(options) {
27
117
  return {
28
118
  name: "console",
29
119
  async send(alert) {
30
- write(formatConsoleAlert(alert));
120
+ write(formatConsoleAlert(alert, options?.format ?? "text"));
31
121
  },
32
122
  };
33
123
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.formatTelegramAlert = formatTelegramAlert;
4
+ const redact_js_1 = require("../core/redact.js");
4
5
  const MAX_MESSAGE_LENGTH = 4_096;
5
6
  const MAX_ACTIONS = 5;
6
7
  const MAX_TITLE_LENGTH = 300;
@@ -8,9 +9,76 @@ const MAX_SERVICE_LENGTH = 300;
8
9
  const MAX_ENVIRONMENT_LENGTH = 300;
9
10
  const MAX_SUMMARY_LENGTH = 2_000;
10
11
  const MAX_ACTION_LENGTH = 600;
12
+ const MAX_DETAIL_LENGTH = 900;
11
13
  const escapeHtml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
12
14
  const limit = (value, maxLength) => value.slice(0, maxLength);
13
- const render = (title, service, environment, summary, actions, firstSeenAt, lastSeenAt, occurrences) => {
15
+ const safeText = (value, maxLength) => {
16
+ const redacted = (0, redact_js_1.redactValue)(value, { maxStringLength: maxLength });
17
+ return typeof redacted === "string" ? limit(redacted, maxLength) : "[unreadable value]";
18
+ };
19
+ const safeJson = (value) => {
20
+ try {
21
+ return JSON.stringify(value);
22
+ }
23
+ catch {
24
+ return "[unreadable value]";
25
+ }
26
+ };
27
+ const details = (alert) => {
28
+ const lines = [];
29
+ if (alert.release !== undefined) {
30
+ lines.push(`Release: ${safeText(alert.release, 200)}`);
31
+ }
32
+ if (alert.instance !== undefined) {
33
+ lines.push(`Instance: ${safeText(alert.instance, 200)}`);
34
+ }
35
+ if (alert.operation !== undefined) {
36
+ lines.push(`Operation: ${safeText(alert.operation, 200)}`);
37
+ }
38
+ if (alert.job !== undefined) {
39
+ lines.push(`Job: ${safeText(alert.job, 200)}`);
40
+ }
41
+ if (alert.correlationId !== undefined) {
42
+ lines.push(`Correlation ID: ${safeText(alert.correlationId, 200)}`);
43
+ }
44
+ if (alert.tags !== undefined) {
45
+ lines.push(`Tags: ${safeText(safeJson(alert.tags), MAX_DETAIL_LENGTH)}`);
46
+ }
47
+ if (alert.error !== undefined) {
48
+ lines.push(`Error: ${safeText(alert.error.name, 250)} — ${safeText(alert.error.message, 500)}`);
49
+ if (alert.error.applicationFrame !== undefined) {
50
+ lines.push(`Application frame: ${safeText(alert.error.applicationFrame, 500)}`);
51
+ }
52
+ }
53
+ if (alert.request !== undefined) {
54
+ const request = [alert.request.method, alert.request.route].filter((value) => typeof value === "string");
55
+ if (alert.request.statusCode !== undefined) {
56
+ request.push(`status ${alert.request.statusCode}`);
57
+ }
58
+ if (request.length > 0) {
59
+ lines.push(`Request: ${request.join(" ")}`);
60
+ }
61
+ if (alert.request.requestId !== undefined) {
62
+ lines.push(`Request ID: ${safeText(alert.request.requestId, 200)}`);
63
+ }
64
+ if (alert.request.correlationId !== undefined) {
65
+ lines.push(`Correlation ID: ${safeText(alert.request.correlationId, 200)}`);
66
+ }
67
+ }
68
+ const trace = alert.trace ?? alert.request?.trace;
69
+ if (trace?.traceId !== undefined || trace?.spanId !== undefined) {
70
+ lines.push(`Trace: ${safeText(trace.traceId ?? "", 128)}${trace.spanId === undefined ? "" : ` span ${safeText(trace.spanId, 128)}`}`);
71
+ }
72
+ if (alert.context !== undefined) {
73
+ const redacted = (0, redact_js_1.redactValue)(alert.context, { maxDepth: 4, maxKeys: 40, maxStringLength: 300 });
74
+ lines.push(`Context: ${safeText(safeJson(redacted), MAX_DETAIL_LENGTH)}`);
75
+ }
76
+ if (alert.links !== undefined) {
77
+ lines.push(`Links: ${safeText(safeJson(alert.links), MAX_DETAIL_LENGTH)}`);
78
+ }
79
+ return lines.map((line) => limit(line, MAX_DETAIL_LENGTH));
80
+ };
81
+ const render = (title, service, environment, summary, actions, context, firstSeenAt, lastSeenAt, occurrences) => {
14
82
  const actionText = actions.length === 0
15
83
  ? "- No suggested checks."
16
84
  : actions.map((action) => `- ${escapeHtml(action)}`).join("\n");
@@ -22,6 +90,7 @@ const render = (title, service, environment, summary, actions, firstSeenAt, last
22
90
  `Occurrences: ${occurrences}`,
23
91
  `First seen: ${escapeHtml(firstSeenAt)}`,
24
92
  `Last seen: ${escapeHtml(lastSeenAt)}`,
93
+ ...context.map((line) => escapeHtml(line)),
25
94
  "Suggested checks:",
26
95
  actionText,
27
96
  ].join("\n");
@@ -36,17 +105,18 @@ function formatTelegramAlert(alert) {
36
105
  let actions = alert.suggestedActions
37
106
  .slice(0, MAX_ACTIONS)
38
107
  .map((action) => limit(action, MAX_ACTION_LENGTH));
39
- let formatted = render(title, service, environment, summary, actions, firstSeenAt, lastSeenAt, alert.occurrences);
108
+ const context = details(alert);
109
+ let formatted = render(title, service, environment, summary, actions, context, firstSeenAt, lastSeenAt, alert.occurrences);
40
110
  while (formatted.length > MAX_MESSAGE_LENGTH && actions.length > 0) {
41
111
  actions = actions.slice(0, -1);
42
- formatted = render(title, service, environment, summary, actions, firstSeenAt, lastSeenAt, alert.occurrences);
112
+ formatted = render(title, service, environment, summary, actions, context, firstSeenAt, lastSeenAt, alert.occurrences);
43
113
  }
44
114
  while (formatted.length > MAX_MESSAGE_LENGTH && summary.length > 0) {
45
115
  summary = summary.slice(0, Math.max(0, summary.length - 200));
46
- formatted = render(title, service, environment, summary, actions, firstSeenAt, lastSeenAt, alert.occurrences);
116
+ formatted = render(title, service, environment, summary, actions, context, firstSeenAt, lastSeenAt, alert.occurrences);
47
117
  }
48
118
  if (formatted.length > MAX_MESSAGE_LENGTH) {
49
- formatted = render(limit(title, 100), limit(service, 100), limit(environment, 100), "Incident details were truncated.", [], firstSeenAt, lastSeenAt, alert.occurrences);
119
+ formatted = render(limit(title, 100), limit(service, 100), limit(environment, 100), "Incident details were truncated.", [], [], firstSeenAt, lastSeenAt, alert.occurrences);
50
120
  }
51
121
  return formatted;
52
122
  }