1. Почему в ChatGPT App ошибки — норма, а не ЧП
В прошлой лекции мы говорили, как резать задачу на шаги и строить многошаговый workflow в ChatGPT App. Теперь добавим к этой схеме честную реальность: ошибки, таймауты и прерывания пользователем.
В классическом вебе логика часто строится вокруг «счастливого пути», а ошибки воспринимаются как что‑то редкое и аварийное — красная страница 500 и т.п. В ChatGPT App картина другая: вы работаете в распределённой системе с LLM, внешними API, MCP, виджетом, да ещё и с пользователем, который может закрыть вкладку в любой момент. Ошибки и прерывания — ежедневная рутина.
Есть несколько особенностей, которые усложняют жизнь:
- Во‑первых, LLM недетерминирована. Даже при одинаковом промпте она может принять чуть другое решение: вызвать другой инструмент, изменить параметры или вообще решить, что лучше «переспросить».
- Во‑вторых, сетевые и инфраструктурные ограничения. Tool‑call от ChatGPT имеет таймауты (обычно десятки секунд), как и ваш Next.js‑/Vercel‑бэкенд. Если внешний API тормозит, всё может оборваться на середине.
- В‑третьих, есть UX‑фактор: пользователь отвлёкся, закрыл чат, вернулся через день, а вы никак не можете держать открытой транзакцию в базе всё это время.
Отсюда главный тезис лекции:
Отказоустойчивый workflow = сценарий, в котором мы заранее считаем, что любой шаг может упасть, и явно определяем, что тогда происходит.
Ошибка — не только повод показать пользователю сообщение, но и сигнал для модели, которая может изменить стратегию, предложить откат, попробовать другой инструмент или аккуратно завершить сценарий.
2. Ландшафт ошибок в workflow: какие они бывают
Чтобы грамотно обрабатывать сбои, нужно сначала научиться их различать. В LLM‑приложении на базе ChatGPT Apps типично встречаются несколько классов ошибок.
Технические ошибки. Это вся классика распределённых систем: сетевые таймауты, 5xx от ваших или внешних API, падения MCP‑сервера, баг в коде handler’а инструмента. Например, в GiftGenius ваш MCP‑tool search_products обращается к каталогу, а тот отвечает 503 Service Unavailable. Это кандидат на автоматический повтор запроса (retry).
Логические (модельные) ошибки. Сюда входят отказы модели (она решила, что запрос нарушает политику), галлюцинации или, например, сломанный JSON при ответе инструмента. Модель могла сгенерировать некорректные аргументы для tool‑call, и ваша JSON‑валидация не пропустила их. Это чаще всего ошибка входных данных, а не инфраструктуры.
Бизнес‑ошибки. Они про смысл: товар закончился, бюджет пользователя слишком мал для выбранных фильтров, промокод недействителен, бронирование уже истекло. В GiftGenius это ситуация «из 500 кандидатов ни один не подходит под заданные ограничения». Здесь retry редко помогает: нужно либо менять параметры, либо объяснить пользователю, что ограничение нереалистично.
UX‑прерывания. Пользователь сам рвёт сценарий: закрывает ChatGPT, нажимает «Назад» в виджете, отменяет действие, меняет ответ на прошлый шаг. Это тоже нужно считать нормальным потоком, а не ошибкой. Важно уметь восстанавливать и откатывать состояние в таких случаях, о чём мы поговорим чуть позже.
Отдельный проблемный кейс на стыке логических и технических ошибок — бесконечные циклы агента: модель получает ошибку, думает «хм, попробую ещё раз», снова ошибка, и так пока не закончится контекст или бюджет. Защититься от такого поведения — важная часть дизайна ошибок.
3. Базовые стратегии: retry, fail‑fast, rollback, вовлечение пользователя
Любую ошибку можно рассматривать как точку ветвления: мы либо пытаемся повторить шаг, либо откатываемся, либо привлекаем пользователя. И, что важно, эти стратегии комбинируются.
Для технических и временных сбоев (сеть моргнула, API вернуло 503) логично делать ограниченный retry с backoff. Для логических и бизнес‑ошибок («валидатор не принял бюджет», «товары закончились») повторять бессмысленно, нужно fail‑fast и просить пользователя изменить ввод или параметры.
Для операций, которые уже что‑то изменили во внешнем мире (создали заказ, сделали бронь), нужен rollback — либо в виде «шага назад» в UI/контексте, либо в виде реальных компенсирующих действий (отмена заказа, возврат средств).
Наконец, есть решения, которые заведомо требуют участия пользователя: например, при отказе платёжной системы по причине «карта отклонена банком» вы не можете автоматически всё это исправить. Модель должна корректно объяснить, что произошло, и предложить варианты: попробовать другую карту, снизить сумму или отказаться от покупки.
Для надежного workflow очень полезно прямо для каждого шага выписать: какие типы ошибок здесь возможны и что вы делаете при каждом из них — auto‑retry, откат, запрос к пользователю или просто лог и завершение ветки.
4. Повторы (retry) и backoff: когда и как их делать
Начнем с самой естественной реакции разработчика: «Ну, давайте просто попробуем ещё раз». Сама идея правильная, но, как всегда, дьявол в деталях.
Какие ошибки можно ретраить
Хорошая эвристика из практики интеграций звучит так: сетевые ошибки и 5xx можно пробовать ещё раз с паузой, а 4xx — скорее всего, нет.
То есть, если вы получили 503, 504 или просто не дождались ответа из внешнего API, повторить запрос с небольшой задержкой имеет смысл. Если же сервер вернул 400 Bad Request или 422 Unprocessable Entity, вероятнее всего, проблема в данных, и повтор с теми же параметрами ничего не изменит.
Простая утилита callWithRetry на TypeScript
Давайте напишем маленькую утилиту для MCP‑ или backend‑слоя, которую можно будет использовать в инструментах:
type RetryOptions = {
maxRetries: number;
baseDelayMs: number;
};
async function callWithRetry<T>(
fn: () => Promise<T>,
{ maxRetries, baseDelayMs }: RetryOptions
): Promise<T> {
let attempt = 0;
// бесконечные циклы нам не нужны
while (true) {
try {
return await fn();
} catch (err: any) {
attempt++;
const status = err?.status ?? err?.response?.status;
// 4xx не ретраим
const isClientError = typeof status === "number" && status >= 400 && status < 500;
if (attempt > maxRetries || isClientError) {
throw err;
}
const delay = Math.min(baseDelayMs * 2 ** (attempt - 1), 10_000);
// небольшая пауза, чтобы не ударить по API всем стадом
const jitter = Math.random() * 200;
await new Promise((r) => setTimeout(r, delay + jitter));
}
}
}
Эта функция:
- повторяет вызов fn ограниченное число раз;
- использует экспоненциальный backoff с небольшим случайным шумом (jitter), чтобы избежать эффекта «стада» при одновременных ретраях;
- прекращает повторы (retry) на 4xx.
Её хорошо использовать, например, внутри MCP‑инструмента, который ходит к каталогу товаров или к внутреннему API рекомендаций.
Где именно делать retry
Частая ошибка — пытаться повторять все запросы подряд, в том числе на уровнях, которые вы не контролируете. В экосистеме ChatGPT у вас есть несколько мест для ретраев:
- внутри собственного backend/MCP (как мы сделали в callWithRetry);
- внутри фонового воркера/очереди (в будущих модулях мы подробнее обсудим job‑очереди и DLQ);
- иногда — в самом виджете, когда речь идёт о лёгком запросе «обновить список» без побочных эффектов.
Важно не дублировать логику: если ваш job‑воркер уже делает три ретрая с backoff, нет смысла сверху навешивать ещё пять ретраев в виджете. И, конечно, никогда не делайте while(true) { try ... } — это верный способ устроить DDoS самому себе.
5. Идемпотентность шагов: защита от дублей
Повторы создают вторую проблему: как не сделать одно и то же действие два раза. В LLM‑мире это особенно остро: модель может случайно вызвать один и тот же инструмент несколько раз, ChatGPT может повторить tool‑call после таймаута, пользователь может нажать «Regenerate», а затем UI или агент могут по‑своему добавить ещё один вызов.
Идея идемпотентности простая: шаг считается идемпотентным, если его повторное выполнение с теми же входными данными не создаёт дополнительных побочных эффектов. Запросить product feed — ок, пересчитать рекомендации — ок, а вот повторно списать деньги или создать второй заказ по тем же данным — совсем не ок.
Idempotency key в ChatGPT App
Классический паттерн: для каждого логического шага с побочными эффектами вы генерируете idempotency_key (обычно UUID), передаёте его через модель к MCP‑инструменту и там храните соответствие «ключ → результат». Если инструмент вызвали второй раз с тем же ключом, он не повторяет действие, а просто возвращает уже сохранённый результат.
В нашем GiftGenius есть шаг create_order. Представьте, что пользователь нажал кнопку «Оплатить», модель вызвала инструмент, платёж прошёл, но где‑то по пути ответ потерялся. Модель или платформа решают повторить вызов, и если у нас нет идемпотентности, мы получим дубль заказа или двойное списание.
Простой пример идемпотентного инструмента на TypeScript
Сделаем очень упрощённый handler MCP‑инструмента create_order с idempotency‑ключом. Для простоты используем in‑memory Map, в реальной жизни это будет БД или кэш.
type CreateOrderInput = {
userId: string;
items: Array<{ sku: string; qty: number }>;
idempotencyKey: string;
};
type CreateOrderResult = { orderId: string; status: "created" };
const idempotencyStore = new Map<
string,
{ paramsHash: string; result: CreateOrderResult }
>();
export async function createOrderTool(input: CreateOrderInput): Promise<CreateOrderResult> {
const { idempotencyKey, ...rest } = input;
const paramsHash = JSON.stringify(rest);
const existing = idempotencyStore.get(idempotencyKey);
if (existing) {
// если ключ уже был, убеждаемся, что параметры совпадают
if (existing.paramsHash !== paramsHash) {
throw new Error("Idempotency key reuse with different params");
}
return existing.result;
}
// здесь мы делаем реальное создание заказа и оплату
const result: CreateOrderResult = {
orderId: "order_" + Math.random().toString(36).slice(2),
status: "created",
};
idempotencyStore.set(idempotencyKey, { paramsHash, result });
return result;
}
Здесь мы:
- требуем idempotencyKey во входных данных инструмента;
- храним вместе с ним хэш параметров (здесь для простоты JSON.stringify);
- при повторном вызове с тем же ключом, но другими данными — считаем это ошибкой;
- при повторном вызове с теми же данными — просто возвращаем прежний результат.
В реальном проекте стоит:
- хранить ключи в БД с TTL (чтобы таблица не разрослась до небес);
- логировать idempotency_key и включать его в _meta MCP‑сообщений, чтобы удобно отслеживать через Inspector и дашборды.
6. Откат шагов и паттерн Saga
Идемпотентность защищает от дублей, но не решает другой кейс: что делать, если один из шагов в середине сценария упал.
В e‑commerce это классическая проблема: вы уже создали заказ и забронировали товар на складе, а на этапе оплаты что‑то пошло не так. Вы не можете просто «забыть об этом» — нужно как‑то откатить предыдущее состояние.
Логический vs технический rollback
В ChatGPT‑workflow есть два уровня отката.
Логический откат — это возвращение к предыдущему шагу сценария и корректировка контекста. Например, на шаге «оплата» произошла ошибка, и вы решаете откатиться к шагу «выбор способа оплаты» или даже «выбор подарка». Тогда важно:
- обновить WorkflowContext на backend’е (текущий шаг, выбранные параметры);
- сообщить модели о смене шага через tool‑call/ToolOutput, чтобы она «забыла» старую ветку и подстроила дальнейшее поведение;
- обновить UI виджета, чтобы шаги и кнопки соответствовали новому состоянию.
Технический rollback — это уже бизнес‑уровень: отмена созданных сущностей, компенсация внешних эффектов. Например: отменить заказ, снять резерв со склада, инициировать возврат оплаты. Это и есть паттерн Saga: для каждого «опасного» шага вы заранее придумываете компенсирующее действие.
Схема forward/compensate для GiftGenius
Для упрощённого GiftGenius‑checkout можем нарисовать такую последовательность:
flowchart TD A[Шаг 1: create_order] --> B[Шаг 2: reserve_items] B --> C[Шаг 3: charge_card] C -->|успех| D[Статус: completed] C -->|ошибка| E[Компенсация: cancel_reservation] E --> F[Компенсация: cancel_order] F --> G[Статус: failed + сообщение пользователю]
Каждому действию, которое изменяет внешний мир (создание заказа, резервирование, оплата), соответствует компенсирующее действие (отмена заказа, снятие резерва, возврат средств). Они не всегда симметричны и не всегда возможны один к одному, но общий принцип именно такой.
Мини‑пример с компенсацией в коде
Посмотрим на небольшой фрагмент кода, который выполняет эти шаги:
async function completeCheckout(ctx: { userId: string }) {
const order = await createOrderInDb(ctx.userId);
try {
await reserveItems(order.id);
await chargeCard(order.id);
return { orderId: order.id, status: "paid" as const };
} catch (err) {
// компенсирующие действия
await safeCancelReservation(order.id);
await safeCancelOrder(order.id);
throw err;
}
}
Здесь:
- createOrderInDb, reserveItems, chargeCard — forward‑шаги;
- safeCancelReservation и safeCancelOrder — компенсирующие шаги, которые сами по себе должны быть идемпотентными (если мы попытаемся отменить уже отменённое, ничего страшного не произойдёт).
Обратите внимание, что при ошибке мы не скрываем её, а пробрасываем дальше. Модель (через ToolOutput) должна получить понятное сообщение об ошибке и уже в человеческом виде объяснить это пользователю и предложить следующий шаг.
7. Откат шагов и синхронизация состояния: как не устроить рассинхрон
Есть особый вид «ошибки», которую легко недооценить: рассинхрон состояния между UI, backend и моделью.
Типичный сценарий:
- Пользователь проходит шаги 1 → 2 → 3.
- На шаге 3 что‑то идёт не так, пользователь нажимает кнопку «Назад» в виджете.
- Виджет честно возвращает свой локальный стейт на шаг 2.
- Но модель «помнит», что мы были на шаге 3 и уже пытались оплатить. В следующем сообщении она продолжает говорить об оплате, хотя пользователь видит экран выбора подарка.
Чтобы такого не происходило, полезно вводить явное событие отката шага. Его отправляет виджет в MCP/модель — либо как вызов инструмента, либо как ToolOutput.
Например, можно сделать простой tool user_navigated_to_step, который фиксирует текущий шаг и его состояние:
type NavigateInput = {
workflowId: string;
stepId: string;
};
export async function userNavigatedToStep(input: NavigateInput) {
await workflowRepo.setCurrentStep(input.workflowId, input.stepId);
return {
message: `User moved to step ${input.stepId}`,
};
}
Виджет при нажатии «Назад» вызывает этот tool; модель видит его результат в истории tool‑call’ов и понимает, что теперь нужно продолжать диалог, исходя из нового шага.
На стороне UI это примерно такой обработчик:
async function handleBackClick() {
const { workflowId, prevStepId } = widgetState;
await window.openai.tools.call("user_navigated_to_step", {
workflowId,
stepId: prevStepId,
});
setWidgetState((s) => ({ ...s, currentStepId: prevStepId }));
}
Важный момент: именно backend/агент — источник истины по текущему шагу, а модель видит его через tools. Тогда даже при восстановлении сессии позже вы можете синхронизировать контекст правильно.
8. UX ошибок: что видит пользователь и что видит модель
Мы уже научились технически переживать ошибки (ретраи, откаты, идемпотентность, синхронизацию состояния). Осталось сделать так, чтобы это выглядело адекватно и для пользователя, и для модели.
Даже идеально реализованный retry и rollback не спасут, если UX ошибок будет «как в старых Java‑сервлетах»: красный текст, stack trace и загадочное «Unexpected error».
Для ChatGPT App есть две аудитории сообщения об ошибке:
- пользователь, который должен понять, что произошло и что он может сделать дальше;
- модель, которая должна получить достаточно структурированную информацию, чтобы принять решение: повторять, менять параметры, предлагать альтернативу или завершать сценарий.
Хорошая практика:
- на уровне MCP/инструментов возвращать структурированную ошибку с кодом, типом, флагом retryable и коротким техническим текстом;
- давать модели именно эту структуру (например, в result.structuredContent), а не километр stack trace;
- в UI показывать пользователю человеческое, короткое сообщение.
Мини‑пример структуры ошибки, которую возвращает инструмент:
type ToolError = {
code: string; // e.g. "PAYMENT_TIMEOUT"
message: string; // краткое тех.описание
retryable: boolean; // можно ли пытаться еще раз
};
throw {
isError: true,
error: <ToolError>{
code: "PAYMENT_TIMEOUT",
message: "Payment provider did not respond in time",
retryable: true,
},
};
Модель видит retryable: true и может попробовать другой инструмент или предложить пользователю повторить попытку.
На стороне виджета вы просто мапите эти коды на понятные пользователю тексты:
function ErrorBanner({ code }: { code: string }) {
const text =
code === "PAYMENT_TIMEOUT"
? "Платёжный сервис не ответил вовремя. Попробуйте ещё раз через минуту."
: "Что-то пошло не так. Пожалуйста, попробуйте ещё раз.";
return <div className="error-banner">{text}</div>;
}
И ещё один важный момент: не показывайте пользователю стеки исключений, токены, секреты. Это и некрасиво, и небезопасно. Техническую информацию логируйте у себя, а пользователю давайте короткий, безопасный месседж.
Insight
В LLM-системах вроде ChatGPT некорректные вызовы инструментов — скорее норма, чем исключение. Модель регулярно генерирует аргументы, которые не проходят валидацию: перепутанные типы, отсутствующие поля, неверные значения, сломанные структуры. Это не ошибка в привычном инженерном смысле — это часть природы стохастической модели, и под неё нужно адаптировать весь интерфейс ошибок.
Ключевая идея: сообщение об ошибке — это не сигнал “сломалось”, а инструкция для исправления следующей попытки. Его главная аудитория — сама модель. Если сообщение структурировано и содержит точные указания, модель способна автоматически скорректировать параметры и повторить вызов уже правильно. Это именно тот принцип, на котором основаны техники Tool-Reflection: корректная обратная связь улучшает следующее действие агента без участия человека.
Рекомендую придерживаться таких требований к формату ошибок:
- сообщение должно указывать конкретное поле, которое не прошло валидацию — без обобщений уровня “Invalid parameters”;
- важно явно описывать ожидаемый формат или допустимые значения, чтобы модель могла выбрать подходящее;
- сообщение должно быть кратким, формальным и структурированным: поля вроде error_type, field, expected или allowed_values существенно помогают модели;
- при возможности стоит дать минимальный пример корректного ввода — это часто повышает точность восстановления модели.
Идеальный error feedback для модели содержит два факта: что пошло не так, и инструкцию как это исправить.
9. Логирование и метрики ошибок workflow
Даже если UX ошибок аккуратный, чтобы понять, что у вас реально ломается, одних сообщений пользователю мало. Нужны структурированные логи и метрики по шагам.
Минимальный полезный набор при логировании каждого шага workflow:
- user_id или хотя бы session_id;
- workflow_id и step_id;
- статус шага (success, failed, retry, rolled_back);
- error_code (если был);
- idempotency_key и correlation_id, если шаг связан с внешними вызовами.
В MCP и Agents есть _meta‑поля; туда удобно класть idempotency_key и correlation_id, чтобы их было видно и в логах, и в Inspector.
Простейший пример логирования на Node.js/TypeScript (можно использовать console, можно — winston/pino):
function logStepFailure(params: {
userId?: string;
workflowId: string;
stepId: string;
errorCode: string;
idempotencyKey?: string;
}) {
console.error(
JSON.stringify({
level: "error",
event: "workflow_step_failed",
...params,
timestamp: new Date().toISOString(),
})
);
}
Такие логи легко парсить, строить по ним дашборды и считать:
- конверсию между шагами;
- самые частые типы ошибок;
- долю шагов, завершившихся retry vs окончательным провалом.
Не каждая ошибка должна становиться алертом в продакшене. Критичные — падение MCP, систематические таймауты, массовые провалы на определённом шаге — да, нужно выносить в мониторинг. А вот «нет результатов по поиску подарков» — это бизнес‑событие, не инцидент.
10. Развиваем GiftGenius: устойчивый checkout‑шаг
Давайте теперь соберём всё вместе: ретраи, идемпотентность, Saga, синхронизацию состояния, UX ошибок и логирование — на примере одного шага в нашем учебном приложении GiftGenius — оформления заказа.
Что уже есть
К этому моменту у нас уже:
- есть многошаговый workflow: сбор информации → подбор идей → выбор подарка → checkout;
- настроен tool gating: на шаге checkout доступен только набор commerce‑инструментов (create_order, get_payment_methods и т.п.);
- есть WorkflowContext, в котором хранятся выбранный подарок, бюджет, userId и текущий шаг.
Что добавим на этой лекции
Для шага checkout внедрим:
- idempotency_key для инструмента create_order;
- retry при временных ошибках платёжного провайдера;
- компенсацию при частично успешных операциях;
- корректный UX ошибок в виджете.
Генерация idempotency‑ключа в виджете при нажатии кнопки «Оплатить»:
import { v4 as uuid } from "uuid";
async function handlePayClick() {
const idempotencyKey = uuid();
setWidgetState((s) => ({ ...s, idempotencyKey }));
await window.openai.tools.call("create_order", {
userId: widgetState.userId,
items: [/* ... */],
idempotencyKey,
});
}
На стороне инструмента create_order — тот самый идемпотентный handler, который мы писали выше: он хранит ключ и результат, и при повторе не создаёт новый заказ.
Код взаимодействия с платёжным API можно обернуть в callWithRetry, чтобы несколько раз попытаться списать деньги при сетевых глюках. И не забыть добавить флаг retryable: true в ошибку, чтобы модель понимала, что можно предложить повтор.
Если после успешного создания заказа и списания денег что‑то ломается (например, внешний webhook не приходит вовремя), мы логируем это с correlation_id и workflow_id и дальше:
- пробуем фоновый retry (в будущем модуле про очереди и события);
- или явно помечаем шаг как failed, вызываем компенсирующие действия и объясняем пользователю, что произошло.
11. Типичные ошибки при проектировании отказоустойчивых workflow
Ошибка №1: «Ретраим всё, пока не взлетит».
Автоматически повторять любой шаг до победного — надёжный способ устроить себе локальный ад. Сетевые и 5xx‑ошибки можно пробовать ещё раз с backoff и лимитом попыток. Но 4xx, бизнес‑ошибки и логические провалы модели нужно либо чинить данными, либо объяснять пользователю. Иначе вы получите нестабильное поведение, странные счета и засорённые логи.
Ошибка №2: Отсутствие идемпотентности там, где есть деньги и заказы.
Если инструмент типа create_order или charge_card не идемпотентен, любой повторный вызов (из‑за таймаута, Regenerate, бага в агенте) может привести к дублям. В LLM‑сценариях повторы встречаются заметно чаще, чем в классическом REST‑фронтенде, поэтому idempotency_key — не «приятный бонус», а обязательное условие для платёжных и других критичных шагов.
Ошибка №3: Нет компенсирующих действий (Saga отсутствует).
Создали заказ, забронировали товар, а на оплате упали и просто показали пользователю «что-то пошло не так». В результате в системе висят полу‑заказы, резервы, финансовые «хвосты». Для каждого шага, который меняет внешний мир, стоит продумать, что вы будете делать при провале следующего шага: отменять, возвращать, помечать как «expired» и т.д.
Ошибка №4: Дать агенту уйти в бесконечный цикл ретраев.
Если вы не ограничите количество попыток (например, через maxRetries в helper’ах или через max_iterations в агентной логике) и не будете помечать ошибки как retryable: false там, где ретраи бесполезны, модель может зациклиться: «Попробую ещё раз… ещё раз…». Это сжигает токены, время и нервы.
Ошибка №5: Рассинхрон состояния между UI и моделью при откате.
Часто разработчики реализуют кнопку «Назад» только в UI, забывая синхронизировать шаг с backend’ом и моделью. В итоге пользователь видит шаг 2, а модель продолжает жить на шаге 3 и делать странные предложения. Решение — явные события типа user_navigated_to_step и обновление WorkflowContext при каждом переходе.
Ошибка №6: Технические сообщения для пользователя и отсутствие логов для разработчиков.
Пользователь получает «Error: ECONNRESET at TcpSocket.onEnd…», а вы — ноль информации о том, какой именно шаг и для какого workflow_id сломался. Грамотный подход: для пользователя — короткий, понятный текст и предложение, что делать дальше; для разработчика — структурированный лог с workflow_id, step_id, error_code, idempotency_key и correlation_id.
Ошибка №7: Отсутствие стратегии по алертам.
Либо алертят всё подряд, включая «нет подходящих подарков под ваш очень узкий фильтр», либо не алертят ничего, включая настоящее падение MCP. Старайтесь отделять критические системные сбои (падение сервиса, массовые таймауты, потеря webhook’ов) от ожидаемых бизнес‑событий. Первые идут в мониторинг и on‑call, вторые — просто считаются в аналитике.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ