Перетяжка, Премия ТПрогер, 13.11
Перетяжка, Премия ТПрогер, 13.11
Перетяжка, Премия ТПрогер, 13.11

Продукт и баги: какие ошибки ломают всё, а какие — просто часть кода

Как отличить опасные баги от некритичных и выстроить систему работы с ними? Разбираем примеры и инструменты для джунов и перечисляем неочевидные фишки для миддлов.

224 открытий3К показов
Продукт и баги: какие ошибки ломают всё, а какие — просто часть кода

Критические баги: когда всё идёт не по плану

Это те самые «монстры», которые рушат ключевую логику, приводят к потере денег, данных или репутации. Их объединяет одно: они блокируют пользовательский сценарий или создают серьёзную уязвимость.

Пример 1: «Призрачный промокод», или классический Race Condition

Представьте себе интернет-магазин, который запустил акцию: «Первые 100 покупателей получат скидку 50% с промокодом SUPER-SALE». Код простой: проверяем, сколько раз промокод уже был использован, и если меньше 100 — применяем скидку.

Ситуация: в понедельник утром маркетологи в панике: промокод применили 350 раз. Бизнес потерял кучу денег. Как так вышло?

Разбор полётов: проблема в параллельных запросах. Когда нагрузка на сервер высока, несколько пользователей могут одновременно отправить запрос на применение промокода.

Упрощённый код на бэкенде мог выглядеть так (Node.js-подобный псевдокод):

			async function applyPromoCode(promoCode) {
  // 1. Получаем текущее количество использований из базы
  const usageCount = await db.getPromoUsage(promoCode); // Допустим, сейчас 99

  // 2. Проверяем, есть ли ещё лимит
  if (usageCount < 100) {
    // 3. Увеличиваем счётчик в базе
    await db.incrementPromoUsage(promoCode);
    return { success: true, message: "Скидка применена!" };
  } else {
    return { success: false, message: "Лимит промокода исчерпан" };
  }
}

		

Что происходит под нагрузкой:

  1. Запрос А приходит. usageCount равен 99. Проверка 99 < 100 проходит. 
  2. Запрос Б приходит сразу после Запроса А, но до того, как Запрос А успел обновить счётчик в базе. Для Запроса Б usageCount всё ещё равен 99. Проверка 99 < 100 тоже проходит.
  3. Запрос В приходит в тот же момент, для него usageCount равен 99.

В итоге все три запроса успешно применяют скидку и увеличивают счётчик. Вместо одного использования мы получили три.

Это классическое состояние гонки (Race Condition). Проблема не в логике как таковой, а во времени и одновременном доступе к общему ресурсу (счётчику в БД).

Многие помнят про Race Condition, но часто забывают, что он может проявляться не только в классических банковских транзакциях, но и в менее очевидных местах: счётчики, генерация уникальных имён, бронирование слотов.

Как чинить? Самый надёжный способ — транзакции с блокировкой. Пояснение для новичков: FOR UPDATE говорит базе данных: «Сейчас буду менять эту строку, никому её не отдавай, пока я не закончу». Другие запросы выстроятся в очередь и будут ждать, пока первый не завершит свою работу.

			BEGIN TRANSACTION;

-- Блокируем строку с промокодом, чтобы другие транзакции ждали
SELECT usage_count FROM promo_codes WHERE code = 'SUPER-SALE' FOR UPDATE;

-- Если счётчик < 100, обновляем его и коммитим транзакцию
-- Иначе откатываем
-- ...

COMMIT;

		

Пример 2: «Тихий убийца производительности», или утечка памяти на фронтенде

Ситуация: пользователи жалуются, что после получаса работы в SPA (Single Page Application) сайт начинает тормозить, «съедать» память, а анимации становятся «дёргаными». Перезагрузка страницы помогает.

Разбор полётов: представим, что у нас есть компонент, который при монтировании подписывается на глобальное событие (например, изменение размера окна).

			// React-подобный код
function ResizableComponent() {
  useEffect(() => {
    const handleResize = () => {
      console.log("Window resized!");
      // ... какая-то сложная логика ...
    };

    window.addEventListener('resize', handleResize);

    // А где отписка?..
  }, []);

  return <div>Я компонент, который следит за размером окна</div>;
}

		

Этот компонент может появляться и исчезать с экрана много раз (например, в модальном окне). Каждый раз, когда он появляется, useEffect навешивает новый обработчик handleResize на глобальный объект window. Но когда компонент исчезает, обработчик не удаляется.

После 10 открытий-закрытий модального окна у нас будет 10 одинаковых обработчиков. При каждом изменении размера окна браузер будет выполнять одну и ту же «сложную логику» 10 раз. Через час их будет уже сотня. Это и есть утечка памяти (Memory Leak). Ссылки на функции handleResize и их замыкания остаются в памяти, потому что на них ссылается window.

Это классика, но дьявол в деталях. Утечки могут быть куда коварнее: неотписанные WebSocket-соединения, забытые таймеры (setInterval), ссылки на DOM-элементы в замыканиях, которые мешают сборщику мусора их убрать.

Как чинить? Всегда отписываться от событий в функции очистки.

			function ResizableComponent() {
  useEffect(() => {
    const handleResize = () => { /* ... */ };
    window.addEventListener('resize', handleResize);

    // Функция очистки, которая вызовется при размонтировании компонента
    return () => {
      window.removeEventListener('resize', handleResize);
      console.log("Обработчик удалён. Память чиста!");
    };
  }, []);
  // ...
}

		

Некритичные баги: «фича, а не баг»

Это ошибки, которые не ломают основной функционал. Они могут быть визуальными, «текстовыми», или проявляться в таких редких условиях, что 99.9% пользователей их никогда не увидят.

Пример 1: «Магия чисел с плавающей запятой»

Ситуация: в корзине интернет-магазина пользователь добавляет товар за 0.1$ и товар за 0.2$. Итоговая сумма заказа отображается как 0.30000000000000004$.

Разбор полётов: это не баг кода. Это фундаментальная особенность того, как компьютеры хранят дробные числа в формате IEEE 754 (floating-point).

Если кратко: большинство десятичных дробей не могут быть точно представлены в двоичной системе счисления, так же как 1/3 не может быть точно записана в виде конечной десятичной дроби (0.3333...). Когда вы пишете 0.1, компьютер хранит ближайшее возможное двоичное представление, которое чуть-чуть больше. То же самое с 0.2. При их сложении эти микроскопические неточности накапливаются и становятся видимыми.

Почему это чаще всего некритично? Проблема чисто визуальная и не мешает работе продукта. Если на бэкенде для финансовых расчётов используются специальные типы данных (как Decimal в Python или BigDecimal в Java), то реальный платёж пройдёт на правильную сумму (0.3$).

Это отличный пример бага, который выглядит как ошибка новичка, но его корни уходят глубоко в основы информатики. Опытные разработчики знают, что с деньгами нельзя работать через float / double и всегда используют либо целочисленное представление (хранят всё в копейках/центах), либо специальные библиотеки.

Как чинить (на фронтенде)? Просто отформатировать вывод.

			const total = 0.1 + 0.2; // 0.30000000000000004
console.log(total.toFixed(2)); // "0.30"

		

Пример 2: «Восставший z-index»

Ситуация: на определённой странице выпадающее меню профиля пользователя оказывается под блоком с баннером. Кликнуть по ссылкам «Профиль» или «Выйти» невозможно. Баг воспроизводится только в Safari на macOS.

Разбор полётов: скорее всего, проблема в контексте наложения (stacking context). Многие думают, что z-index — это просто глобальный номер слоя: у кого больше, тот и выше. Но это не так.

Элемент с transform, opacity < 1, filter и некоторыми другими CSS-свойствами создаёт свой собственный «мини-мир» слоёв — stacking context. Внутри этого мира z-index работает как ожидается. Но никакой z-index: 9999 внутри одного контекста не поможет элементу перекрыть другой элемент из другого контекста, если сам родительский контекст находится «ниже».

В нашем случае, блок с баннером мог иметь, например, transform: scale(1) (для анимации при наведении), что создало новый контекст наложения. И если этот блок в DOM-дереве находится после шапки с меню, то весь его «мир» (включая фон и сам баннер) будет выше «мира» шапки.

Почему это некритично? Во-первых, не влияет на данные или безопасность. Во-вторых, проявляется только в одном браузере и на одной странице. В-третьих, функционал не блокируется полностью: пользователь может перейти на страницу профиля по прямой ссылке.

Как чинить? Вариантов несколько:

  • Убрать свойство, создающее stacking context (контекст наложения) с баннера, если оно не критично: это поможет избежать проблем с порядком наложения элементов (z-index), предотвратить случайное перекрытие модальных окон, тултипов и др., упростить управление слоями в интерфейсе;
  • Создать stacking context для родительского элемента (это элемент интерфейса, на котором активируется выпадающий список) меню, например, добавив position: relative; z-index: 1; на саму шапку;
  • Перенести элемент с меню в конец <body> через портал (как это делают в React/Vue), чтобы он не зависел от родительских контекстов.

Приоритет и серьёзность: как отличить одно от другого?

Новички часто путают эти два понятия, а ведь именно их правильное понимание экономит команде кучу времени, поскольку важно корректно выделять наиболее приоритетные и серьезные задачи — то есть срочные и важные — и лишь потом приступать к остальным.

Серьёзность (Severity): описывает, насколько сильно баг ломает продукт.

Уровень 1: приложение не работает, данные теряются.

Примеры: приложение ломается при запуске, пользователи не могут его открыть; платёжная система не сохраняет данные транзакций, и деньги «исчезают».

Уровень 2: ключевой функционал не работает, но есть обходные пути.

Пример: функция экспорта данных в CSV для пользователей не работает, они не могут получить свои данные, остальные функции работают в стандартном режиме.

Уровень 3: неключевой функционал работает некорректно.

Примеры: не работает кнопка «Поделиться» в соцсетях (если это не ключевая функция продукта); в списке товаров некорректно отображаются некоторые параметры, например, дата добавления.

Уровень 4: визуальный дефект.

Пример: опечатка в тексте.

Приоритет (Priority): этот термин определяет, насколько срочно нужно исправлять баг.

Уровень 1: чинить немедленно, бросив всё.

Пример: любой пользователь может получить доступ к чужим данным через URL (критическая уязвимость).

Уровень 2: включить в следующий спринт/релиз.

Пример: заметное падение производительности на мобильных устройствах при обработке данных в реальном времени.

Уровень 3: починить, когда будет время.

Пример: у кнопки на тёмной теме сайта некорректно отображается цвет.

Приоритет (то есть срочность) может зависеть от бизнес-контекста, сроков релиза и аудитории. Серьёзность (важность) — от технического влияния на продукт. При этом серьёзность и приоритет могут не только не совпадать по уровням, но и конфликтовать.

Например, на главной странице в названии компании замечена опечатка. Это 4-ый уровень серьёзности: сайт работает, ничего не сломано. Но при этом 1-ый уровень приоритета: ведь сайт — лицо компании, и отдел маркетинга требует исправить «ещё вчера». Что делать в подобном случае? Зависит от корпоративных правил и коммуникаций.

Важно обращать внимание на детали из контекста. Например: критическая утечка данных (уровень 1 серьезности) требует немедленного исправления (уровень 1 приоритета). Но баг с крашем приложения в редком сценарии и приоритетом 2-го уровня, если затронуты всего 0.1% пользователей. Или, допустим, сломалась опция «Экспорт в PDF» (это уровень 3 серьёзности), но клиент заплатил за неё высокую цену — значит, повышаем срочность.

Инструментарий охотника за багами: от нахождения до профилактики

Как превратить борьбу с ошибками из хаотичного тушения пожаров в контролируемый процесс? Нужен комплексный подход к «охоте на баги».

Поимка бага — лишь начало битвы. Настоящее мастерство проявляется в эффективном управлении его жизненным циклом: фиксация, приоритезация, анализ, исправление, профилактика. Давайте рассмотрим основные категории этого инструментария.

Системы отслеживания ошибок (Bug Trackers): центр управления полётами

Когда баг обнаружен, нужно выстраивать систему для регистрации, классификации, назначения, отслеживания статуса и анализа истории ошибок. Здесь в бой вступают специализированные трекеры.

Jira: мощный и гибкий инструмент с глубокими возможностями кастомизации и интеграцией практически с любой DevOps-тулчейн. Однако его богатство функций может быть избыточным и сложным для освоения в маленьких командах.

YouTrack (JetBrains): трекер отличает скорость и «умный» поиск (на естественном языке), тесная интеграция с IDE JetBrains и GitHub. Часто воспринимается как более легковесная и быстрая альтернатива Jira для команд, ценящих эффективность.

Linear: продукт с минималистичным UI, упором на клавиатурные сокращения. Подходит для стартапов и небольших команд, где важен фокус и отсутствие накладных расходов на управление самим трекером.

GitHub Issues / GitLab Issues: интегрированы напрямую в репозиторий. Удобны для open-source проектов и команд, чья разработка тесно завязана на Git-операциях (мердж-реквесты, коммиты). Прямая привязка багов к коду — их главный козырь.

Системы мониторинга и сбора ошибок: радар, ловящий баги в реальном времени

Что, если баг проявился у пользователя, а вы об этом ещё не знаете? Трекеры молчат, пока проблема не зафиксирована человеком. Системы мониторинга и сбора ошибок действуют на опережение, автоматически вылавливая сбои в работающем приложении.

Sentry: становится вашими глазами и ушами в продакшене. В реальном времени ловит исключения и ошибки на фронтенде (JavaScript, React, Vue и др.) и бэкенде (Python, Java, Node.js, Go и др.). Магия Sentry — в автоматической группировке схожих ошибок, алертах с детальным стектрейсом, контекстом (параметры запроса, данные пользователя) и даже возможностью записать шаги, приведшие к ошибке. Позволяет узнать о проблеме раньше, чем начнут сыпаться жалобы.

ELK Stack (Elasticsearch, Logstash, Kibana) / Grafana Loki: когда ошибка — лишь симптом, а корень проблемы спрятан глубоко в логике распределенной системы или инфраструктуре, нужен мощный анализ логов. Эти инструменты (особенно в паре со сборщиками логов по типу Fluentd или Promtail для Loki) собирают, индексируют и визуализируют гигантские объемы лог-данных со всех серверов и сервисов. Kibana и Grafana предоставляют мощные дашборды для поиска закономерностей, аномалий и первопричин сбоев.

Инструменты для дебага: скальпель для вскрытия проблемы

Когда баг локализован (благодаря трекеру и мониторингу), наступает время точечной работы — понять, почему он возникает и как исправить. Здесь незаменимы отладчики.

Browser DevTools (Chrome DevTools, Firefox Developer Tools): комфортный инструмент фронтенд-разработчика. Мощный отладчик JavaScript, инспектор DOM/CSS, детальный анализ сетевых запросов (заголовки, время, размеры), профилировщик производительности (выявление «бутылочных горлышек»), аудит безопасности и доступности — всё под рукой прямо в браузере.

IDE Debuggers (VS Code, IntelliJ IDEA, PyCharm и др.): дают суперспособность пошагового выполнения кода на бэкенде или даже на фронтенде (интегрируясь с браузером). Установка точек останова (breakpoints), просмотр состояния переменных в реальном времени, пошаговый проход (step into/over), оценка выражений на лету — это фундамент для понимания потока выполнения и нахождения логических ошибок.

Инструменты для профилактики: строим оборону до появления врага

Самые эффективные баги — те, которые никогда не попали в продакшен. Современные практики разработки делают ставку на автоматизированную профилактику ошибок на этапе написания кода. Вот ключевые союзники в этом:

  • ESLint (JavaScript/TypeScript), статический анализатор кода — сканирует код до запуска, выявляя потенциальные баги, антипаттерны и нарушения соглашений по стилю. Находит опечатки, необъявленные переменные, опасные конструкции (напр., console.log в prod), потенциальные утечки памяти. Многие ошибки (например, сравнение == вместо === по правилу eqeqeq) может исправить автоматически (--fix). Интеграция в редактор (VS Code, WebStorm) и CI/CD пайплайны перехватывает ошибки мгновенно.
  • SonarQube (25+ языков) для непрерывного контроля качества кода. Идёт глубже ESLint, выискивая сложные баги, уязвимости безопасности (OWASP Top 10: SQL-инъекции, XSS) и «запахи кода», ведущие к будущим проблемам. Выявляет критические ошибки: разыменование null (Null Pointer Exception), утечки ресурсов (файлы, соединения), возможные состояния гонки (race conditions). Оценивает технический долг и ключевые метрики (сложность кода, покрытие тестами), помогая поддерживать здоровье кодовой базы. Работает как часть CI/CD, предоставляя наглядные дашборды.
  • Prettier (50+ языков) бескомпромиссно применяет единые стилистические правила (отступы, точки с запятой, переносы строк, кавычки). Устраняет целый класс потенциальных ошибок, связанных с неочевидной работой парсера из-за форматирования (напр., Automatic Semicolon Insertion в JS). Фокусирует код-ревью на логике, а не на пробелах.
  • Проактивная профилактика: Prettier, ESLint, SonarQube автоматически блокируют огромный пласт рутинных ошибок и уязвимостей до того момента, как код попадет в репозиторий или сборку.
  • Раннее выявление: Sentry, ELK/Loki мгновенно сигнализируют о сбоях в работе приложения, минимизируя время реакции и воздействие на пользователей.
  • Эффективный менеджмент: Jira, YouTrack, Linear, GitHub Issues обеспечат прозрачность, контроль и анализ потока ошибок.
  • DevTools, IDE Debuggers дают разработчику возможность точно диагностировать и исправлять корневые причины сложных багов. 
Важно: максимальный эффект достигается при интеграции этих инструментов в CI/CD пайплайн. Prettier, ESLint и статический анализ SonarQube должны запускаться автоматически на каждый пул-реквест, блокируя мердж в основную ветку при обнаружении проблем. Сборка с ошибками или уязвимостями просто не должна попадать дальше. Это создает культуру качества и экономит сотни часов на исправлении «глупых» багов, позволяя команде сосредоточиться на сложных задачах и инновациях.
Следите за новыми постами
Следите за новыми постами по любимым темам
224 открытий3К показов