@andrey4emk/npm-app-back-b24 5.1.0 → 5.2.0

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.
@@ -133,8 +133,11 @@ export function retryDelayFor(error: unknown, attempt: number): number {
133
133
  return isNonJsonResponseError(error) ? NON_JSON_RETRY_BASE_DELAY_MS * 2 ** (attempt - 1) : RETRY_DELAY_MS;
134
134
  }
135
135
 
136
+ /** Начало фразы про создающий вызов — по нему уровень отмены повтора отличает её от резервирующей */
137
+ const CREATING_REASON_HEAD = "вызов создаёт сущности (";
138
+
136
139
  /** Фраза для лога: повтор отменён, потому что вызов создаёт сущности */
137
- const creatingReason = (what: string): string => `вызов создаёт сущности (${what})`;
140
+ const creatingReason = (what: string): string => `${CREATING_REASON_HEAD}${what})`;
138
141
 
139
142
  /** Фраза для лога: повтор отменён, потому что вызов резервирует данные на портале */
140
143
  const reservingReason = (what: string): string => `вызов резервирует данные на портале (${what})`;
@@ -447,8 +450,12 @@ export async function runWithRetry<T>(fn: () => Promise<T>, label: string, block
447
450
  // потеряться уже после того, как портал его выполнил. Различить нельзя,
448
451
  // поэтому вызовы под ограничением не повторяем — лучше вернуть ошибку,
449
452
  // чем создать дубликат или потерять зарезервированный пакет
453
+ // Отмена только резервирующего вызова (event.offline.get) — warn: приложение и так
454
+ // пишет свою строку error о том же сбое, а две строки дали бы два сообщения в
455
+ // Телеграме. Отмена создающего остаётся error: сущность могла не создаться
450
456
  if (blockReason && !proven) {
451
- logs.add(`${label}${code}: повтор отменён, ${blockReason} — ${msg}`, "error");
457
+ const level = blockReason.includes(CREATING_REASON_HEAD) ? "error" : "warn";
458
+ logs.add(`${label}${code}: повтор отменён, ${blockReason} — ${msg}`, level);
452
459
  throw error;
453
460
  }
454
461
 
package/logs/logs.ts CHANGED
@@ -68,6 +68,26 @@ function describeSendError(error: unknown): string {
68
68
  return typeof causeLabel === "string" && causeLabel ? `${name} (${causeLabel})` : name;
69
69
  }
70
70
 
71
+ /**
72
+ * Признаки разового сетевого сбоя в тексте error-лога: обрыв, таймаут, недоступный хост.
73
+ * Проверяется текст, а не тип ошибки: строки о сбое пишут и сами приложения
74
+ * («Не удалось получить события. read ECONNRESET»), типа ошибки у логгера нет
75
+ */
76
+ const NETWORK_FAILURE_RE =
77
+ /ECONNRESET|socket hang up|ETIMEDOUT|ECONNREFUSED|EHOSTUNREACH|ENETUNREACH|ENOTFOUND|EAI_AGAIN|UND_ERR_SOCKET|UND_ERR_CONNECT_TIMEOUT|Request timeout exceeded|fetch failed|other side closed|Error connecting to authorization server/i;
78
+
79
+ /**
80
+ * Строки с сетевым признаком, которые уходят сразу и по одной: отменённое создание
81
+ * сущности, провал обмена токена, неизвестный статус доставки — их разбирает человек
82
+ */
83
+ const NETWORK_KEEP_RE = /вызов создаёт сущности|обновление токена не удалось|статус неизвестен/;
84
+
85
+ /** Окно, за которое считаются сетевые сбои, и пауза между сводками, мс */
86
+ const NETWORK_WINDOW_MS = 15 * 60_000;
87
+
88
+ /** Со скольких сетевых сбоев за окно в Телеграм уходит сводка */
89
+ const NETWORK_THRESHOLD = 3;
90
+
71
91
  /** Ключ ретранслятора — только видимые символы ASCII, без пробелов и управляющих */
72
92
  const RELAY_KEY_RE = /^[\x21-\x7e]+$/;
73
93
 
@@ -140,6 +160,12 @@ class LogsAPI {
140
160
  /** Ключ клиента ретранслятора (`LOG_RELAY_KEY`); `undefined` — канал выключен */
141
161
  private readonly relayKey: string | undefined;
142
162
 
163
+ /** Время сетевых сбоев за последние `NETWORK_WINDOW_MS`, мс */
164
+ private networkFailures: number[] = [];
165
+
166
+ /** Когда ушла последняя сводка сетевых сбоев, мс */
167
+ private lastNetworkSummaryAt = Number.NEGATIVE_INFINITY;
168
+
143
169
  constructor() {
144
170
  this.initConfig();
145
171
  const url = process.env.LOG_RELAY_URL?.trim() || undefined;
@@ -189,6 +215,7 @@ class LogsAPI {
189
215
  * независимо от настройки `error.enabled` в `log.json`. Настройка `enabled`
190
216
  * управляет только выводом в консоль. Это сделано намеренно, чтобы критические
191
217
  * ошибки всегда доходили до команды даже при отключённом консольном логировании.
218
+ * Исключение — разовые сетевые сбои: они уходят сводкой, см. `routeToRelay()`.
192
219
  *
193
220
  * @param message - Текст сообщения или объект для вывода
194
221
  * @param level - Уровень логирования (trace, debug, info, warn, error)
@@ -204,7 +231,7 @@ class LogsAPI {
204
231
  // В консоль ниже уходят исходные message и jsonData — маскировка телефонов
205
232
  // касается только текста для Телеграма
206
233
  if (levelStr === "error") {
207
- this.sendToRelay(messageText, jsonData);
234
+ this.routeToRelay(messageText, jsonData);
208
235
  }
209
236
 
210
237
  // Проверяем конфиг, и если логирование отключено, то выходим
@@ -228,6 +255,36 @@ class LogsAPI {
228
255
  }
229
256
  }
230
257
 
258
+ /**
259
+ * Решает, уходит ли error-лог в Телеграм сразу.
260
+ *
261
+ * Разовый сетевой сбой проходит сам, а сообщение о нём в Телеграме — шум: 25.10.2026
262
+ * один недоступный сервер авторизации дал 14 сообщений подряд. Поэтому строка
263
+ * с сетевым признаком остаётся только в консоли, а в Телеграм уходит одна сводка,
264
+ * когда за 15 минут набралось 3 сбоя и больше; следующая — не раньше чем через 15 минут.
265
+ * Длинный обрыв так и виден — сводкой раз в 15 минут.
266
+ */
267
+ private routeToRelay(message: string, jsonData?: unknown): void {
268
+ if (!this.relayUrl || !this.relayKey) return;
269
+
270
+ if (!NETWORK_FAILURE_RE.test(message) || NETWORK_KEEP_RE.test(message)) {
271
+ this.sendToRelay(message, jsonData);
272
+ return;
273
+ }
274
+
275
+ const now = Date.now();
276
+ this.networkFailures = this.networkFailures.filter((at) => now - at < NETWORK_WINDOW_MS);
277
+ this.networkFailures.push(now);
278
+
279
+ if (this.networkFailures.length < NETWORK_THRESHOLD) return;
280
+ if (now - this.lastNetworkSummaryAt < NETWORK_WINDOW_MS) return;
281
+
282
+ this.lastNetworkSummaryAt = now;
283
+ const minutes = NETWORK_WINDOW_MS / 60_000;
284
+ const summary = `Сетевые сбои: ${this.networkFailures.length} за ${minutes} мин (одиночные в Телеграм не отправляются, все строки — в логе контейнера). Последний:\n${message}`;
285
+ this.sendToRelay(summary, jsonData);
286
+ }
287
+
231
288
  /**
232
289
  * Готовит текст error-лога и отправляет его в ретранслятор (fire-and-forget).
233
290
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andrey4emk/npm-app-back-b24",
3
- "version": "5.1.0",
3
+ "version": "5.2.0",
4
4
  "description": "Bitrix24 OAuth helpers for Node.js projects",
5
5
  "main": "index.ts",
6
6
  "type": "module",