Перейти к содержимому

Code Review: от формальности к инструменту роста команды

Константин Потапов
42 min

Code Review в 80% команд — это театр. «LGTM» через 30 секунд, формальная галочка перед мержем, потерянное время. Я 12 лет наблюдаю, как ревью убивает продуктивность вместо того, чтобы её повышать. Вот система, которая превращает CR из бюрократии в главный инструмент роста команды.

Code Review: от формальности к инструменту роста команды

Понедельник, 10:37. Андрей (мидл) открывает Pull Request. Рефакторинг платёжного модуля, 847 строк кода, 3 дня работы.

11:42. Первое ревью от Димы (синьор): "LGTM 👍"

Время ревью: 30 секунд. Да, тридцать секунд на 847 строк.

12:15. Второе ревью от меня (техлид): "Approved ✅"

Время ревью: 2 минуты. Я пролистал файлы, проверил, что тесты зелёные, и нажал кнопку.

14:00. Андрей мержит PR в мастер.

14:47. Production падает. Платежи не работают. Клиенты не могут оплачивать заказы.

15:30. Находим баг. Андрей в рефакторинге случайно изменил логику обработки ошибок от платёжного провайдера. Раньше система ретраила запрос 3 раза. Теперь — отменяет транзакцию сразу.

Результат: $127,000 упущенной выгоды за 4 часа (40% транзакций завершились ошибкой вместо успеха), 6 часов работы команды на hotfix, 230 тикетов в саппорт от бесящихся клиентов.

Вопрос: почему два опытных разработчика пропустили критический баг на ревью?

Ответ: потому что наше Code Review было театром. Формальностью. Галочкой.


Эта история случилась 4 года назад. С тех пор я полностью переосмыслил подход к Code Review. Вот что я узнал.

Почему Code Review превращается в театр

Симптомы больного процесса

Проверьте себя. Если хотя бы 3 пункта про вашу команду — у вас проблема:

❌ "LGTM" синдром:

  • Ревью занимает меньше минуты на 200+ строк кода
  • Комментарии поверхностные или их нет вообще
  • Одобрение без реального чтения кода

❌ Nitpicking война:

  • 90% комментариев — про форматирование и naming
  • Споры про camelCase vs snake_case на полчаса
  • Игнорирование архитектурных проблем

❌ Ревью-блокер:

  • PR висит без ревью 3+ дня
  • Разработчик заблокирован, переключается на другую задачу
  • Context switch убивает продуктивность

❌ Ego battle:

  • Комментарии звучат как нападение ("это ужасно", "зачем ты так сделал?")
  • Автор PR воспринимает ревью как личную критику
  • Защитная реакция вместо конструктивного диалога

❌ Отсутствие метрик:

  • Не знаете, сколько времени занимает среднее ревью
  • Не знаете, сколько багов находится на ревью
  • Не знаете, сколько PR висят больше 24 часов

Статистика SmartBear (2023): 60% команд проводят Code Review формально. Основные причины: нет времени (43%), непонятные критерии (38%), отсутствие культуры обратной связи (29%). Результат: 70% серьёзных багов проходят через ревью незамеченными.

Корневые причины проблемы

1. CR воспринимается как барьер, а не инструмент

Разработчик думает: "Мне нужно пройти ревью, чтобы замержить код."

Должен думать: "Мне нужно получить фидбэк, чтобы улучшить решение."

2. Нет чётких критериев

Что проверять? Как глубоко? Когда одобрять, а когда просить изменения?

Без ответов на эти вопросы каждый ревьюит по-своему.

3. Неправильные стимулы

Метрики команды: velocity, количество закрытых тасок, time to production.

Метрики CR: никаких.

Результат: ревью откладывается, делается поверхностно, воспринимается как потеря времени.

4. Отсутствие психологической безопасности

Разработчик боится критики → делает минимальные PR → не экспериментирует → не растёт.

Ревьюер боится обидеть → пишет "LGTM" → баги проходят в продакшен.

Театр CR
Настоящий CR
Восприятие CR
Барьер перед мержем
Инструмент роста
Цель ревьюера
Одобрить быстрее
Найти улучшения
Метрики
Velocity, закрытые таски
Качество кода, багов на проде
Культура
Избегание конфликтов
Конструктивный фидбэк

Checklist, который работает

Перепробовал десятки чек-листов. Работает только этот — проверенный на 20+ проектах.

Структура ревью: что проверять и в каком порядке

Уровень 1: Автоматика (30 секунд)

Это должно быть автоматизировано, а не проверяться человеком.

  • Тесты проходят (CI зелёный)
  • Линтер без ошибок
  • Форматирование код-стайл
  • Security сканеры (SAST, dependency check)
  • Coverage не упал (или есть объяснение)

Правило: если ревьюер тратит время на проверку форматирования или запуск тестов вручную — процесс сломан. Это работа для CI, а не для человека.

Инструменты:

# .github/workflows/pull-request.yml
name: PR Checks
on: pull_request
 
jobs:
  checks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Run linter
        run: npm run lint
 
      - name: Run tests
        run: npm test
 
      - name: Check coverage
        run: npm run test:coverage
        # Fail if coverage < 80%
 
      - name: Security scan
        uses: snyk/actions/node@master
 
      - name: Check formatting
        run: npm run format:check

Результат: если CI красный — ревью даже не начинается.

Уровень 2: Функциональность (5-10 минут)

Проверяем, что код делает то, что должен.

  • Требования выполнены (чекнуть описание таски/PR)
  • Граничные случаи обработаны (что если null, пустой массив, 0, очень большое число?)
  • Ошибки обрабатываются (нет потенциальных exception/crash)
  • Логика понятна и корректна

Пример комментария:

// ❌ Плохо
function calculateDiscount(user, total) {
  if (user.isVIP) {
    return total * 0.2;
  }
  return total * 0.1;
}
 
// ✅ Комментарий на ревью:
// Что происходит, если total === 0 или user === null?
// Нужна валидация входных параметров:
 
function calculateDiscount(user, total) {
  if (!user || total <= 0) {
    throw new Error("Invalid input");
  }
 
  const discountRate = user.isVIP ? 0.2 : 0.1;
  return total * discountRate;
}

Уровень 3: Архитектура и дизайн (10-15 минут)

Проверяем, что решение вписывается в систему.

  • Согласованность с существующей архитектурой
  • Нет дублирования кода (DRY principle)
  • Разделение ответственности (Single Responsibility)
  • Абстракции на правильном уровне (не переусложнение, но и не копипаста)
  • Dependency direction корректен (нет циклических зависимостей)

Пример комментария:

# ❌ Плохо (дублирование логики валидации)
class UserService:
    def create_user(self, data):
        if not data.get('email'):
            raise ValueError('Email required')
        if not re.match(r'^[\w\.-]+@[\w\.-]+\.\w+$', data['email']):
            raise ValueError('Invalid email')
        # ...
 
    def update_user(self, user_id, data):
        if not data.get('email'):
            raise ValueError('Email required')
        if not re.match(r'^[\w\.-]+@[\w\.-]+\.\w+$', data['email']):
            raise ValueError('Invalid email')
        # ...
 
# ✅ Комментарий на ревью:
# Логика валидации email дублируется.
# Предлагаю вынести в отдельный валидатор:
 
class EmailValidator:
    @staticmethod
    def validate(email):
        if not email:
            raise ValueError('Email required')
        if not re.match(r'^[\w\.-]+@[\w\.-]+\.\w+$', email):
            raise ValueError('Invalid email')
        return email.lower()
 
class UserService:
    def create_user(self, data):
        email = EmailValidator.validate(data.get('email'))
        # ...

Уровень 4: Производительность и масштабируемость (5-10 минут)

Проверяем, что код не станет узким местом.

  • Нет N+1 запросов (особенно в БД)
  • Сложные операции не в циклах
  • Индексы на нужных полях БД (если есть миграции)
  • Нет memory leaks (особенно для long-running процессов)
  • Pagination для больших списков

Пример комментария:

# ❌ Плохо (N+1 query problem)
def get_users_with_orders():
    users = User.objects.all()
    result = []
    for user in users:
        # Каждая итерация = новый запрос к БД
        orders = Order.objects.filter(user_id=user.id).count()
        result.append({
            'user': user,
            'orders_count': orders
        })
    return result
 
# ✅ Комментарий на ревью:
# Классическая N+1 проблема. Для 1000 юзеров = 1001 запрос к БД.
# Используй annotate:
 
def get_users_with_orders():
    return User.objects.annotate(
        orders_count=Count('orders')
    ).all()
 
# Теперь: 1 запрос вместо 1001

Уровень 5: Безопасность (5 минут)

Проверяем уязвимости.

  • Нет SQL injection (используем параметризованные запросы)
  • Нет XSS (экранирование пользовательского ввода)
  • Нет hardcoded секретов (API keys, пароли)
  • Авторизация проверяется (не только аутентификация)
  • Rate limiting (если это API endpoint)
  • Input validation (не доверяем пользовательскому вводу)

Пример комментария:

# ❌ Плохо (SQL injection)
def get_user_by_email(email):
    query = f"SELECT * FROM users WHERE email = '{email}'"
    return db.execute(query)
 
# Атака: email = "' OR '1'='1"
# Запрос станет: SELECT * FROM users WHERE email = '' OR '1'='1'
# Результат: вернутся ВСЕ пользователи
 
# ✅ Комментарий на ревью:
# SQL injection уязвимость. Используй параметризованные запросы:
 
def get_user_by_email(email):
    query = "SELECT * FROM users WHERE email = ?"
    return db.execute(query, [email])

Уровень 6: Читаемость и поддерживаемость (5 минут)

Проверяем, что код будут понимать через 6 месяцев.

  • Naming понятен (функции, переменные, классы)
  • Комментарии там, где логика неочевидна (но не там, где код сам себя объясняет)
  • Функции короткие (< 50 строк как правило)
  • Cyclomatic complexity низкая (< 10 на функцию)
  • Нет magic numbers (константы вынесены)

Пример комментария:

// ❌ Плохо
function p(u, a) {
  if (u.t === 1 && a > 100) {
    return a * 0.15;
  } else if (u.t === 2) {
    return a * 0.25;
  }
  return 0;
}
 
// ✅ Комментарий на ревью:
// Непонятно, что делает функция. Предлагаю переименовать:
 
enum UserTier {
  BASIC = 1,
  PREMIUM = 2,
}
 
const BASIC_DISCOUNT_THRESHOLD = 100;
const BASIC_DISCOUNT_RATE = 0.15;
const PREMIUM_DISCOUNT_RATE = 0.25;
 
function calculateDiscount(user: User, amount: number): number {
  if (user.tier === UserTier.BASIC && amount > BASIC_DISCOUNT_THRESHOLD) {
    return amount * BASIC_DISCOUNT_RATE;
  }
 
  if (user.tier === UserTier.PREMIUM) {
    return amount * PREMIUM_DISCOUNT_RATE;
  }
 
  return 0;
}

Уровень 7: Тесты (5-10 минут)

Проверяем, что код протестирован правильно.

  • Есть тесты на новую функциональность
  • Happy path покрыт
  • Edge cases покрыты (null, empty, граничные значения)
  • Error cases покрыты (что если всё идёт не так)
  • Тесты понятны и изолированы (не зависят друг от друга)
  • Нет флакующих тестов

Пример комментария:

# ❌ Плохо (только happy path)
def test_calculate_discount():
    user = User(is_vip=True)
    discount = calculate_discount(user, 100)
    assert discount == 20
 
# ✅ Комментарий на ревью:
# Нужны тесты на edge cases:
 
def test_calculate_discount_happy_path():
    user = User(is_vip=True)
    assert calculate_discount(user, 100) == 20
 
def test_calculate_discount_zero_amount():
    user = User(is_vip=True)
    with pytest.raises(ValueError):
        calculate_discount(user, 0)
 
def test_calculate_discount_null_user():
    with pytest.raises(ValueError):
        calculate_discount(None, 100)
 
def test_calculate_discount_non_vip():
    user = User(is_vip=False)
    assert calculate_discount(user, 100) == 10

Чек-лист в виде шаблона для PR

Сохраните в .github/PULL_REQUEST_TEMPLATE.md:

## Описание изменений
 
<!-- Что делает этот PR? Зачем нужен? -->
 
## Связанные задачи
 
<!-- Closes #123 -->
 
## Тип изменений
 
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Refactoring
- [ ] Documentation
 
## Чек-лист автора PR
 
- [ ] Код соответствует code style проекта
- [ ] Самоpревью проведено (прочитал свой код как ревьюер)
- [ ] Комментарии добавлены в сложных местах
- [ ] Документация обновлена (если нужно)
- [ ] Тесты добавлены/обновлены
- [ ] Все тесты проходят локально
- [ ] Нет breaking changes (или они задокументированы)
 
## Чек-лист для ревьюера
 
- [ ] Функциональность работает корректно
- [ ] Обработаны граничные случаи
- [ ] Нет архитектурных проблем
- [ ] Нет проблем с производительностью
- [ ] Нет security уязвимостей
- [ ] Код читаем и понятен
- [ ] Тесты адекватные
 
## Screenshots (если UI)
 
<!-- Добавьте скриншоты до/после -->
 
## Вопросы к ревьюерам
 
<!-- Есть неуверенность в каком-то решении? Спросите здесь -->

Как давать feedback, который не демотивирует

Главный принцип: критикуйте код, а не человека

❌ Плохо:

"Ты написал ужасный код."

"Как ты вообще думал, что это будет работать?"

"Это же очевидно неправильно."

✅ Хорошо:

"Этот подход может привести к проблемам с производительностью. Предлагаю рассмотреть альтернативу."

"Логика здесь неочевидна. Можно добавить комментарий?"

"Есть риск race condition. Как думаешь про добавление блокировки?"

Техника "сэндвича" для сложного фидбэка

Структура: позитив → критика → позитив + предложение

Пример:

## Review comment:
 
Круто, что добавил обработку ошибок — раньше тут был голый запрос без проверок. 👍
 
Одна зона риска: сейчас retry logic делает 10 попыток без задержки.
Это может создать нагрузку на API при сбое.
 
Предлагаю добавить exponential backoff:
 
- 1я попытка: сразу
- 2я попытка: через 1 сек
- 3я попытка: через 2 сек
- и т.д.
 
Вот пример реализации: [ссылка на либу или код]
 
Как думаешь?

Почему работает:

  • Признаёте хорошее (человек не чувствует, что всё плохо)
  • Фокус на проблеме, а не на человеке
  • Конкретное предложение (а не просто "плохо")
  • Вопрос в конце (диалог, а не приказ)

Классификация комментариев по важности

Используйте префиксы, чтобы автор понимал приоритет:

[CRITICAL] — блокер, нужно исправить

[CRITICAL] SQL injection уязвимость в строке 47.
Используй параметризованные запросы вместо f-string.

[MAJOR] — серьёзная проблема, желательно исправить

[MAJOR] N+1 query problem. Для 10k пользователей это будет 10k запросов к БД.
Предлагаю использовать select_related().

[MINOR] — небольшое улучшение, можно игнорировать

[MINOR] Переменная `data` слишком общее название.
Может быть `userData` или `profileData`?

[NITPICK] — придирка, на ваше усмотрение

[NITPICK] Лично мне больше нравится guard clause вместо вложенного if,
но это на вкус.

[QUESTION] — вопрос для понимания

[QUESTION] Почему здесь используется setTimeout, а не Promise?
Хочу понять логику.

[PRAISE] — позитивный фидбэк

[PRAISE] Отличное решение с мемоизацией!
Не подумал бы про это сам.

Исследование Google (2019): команды, которые различают критичность комментариев, мержат PR на 40% быстрее и имеют на 60% меньше конфликтов в ревью.

Примеры конструктивных комментариев

1. Проблема с производительностью

❌ Плохо:

"Это будет медленно работать."

✅ Хорошо:

[MAJOR] Потенциальная проблема с производительностью:
 
Цикл по всем пользователям (строка 23-27) будет работать O(n\*m),
где n = пользователи, m = заказы.
 
Для 10k пользователей с 100 заказами у каждого = 1M итераций.
 
Предлагаю:
 
1. Использовать JOIN вместо вложенных циклов
2. Или загружать только нужных пользователей с фильтром
 
Пример:
 
```python
# Вместо
for user in all_users:
    for order in user.orders:
        process(order)
 
# Сделать
orders = Order.objects.filter(
    user__is_active=True
).select_related('user')
 
for order in orders:
    process(order)
```

Как думаешь?


#### 2. Архитектурное замечание

**❌ Плохо:**

> "Это нарушает принципы SOLID."

**✅ Хорошо:**

```markdown
[MAJOR] Архитектурный момент:

Класс `UserService` сейчас отвечает за:
- Создание пользователей
- Отправку email
- Валидацию
- Работу с БД

Это нарушает Single Responsibility Principle.

Предлагаю разделить:
- `UserRepository` — работа с БД
- `EmailService` — отправка писем
- `UserValidator` — валидация

Тогда `UserService` будет только оркестрировать эти сервисы.

Могу помочь с рефакторингом, если хочешь.

3. Вопрос для понимания

❌ Плохо:

"Зачем ты это сделал?"

✅ Хорошо:

[QUESTION] Хочу понять логику:
 
Почему используется `setTimeout(..., 0)` вместо прямого вызова?
 
Возможно, я что-то упускаю, но кажется, что прямой вызов был бы проще.
Есть какой-то edge case, который я не вижу?

4. Позитивный фидбэк

❌ Плохо:

"LGTM"

✅ Хорошо:

[PRAISE] Отлично сделано! Особенно понравилось:
 
1. Добавил comprehensive тесты (даже edge cases)
2. Рефакторинг error handling стал намного чище
3. Комментарии в сложных местах — очень помогают
 
Маленький нит: можно упростить строку 47 через destructuring,
но это детали.
 
Approved ✅

Как реагировать на фидбэк (для авторов PR)

Правила для автора PR:

  1. Не защищайтесь рефлекторно

    Первая реакция на критику — защита. Подавите её. Прочитайте комментарий второй раз.

  2. Задавайте уточняющие вопросы

    Спасибо за комментарий! Уточню:
     
    - Ты имеешь в виду вынести всю логику валидации или только email?
    - Есть пример паттерна, который ты предлагаешь?
  3. Соглашайтесь или аргументируйте

    ✅ Согласие:

    Отличная идея, не подумал про это. Сейчас исправлю.

    ✅ Аргумент:

    Понимаю твою точку зрения. Я делал так потому что:
     
    1. [причина 1]
    2. [причина 2]
     
    Как думаешь, это меняет ситуацию?
  4. Благодарите за фидбэк

    Спасибо за ревью! Ценный фидбэк, не знал про этот паттерн.

Автоматизация рутины: pre-commit hooks и CI

Pre-commit hooks: первая линия защиты

Цель: поймать проблемы до того, как код попадёт в PR.

Установка pre-commit

# Установка
npm install --save-dev husky lint-staged
 
# Инициализация
npx husky init

Конфигурация .husky/pre-commit

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
 
npx lint-staged

Конфигурация lint-staged.config.js

module.exports = {
  // TypeScript/JavaScript файлы
  "*.{ts,tsx,js,jsx}": [
    "eslint --fix", // Автофикс проблем
    "prettier --write", // Форматирование
    "vitest related --run", // Тесты связанных файлов
  ],
 
  // Python файлы
  "*.py": [
    "black", // Форматирование
    "isort", // Сортировка импортов
    "flake8", // Линтинг
    "mypy", // Type checking
    "pytest --testmon", // Тесты
  ],
 
  // CSS/SCSS
  "*.{css,scss}": ["stylelint --fix", "prettier --write"],
 
  // Markdown
  "*.md": ["markdownlint --fix", "prettier --write"],
 
  // JSON/YAML
  "*.{json,yaml,yml}": ["prettier --write"],
};

Результат: ваш код всегда чистый, отформатированный и протестированный до коммита.

CI/CD: автоматизация проверок в Pull Request

GitHub Actions пример

# .github/workflows/pull-request.yml
name: Pull Request Checks
 
on:
  pull_request:
    types: [opened, synchronize, reopened]
 
jobs:
  # Быстрые проверки (должны пройти за < 2 мин)
  quick-checks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"
 
      - name: Install dependencies
        run: npm ci
 
      - name: Lint
        run: npm run lint
 
      - name: Type check
        run: npm run type-check
 
      - name: Format check
        run: npm run format:check
 
  # Тесты (могут идти дольше)
  tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0 # Нужно для coverage diff
 
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"
 
      - name: Install dependencies
        run: npm ci
 
      - name: Run tests
        run: npm run test:coverage
 
      - name: Coverage report
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/coverage-final.json
          fail_ci_if_error: true
 
      - name: Check coverage threshold
        run: |
          COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
          if (( $(echo "$COVERAGE < 80" | bc -l) )); then
            echo "Coverage is below 80%: $COVERAGE%"
            exit 1
          fi
 
  # Security проверки
  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Run Snyk security scan
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          args: --severity-threshold=high
 
      - name: Check for secrets
        uses: trufflesecurity/trufflehog@main
        with:
          path: ./
 
  # Bundle size проверка
  bundle-size:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
 
      - name: Install dependencies
        run: npm ci
 
      - name: Build
        run: npm run build
 
      - name: Check bundle size
        uses: andresz1/size-limit-action@v1
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
 
  # Complexity анализ
  complexity:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
 
      - name: Install dependencies
        run: npm ci
 
      - name: Analyze complexity
        run: |
          npx eslint . \
            --format json \
            --output-file eslint-report.json \
            || true
 
          # Fail если есть функции с complexity > 15
          COMPLEX_FUNCS=$(cat eslint-report.json | \
            jq '[.[] | .messages[] | select(.ruleId == "complexity")] | length')
 
          if [ "$COMPLEX_FUNCS" -gt 0 ]; then
            echo "Found $COMPLEX_FUNCS functions with complexity > 15"
            exit 1
          fi
 
  # Автокомментарии в PR
  pr-comments:
    runs-on: ubuntu-latest
    needs: [quick-checks, tests, security]
    if: always()
    steps:
      - name: Comment PR
        uses: actions/github-script@v6
        with:
          script: |
            const status = '${{ needs.tests.result }}' === 'success' ? '✅' : '❌';
            const coverage = '${{ needs.tests.outputs.coverage }}';
 
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `## Code Review Checks ${status}\n\n- Coverage: ${coverage}%\n- All checks passed!`
            });

Автоматические ревьюеры (AI помощники)

1. Danger JS — автоматические комментарии

// dangerfile.ts
import { danger, warn, fail, message } from "danger";
 
// PR должен иметь описание
if (!danger.github.pr.body || danger.github.pr.body.length < 10) {
  fail("Пожалуйста, добавьте описание к PR");
}
 
// PR не должен быть слишком большим
const bigPRThreshold = 500;
if (danger.github.pr.additions + danger.github.pr.deletions > bigPRThreshold) {
  warn(
    `Большой PR (${danger.github.pr.additions + danger.github.pr.deletions} строк). Рассмотрите разбиение на несколько PR.`
  );
}
 
// Проверка, что изменения в package.json сопровождаются изменениями в package-lock.json
const packageChanged = danger.git.modified_files.includes("package.json");
const lockfileChanged = danger.git.modified_files.includes("package-lock.json");
 
if (packageChanged && !lockfileChanged) {
  fail(
    "package.json изменён, но package-lock.json нет. Запустите npm install."
  );
}
 
// Напоминание о тестах
const hasAppChanges = danger.git.modified_files.some((f) =>
  f.startsWith("src/")
);
const hasTestChanges = danger.git.modified_files.some(
  (f) => f.includes(".test.") || f.includes(".spec.")
);
 
if (hasAppChanges && !hasTestChanges) {
  warn("Изменения в коде, но нет изменений в тестах. Добавьте тесты?");
}
 
// Проверка TODO комментариев
const todos = danger.git.modified_files
  .filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"))
  .flatMap((f) => {
    const content = fs.readFileSync(f, "utf8");
    return content
      .split("\n")
      .map((line, i) => ({ line: i + 1, text: line }))
      .filter((l) => l.text.includes("TODO") || l.text.includes("FIXME"));
  });
 
if (todos.length > 0) {
  message(
    `Найдено ${todos.length} TODO/FIXME комментариев. Не забудьте их решить.`
  );
}

2. GitHub Copilot для PR (Beta)

# Автоматическое генерирование описания PR
name: AI PR Description
on:
  pull_request:
    types: [opened]
 
jobs:
  generate-description:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Generate PR description
        uses: github/copilot-pr-description@v1
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}

3. CodeRabbit / CodeGuru — AI ревью

CodeRabbit анализирует PR и оставляет комментарии:

  • Находит баги
  • Предлагает оптимизации
  • Проверяет best practices
  • Оценивает security риски

Установка:

  1. Добавьте GitHub App: CodeRabbit
  2. Настройте .coderabbit.yaml:
reviews:
  auto_review: true
  request_changes_workflow: false
 
  checks:
    - name: "Security"
      enabled: true
    - name: "Performance"
      enabled: true
    - name: "Best Practices"
      enabled: true
    - name: "Code Smell"
      enabled: true
 
  filters:
    - "!**/*.test.*"
    - "!**/node_modules/**"

Результат: AI оставляет комментарии в PR автоматически.

Инструменты для автоматизации CR

Husky + lint-staged
Pre-commit hooks
GitHub Actions
CI/CD проверки
Danger JS
Автокомментарии
CodeRabbit
AI ревьюер

Метрики эффективности Code Review

Нельзя управлять тем, что не измеряешь.

Ключевые метрики

1. Time to First Review (время до первого ревью)

Что измеряет: сколько времени PR висит без ревью после создания.

Формула:

Time to First Review = Время первого комментария - Время создания PR

Как измерить:

# GitHub CLI
gh pr list --state all --json number,createdAt,reviews \
  --jq '.[] | select(.reviews | length > 0) |
       {pr: .number,
        time_to_review: (.reviews[0].submittedAt - .createdAt)}'

Бенчмарки:

  • < 4 часа: Отлично
  • 4-24 часа: Хорошо
  • 24-48 часов: Плохо (context switch убивает продуктивность)
  • > 48 часов: Критично (PR устарел, конфликты, демотивация)

2. Review Throughput (скорость ревью)

Что измеряет: сколько времени от создания PR до мержа.

Формула:

Review Throughput = Время мержа - Время создания PR

Инструмент:

// Скрипт для анализа (Node.js)
const { Octokit } = require("@octokit/rest");
 
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
 
async function analyzeReviewThroughput(owner, repo) {
  const prs = await octokit.pulls.list({
    owner,
    repo,
    state: "closed",
    per_page: 100,
  });
 
  const merged = prs.data.filter((pr) => pr.merged_at);
 
  const throughputs = merged.map((pr) => {
    const created = new Date(pr.created_at);
    const merged = new Date(pr.merged_at);
    const hours = (merged - created) / (1000 * 60 * 60);
    return { pr: pr.number, hours };
  });
 
  const avg =
    throughputs.reduce((sum, t) => sum + t.hours, 0) / throughputs.length;
 
  console.log(`Average throughput: ${avg.toFixed(1)} hours`);
  console.log(
    `Median: ${median(throughputs.map((t) => t.hours)).toFixed(1)} hours`
  );
}

Бенчмарки:

  • < 24 часа: Отлично
  • 24-72 часа: Хорошо
  • > 1 недели: Плохо

3. Review Coverage (охват ревью)

Что измеряет: сколько процентов PR получают реальное ревью (не просто LGTM).

Метрика:

Review Coverage = (PR с 2+ комментариями / Всего PR) × 100%

Как измерить:

import requests
 
def analyze_review_coverage(owner, repo, token):
    headers = {'Authorization': f'token {token}'}
 
    # Получить все PR за последний месяц
    prs = requests.get(
        f'https://api.github.com/repos/{owner}/{repo}/pulls',
        params={'state': 'closed', 'per_page': 100},
        headers=headers
    ).json()
 
    reviewed = 0
    for pr in prs:
        # Получить комментарии к PR
        reviews = requests.get(
            pr['url'] + '/reviews',
            headers=headers
        ).json()
 
        # Считаем PR "отревьюенным", если > 2 комментариев
        if len(reviews) > 2:
            reviewed += 1
 
    coverage = (reviewed / len(prs)) * 100
    print(f'Review Coverage: {coverage:.1f}%')

Бенчмарки:

  • > 80%: Отлично
  • 60-80%: Хорошо
  • < 60%: Плохо (много формальных ревью)

4. Bugs Found in Review (баги, найденные на ревью)

Что измеряет: сколько серьёзных багов поймано на ревью (а не в продакшене).

Как отслеживать:

Используйте label в GitHub:

  • bug-found-in-review — баг найден на ревью
  • bug-in-production — баг в продакшене

Метрика:

Review Effectiveness = Bugs Found in Review / (Bugs Found in Review + Bugs in Production)

Пример:

За месяц:
- Найдено на ревью: 15 багов
- Пропущено в продакшен: 5 багов

Effectiveness = 15 / (15 + 5) = 75%

Бенчмарки:

  • > 80%: Отлично (ревью работает)
  • 60-80%: Хорошо
  • < 60%: Плохо (ревью формальное)

5. Review Participation (участие в ревью)

Что измеряет: сколько человек из команды активно ревьюят.

Метрика:

Participation Rate = (Активных ревьюеров / Размер команды) × 100%

Активный ревьюер: тот, кто сделал минимум 5 ревью за месяц.

Бенчмарки:

  • > 80%: Отлично (все участвуют)
  • 60-80%: Хорошо
  • < 60%: Плохо (ревью делают только синьоры)

6. PR Size Distribution (распределение размеров PR)

Что измеряет: насколько PR разумного размера.

Метрика:

Размер PR = Additions + Deletions

Бенчмарки:

  • < 200 строк: Идеально (легко ревьюить)
  • 200-500 строк: Хорошо
  • 500-1000 строк: Плохо (сложно ревьюить)
  • > 1000 строк: Критично (никто не ревьюит всерьёз)

Статистика Google:

PR размером > 400 строк ревьюятся на 60% поверхностнее, чем PR < 200 строк.

Dashboard метрик CR

Пример дашборда в Grafana:

┌─────────────────────────────────────────────────────┐
│ Code Review Metrics Dashboard           Dec 2025    │
├─────────────────────────────────────────────────────┤
│ Time to First Review:       6.2 hours ✅            │
│ Review Throughput:         42 hours ✅              │
│ Review Coverage:              82% ✅                │
│ Bugs Found in Review:      15/month                 │
│ Bugs in Production:         3/month                 │
│ Review Effectiveness:         83% ✅                │
│ Participation Rate:           75% ⚠️                │
│                                                      │
│ PR Size Distribution:                                │
│   < 200 lines:    45% █████████                     │
│   200-500 lines:  35% ███████                       │
│   500-1000 lines: 15% ███                           │
│   > 1000 lines:    5% █                             │
└─────────────────────────────────────────────────────┘

Tracking в Jira/Linear

Создайте кастомные поля:

  • Review Time — время на ревью (часы)
  • Review Comments — количество комментариев
  • Bugs Found — баги, найденные на ревью

Автоматизация:

# GitHub Webhook → Jira update
@app.route('/webhook', methods=['POST'])
def github_webhook():
    event = request.json
 
    if event['action'] == 'closed' and event['pull_request']['merged']:
        pr = event['pull_request']
 
        # Рассчитать время на ревью
        created = datetime.fromisoformat(pr['created_at'])
        merged = datetime.fromisoformat(pr['merged_at'])
        review_time = (merged - created).total_seconds() / 3600
 
        # Получить количество комментариев
        comments = len(pr.get('comments', []))
 
        # Обновить Jira
        jira_issue = extract_jira_issue(pr['title'])
        if jira_issue:
            jira.update_issue(
                jira_issue,
                fields={
                    'customfield_10050': review_time,  # Review Time
                    'customfield_10051': comments       # Review Comments
                }
            )

Кейсы: от театра к настоящему CR

Кейс #1: "LGTM" через 30 секунд стоил $127k

Проблема:

Команда 8 человек. Code Review формальный. Синьоры ставят LGTM за секунды, не читая код.

Метрики (до):

  • Time to First Review: 15 минут ✅ (кажется круто?)
  • Review Coverage: 12% ❌ (почти нет комментариев)
  • Bugs in Production: 8/месяц ❌

Что случилось:

PR с рефакторингом платёжного модуля (847 строк). Два синьора поставили LGTM за 30 секунд и 2 минуты соответственно.

Баг в логике retry — вместо 3 попыток система отменяла транзакцию сразу.

Результат: $127k упущенной выгоды за 4 часа.

Решение:

  1. Внедрили чек-лист для ревью (из этой статьи)
  2. Минимальное время ревью: для PR > 200 строк ревью не может быть одобрено быстрее, чем через 30 минут (физически невозможно прочитать)
  3. Review Coverage метрика: цель > 80%
  4. Правило: минимум 2 ревьюера, минимум 3 комментария на 100 строк

Метрики (после 3 месяцев):

  • Time to First Review: 4 часа ⚠️ (стало дольше, но это нормально)
  • Review Coverage: 78% ✅
  • Bugs in Production: 2/месяц ✅ (снизилось в 4 раза)
  • Bugs Found in Review: 12/месяц ✅

ROI: 1 production баг = 3 дня команды на hotfix + репутация. CR экономит месяцы.

Кейс #2: Ego battle разрушил команду

Проблема:

Синьор Павел и мидл Андрей. Каждое ревью Павла — война.

Примеры комментариев Павла:

"Это ужасный код. Переделывай."

"Как ты вообще думал, что это будет работать?"

"Очевидно, нужно было использовать паттерн X."

Результат:

  • Андрей демотивирован, делает минимальные PR
  • Боится экспериментировать
  • Через 2 месяца увольняется ("не вижу роста, постоянная критика")

Решение:

  1. 1-on-1 с Павлом: объяснил, что критика кода ≠ критика человека
  2. Обучение: техника "сэндвича", использование префиксов [MAJOR], [MINOR]
  3. Правило команды: "критикуем код, а не человека"
  4. Примеры хороших комментариев в wiki

Результат (после 2 месяцев):

  • Павел начал писать конструктивные комментарии
  • Новый джун в команде: "Павел лучший ментор, многому учусь"
  • Атмосфера в команде улучшилась

Урок: культура фидбэка важнее процесса.

Кейс #3: PR висит 5 дней — контекст потерян

Проблема:

PR создан в понедельник. Первое ревью — в пятницу.

Что происходит:

  • Андрей уже переключился на 2 другие задачи
  • Контекст потерян
  • Комментарии ревьюера непонятны (код уже забыт)
  • Merge conflicts (другие изменения в master)

Решение:

  1. SLA на ревью: первое ревью в течение 4 часов
  2. Бот напоминает: если PR без ревью > 4 часов → пингует в Slack
  3. Rotation ревьюеров: каждый день кто-то дежурный
  4. Приоритет: ревью важнее новых задач

Результат:

  • Time to First Review: 5 дней → 3 часа ✅
  • Review Throughput: 7 дней → 24 часа ✅
  • Меньше merge conflicts
  • Выше продуктивность (меньше context switch)

Чек-лист внедрения культуры CR

✅ Фаза 1: Подготовка (1 неделя)

  • Проанализировать текущие метрики CR (если есть)
  • Собрать feedback команды: что не работает в текущем процессе
  • Адаптировать чек-лист из этой статьи под свой проект
  • Создать шаблон PR в .github/PULL_REQUEST_TEMPLATE.md
  • Написать гайд "Как делать Code Review" в wiki

✅ Фаза 2: Автоматизация (1 неделя)

  • Настроить pre-commit hooks (husky + lint-staged)
  • Настроить CI проверки (линтер, тесты, coverage)
  • Подключить автоматические комментарии (Danger JS)
  • Настроить Slack уведомления для PR
  • (Опционально) Подключить AI ревьюер (CodeRabbit)

✅ Фаза 3: Процесс (2 недели)

  • Провести воркшоп для команды: "Культура Code Review"
    • Почему CR важен
    • Как давать конструктивный фидбэк
    • Как реагировать на фидбэк
  • Установить SLA: первое ревью в течение 4 часов
  • Назначить rotation ревьюеров
  • Внедрить правило: минимум 2 ревьюера для критичных модулей

✅ Фаза 4: Метрики (постоянно)

  • Настроить отслеживание метрик:
    • Time to First Review
    • Review Throughput
    • Review Coverage
    • Bugs Found in Review vs Production
  • Создать дашборд метрик
  • Еженедельный ретро: что можно улучшить в процессе CR

✅ Фаза 5: Культура (3-6 месяцев)

  • Публично хвалить за качественные ревью
  • Включить CR в performance review разработчиков
  • Менторство: синьоры учат мидлов делать хорошее ревью
  • Собирать примеры отличных комментариев в wiki
  • Ежеквартально обновлять гайд на основе опыта

Главные мысли

Code Review — это не барьер. Это инвестиция.

Инвестиция в:

  • Качество кода (меньше багов в продакшене)
  • Рост команды (обучение через фидбэк)
  • Знания (больше людей знают, как работает система)
  • Культуру (открытость, доверие, конструктивность)

Статистика не врёт:

60%
Команд проводят CR формально
70%
Серьёзных багов проходят незамеченными
4x
Снижение багов при хорошем CR
40%
Рост скорости мержа с классификацией комментариев

Помните:

"Code Review — это не про поиск ошибок. Это про рост команды. Каждый PR — это возможность научиться и научить."

Ключевые принципы:

  1. Автоматизируйте рутину: форматирование, тесты, security — это работа для CI, а не для человека
  2. Критикуйте код, а не человека: конструктивный фидбэк, а не ego battle
  3. Измеряйте эффективность: метрики показывают, работает ли ваш процесс
  4. Делайте быстро: PR > 24 часов без ревью = потеря контекста
  5. Учите и учитесь: CR — это главный инструмент обучения в команде

Что делать прямо сейчас

Если у вас ещё нет процесса CR:

  1. Создайте .github/PULL_REQUEST_TEMPLATE.md (шаблон выше)
  2. Настройте pre-commit hooks (5 минут)
  3. Настройте CI проверки (15 минут)
  4. Проведите 30-минутный sync с командой про культуру CR

Если у вас есть процесс, но он формальный:

  1. Измерьте текущие метрики (Time to Review, Coverage, Bugs)
  2. Проведите ретро: что не работает в текущем CR?
  3. Внедрите классификацию комментариев ([CRITICAL], [MAJOR], etc.)
  4. Установите SLA: первое ревью в течение 4 часов

Если у вас хороший процесс:

  1. Автоматизируйте ещё больше (AI ревьюеры, автокомментарии)
  2. Научите мидлов делать качественное ревью
  3. Соберите примеры лучших комментариев в wiki
  4. Поделитесь опытом с другими командами

Первый шаг (займёт 5 минут): создайте PR template прямо сейчас. Это самое простое изменение с максимальным эффектом.


Полезные ресурсы

Книги:

  • "The Art of Readable Code" (Boswell, Foucher) — как писать читаемый код
  • "Code Complete" (Steve McConnell) — классика про качество кода
  • "Accelerate" (Nicole Forsgren) — метрики эффективности DevOps

Статьи:

Инструменты:

  • Husky — Git hooks
  • lint-staged — запуск линтеров на staged файлах
  • Danger JS — автоматические комментарии в PR
  • CodeRabbit — AI ревьюер
  • SonarQube — статический анализ кода

Метрики:

  • DORA Metrics — метрики эффективности DevOps
  • Code Climate — мониторинг качества кода

Делитесь опытом

Я собираю примеры культуры Code Review и лучшие практики. Если у вас есть интересный кейс — поделитесь:

  • Самая полезная практика в вашем CR процессе?
  • Какие метрики вы отслеживаете?
  • Были случаи, когда CR спас от критичного бага?

Пишите в комментариях или в Telegram. Обсудим процессы, поделимся опытом.

Нужна помощь с внедрением культуры Code Review? Напишите мне — проведу аудит вашего процесса, помогу настроить автоматизацию, обучу команду. Первая консультация бесплатно.

Понравилась статья? Поделитесь с коллегой, который говорит "ревью — это потеря времени" или "у нас нет времени на качественный CR". Это может спасти его проект от дорогих багов в продакшене.


Подписывайтесь на обновления в Telegram — пишу про разработку, архитектуру и культуру команды. Только практика, без воды.

Похожие материалы

·20 мин

Technical Due Diligence: как проверить код перед покупкой стартапа

Чек-лист для инвесторов и покупателей: что проверять в коде, как распознать red flags и оценить технический долг. Практическое руководство от Senior/CTO с опытом аудита 10+ проектов.

·45 min

Pydantic v2 Production Patterns: ConfigDict, валидация и сериализация для enterprise

Исчерпывающий справочник по Pydantic v2 для production: все параметры ConfigDict, 4 уровня валидации, продвинутая сериализация, error handling, security и паттерны из реальных проектов

·22 min

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

Production упал в пятницу вечером. Команда чинила до 3 ночи. В понедельник — совещание. CTO кричит: 'Кто допустил?!' Разработчики молчат. Инцидент повторится. Я видел это 40 раз. Постмортем — это не допрос. Это система, которая превращает ошибки в рост. Разбираем культуру blame-free, структуру отчетов и что делать с результатами.