Понедельник, 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
- Споры про
camelCasevssnake_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" → баги проходят в продакшен.
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:
-
Не защищайтесь рефлекторно
Первая реакция на критику — защита. Подавите её. Прочитайте комментарий второй раз.
-
Задавайте уточняющие вопросы
Спасибо за комментарий! Уточню: - Ты имеешь в виду вынести всю логику валидации или только email? - Есть пример паттерна, который ты предлагаешь? -
Соглашайтесь или аргументируйте
✅ Согласие:
Отличная идея, не подумал про это. Сейчас исправлю.✅ Аргумент:
Понимаю твою точку зрения. Я делал так потому что: 1. [причина 1] 2. [причина 2] Как думаешь, это меняет ситуацию? -
Благодарите за фидбэк
Спасибо за ревью! Ценный фидбэк, не знал про этот паттерн.
Автоматизация рутины: 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 риски
Установка:
- Добавьте GitHub App: CodeRabbit
- Настройте
.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
Метрики эффективности 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 часа.
Решение:
- Внедрили чек-лист для ревью (из этой статьи)
- Минимальное время ревью: для PR > 200 строк ревью не может быть одобрено быстрее, чем через 30 минут (физически невозможно прочитать)
- Review Coverage метрика: цель > 80%
- Правило: минимум 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-on-1 с Павлом: объяснил, что критика кода ≠ критика человека
- Обучение: техника "сэндвича", использование префиксов
[MAJOR],[MINOR] - Правило команды: "критикуем код, а не человека"
- Примеры хороших комментариев в wiki
Результат (после 2 месяцев):
- Павел начал писать конструктивные комментарии
- Новый джун в команде: "Павел лучший ментор, многому учусь"
- Атмосфера в команде улучшилась
Урок: культура фидбэка важнее процесса.
Кейс #3: PR висит 5 дней — контекст потерян
Проблема:
PR создан в понедельник. Первое ревью — в пятницу.
Что происходит:
- Андрей уже переключился на 2 другие задачи
- Контекст потерян
- Комментарии ревьюера непонятны (код уже забыт)
- Merge conflicts (другие изменения в master)
Решение:
- SLA на ревью: первое ревью в течение 4 часов
- Бот напоминает: если PR без ревью > 4 часов → пингует в Slack
- Rotation ревьюеров: каждый день кто-то дежурный
- Приоритет: ревью важнее новых задач
Результат:
- 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 — это не барьер. Это инвестиция.
Инвестиция в:
- Качество кода (меньше багов в продакшене)
- Рост команды (обучение через фидбэк)
- Знания (больше людей знают, как работает система)
- Культуру (открытость, доверие, конструктивность)
Статистика не врёт:
Помните:
"Code Review — это не про поиск ошибок. Это про рост команды. Каждый PR — это возможность научиться и научить."
Ключевые принципы:
- Автоматизируйте рутину: форматирование, тесты, security — это работа для CI, а не для человека
- Критикуйте код, а не человека: конструктивный фидбэк, а не ego battle
- Измеряйте эффективность: метрики показывают, работает ли ваш процесс
- Делайте быстро: PR > 24 часов без ревью = потеря контекста
- Учите и учитесь: CR — это главный инструмент обучения в команде
Что делать прямо сейчас
Если у вас ещё нет процесса CR:
- Создайте
.github/PULL_REQUEST_TEMPLATE.md(шаблон выше) - Настройте pre-commit hooks (5 минут)
- Настройте CI проверки (15 минут)
- Проведите 30-минутный sync с командой про культуру CR
Если у вас есть процесс, но он формальный:
- Измерьте текущие метрики (Time to Review, Coverage, Bugs)
- Проведите ретро: что не работает в текущем CR?
- Внедрите классификацию комментариев ([CRITICAL], [MAJOR], etc.)
- Установите SLA: первое ревью в течение 4 часов
Если у вас хороший процесс:
- Автоматизируйте ещё больше (AI ревьюеры, автокомментарии)
- Научите мидлов делать качественное ревью
- Соберите примеры лучших комментариев в wiki
- Поделитесь опытом с другими командами
Первый шаг (займёт 5 минут): создайте PR template прямо сейчас. Это самое простое изменение с максимальным эффектом.
Полезные ресурсы
Книги:
- "The Art of Readable Code" (Boswell, Foucher) — как писать читаемый код
- "Code Complete" (Steve McConnell) — классика про качество кода
- "Accelerate" (Nicole Forsgren) — метрики эффективности DevOps
Статьи:
- Google Engineering Practices: Code Review
- Atlassian: Code Review Best Practices
- Microsoft: Code Review Guidelines
Инструменты:
- 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 — пишу про разработку, архитектуру и культуру команды. Только практика, без воды.


