Глава 8. Issues, bug reports и техническая документация

211 просмотров
0 лайков
0 в избранном
Войдите, чтобы поставить лайк. Лайков:

Введение

Issue читает человек, у которого нет твоего контекста. Нет твоего ноутбука, твоей версии зависимостей, твоего браузера с двадцатью расширениями и твоей памяти о том, что ты нажимал последние полчаса. Всё это придётся положить внутрь текста руками. Не положишь — первым ответом будет cannot reproduce, вторым тишина, а через месяц бот повесит метку stale и закроет заявку.

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

С документацией ровно то же. README, который спасает, отличается от README, который бесит, не объёмом и не грамматикой, а структурой: жанры смешаны, команды для быстрого старта нет, зато есть три абзаца о философии проекта. Разберём оба формата — от баг-репорта до README — и правила технического стиля, к которым крупные стайлгайды за годы пришли почти одинаковые.

Термины на берегу: issue — любая заявка в трекере, bug report — её частный случай про поломку, feature request — про новую функциональность. Форматы у них разные, и путать их дорого.

Заголовок issue

Заголовок — единственное, что видно в списке из двухсот заявок. По нему решают, открывать твою или пролистать. Формула: что не так + где + при каких условиях. Без эмоций, без оценок, без вопросительных знаков.

Bad:  Bug
Bad:  Doesn't work
Bad:  URGENT!!! Everything is broken, please fix ASAP
Bad:  Question about export
Bad:  I think there might be a problem somewhere in the export module?

Good: CSV export returns an empty file when a filter is applied
Good: Login fails with 500 after session timeout on Safari 18
Good: Memory usage grows by ~200 MB per hour in the image worker
Good: `parse_date` raises ValueError for ISO dates with a timezone offset

Все хорошие варианты собраны по одной схеме: подлежащее (что сломано — CSV export, login, `parse_date`), сказуемое-симптом (returns an empty file, fails with 500, raises ValueError) и условие (when a filter is applied, after session timeout on Safari 18). Три секунды — и человек понимает, его это область или нет.

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

Структура баг-репорта

Семь блоков. Названия в разных компаниях чуть разные, набор один и тот же уже лет двадцать.

  • Summary — одно-два предложения, повторяющие заголовок чуть подробнее.
  • Environment — версия приложения, ОС, браузер, окружение (staging / production), тип аккаунта.
  • Steps to reproduce — нумерованный список, каждый шаг — одно действие.
  • Expected behavior — что должно было произойти.
  • Actual behavior — что произошло на самом деле.
  • Logs / screenshots — вывод ошибки, стек-трейс, скриншот, request ID.
  • Workaround — есть ли обходной путь; это влияет на приоритет.
## Summary
CSV export produces a file with only the header row when any date filter
is applied. Without a filter the export works.

## Environment
- App version: 4.7.2 (build 1183)
- Environment: production
- Browser: Firefox 141, also reproduced in Chrome 139
- Account: admin role, workspace `acme-eu`

## Steps to reproduce
1. Open **Reports → Orders**.
2. Set the date filter to `2026-07-01 – 2026-07-31`.
3. Click **Export → CSV**.
4. Open the downloaded file.

## Expected behavior
The file contains all 412 orders shown in the table.

## Actual behavior
The file contains only the header row (168 bytes). The table on screen
still shows 412 rows.

## Logs
Request ID: `7f2c-9d10-44ab`. The backend logs:

    WARN  exporter: filter serialization failed, falling back to empty set
    INFO  exporter: wrote 0 rows in 12ms

## Workaround
Exporting without a filter and filtering in Excel works, but the full
export is 300k rows and takes about 4 minutes.

Expected и Actual новички почти всегда сливают в один блок — и зря. Разделив их, иногда обнаруживаешь, что система права, а ошибались ожидания: тогда заявка закрывается как works as intended за две минуты, а не после трёх дней раскопок в чужом коде. Обидно, но дёшево.

Отдельная норма — minimal reproducible example, минимальный воспроизводимый пример. Свёл баг к десяти строкам — резко поднял шанс, что его починят на этой неделе: порог входа для того, кто возьмётся, упал с «поднять твой проект» до «скопировать в консоль».

# minimal reproducible example
from app.dates import parse_date

parse_date("2026-07-15T10:00:00+02:00")
# ValueError: unconverted data remains: +02:00
# expected: datetime(2026, 7, 15, 10, 0, tzinfo=timezone(timedelta(hours=2)))

Почему «doesn't work» — худшая формулировка

В it doesn't work нет ни одного бита информации: что ты делал, чего ждал, что увидел — ничего. Живучесть у фразы от того, что в голове автора картинка стоит яркая, во всех подробностях, и кажется, будто она такая же у всех. Лечится одним движением: заменить «не работает» на глагол симптома.

ПустоИнформативно
It doesn't work.The request returns 500 instead of 201.
The page is broken.The page renders but the "Save" button stays disabled.
Nothing happens.Clicking "Sync" logs nothing and the status stays "idle".
It crashes.The worker exits with SIGSEGV after ~2000 messages.
Import fails.Import stops at row 4312 with "duplicate key value violates unique constraint".

Полезный набор глаголов для описания симптомов: returns (возвращает), raises / throws (бросает исключение), hangs (зависает), times out (падает по таймауту), crashes (аварийно завершается), silently fails (молча ничего не делает), renders incorrectly (отображается неправильно), leaks memory (течёт по памяти), flickers (мерцает).

$ curl -i -X POST https://api.example.com/v2/orders -H "Authorization: Bearer $TOKEN" -d '{"sku": "AB-12", "qty": 2}'

HTTP/1.1 500 Internal Server Error
x-request-id: 7f2c-9d10-44ab

{"error": "internal", "message": "unexpected token in payload"}

Вставленный вывод команды — доказательство высшей пробы: тут и шаги воспроизведения, и фактическое поведение, и request ID для поиска по логам. Три блока шаблона одним copy-paste.

Feature request

Здесь задача другая: доказать, что боль настоящая, и при этом не навязать своё решение. Отсюда и другая структура — четыре блока:

  • Problem statement — какая боль у пользователя, без упоминания решения.
  • Proposed solution — что предлагаешь.
  • Alternatives considered — что ещё рассматривал и почему отбросил.
  • Out of scope — чего эта задача НЕ делает.
## Problem
Support agents export the same three reports every morning and paste
them into a shared spreadsheet by hand. It takes about 40 minutes a day
across the team, and the numbers are stale by the time anyone reads them.

## Proposed solution
A scheduled export: pick a saved filter, a format and a time, and the
file lands in a configured S3 bucket every day.

## Alternatives considered
- A public API endpoint + a script on the customer side. Rejected:
  most support teams have no one to write or maintain that script.
- Email delivery. Rejected for now: report files regularly exceed 25 MB.

## Out of scope
- Custom report builders. This only schedules exports that already exist.
- Per-recipient permissions.

В секции Problem нет ни одного технического термина, и это не случайность. Стоит сформулировать проблему через решение — «нам нужна кнопка в настройках» — и обсуждать будут кнопку: где она, какого цвета, нужна ли роль. А сорок минут ручной работы каждое утро, ради которых всё затевалось, в разговор так и не попадут.

Лексика статусов и меток

Метки трекера — словарь на дюжину слов, зато обязательный. Их ставят в поля и пишут в комментариях, и от того, какая метка прилипла к твоей заявке, зависит вся её дальнейшая судьба.

МеткаЧто означает
triage / needs triageЗаявка ещё не разобрана, приоритет не назначен
confirmed / reproducedБаг воспроизвели, он настоящий
cannot reproduceПо описанию воспроизвести не удалось
needs more infoНе хватает данных; без ответа автора задачу закроют
duplicateУже есть такая заявка, обсуждение переезжает туда
regressionРаньше работало, сломалось в конкретной версии
flakyВоспроизводится через раз (чаще всего про тесты)
works as intended / by designПоведение задумано таким, это не баг
wontfixПроблема признана, но чинить не будут
good first issueПростая задача для новичка в проекте
help wantedМейнтейнеры ждут PR от сообщества
staleДавно нет активности, скоро закроется автоматически
Thanks for the report! I can confirm this on 4.7.2 — labelling it as a
regression, it worked in 4.6.x. Looks like it came in with #689.

I'm afraid I cannot reproduce this on a clean install. Could you share
the output of `app doctor` and the exact steps after login?

Closing as a duplicate of #431 — let's keep the discussion there so it
stays in one place. Thanks for taking the time to write this up.

Marking as `works as intended`: the API returns 404 for soft-deleted
records on purpose, so that deletion isn't observable. Happy to discuss
if that breaks a real use case for you.

Даже отказ здесь начинается с благодарности. Это не ритуал: человек потратил вечер на описание бага, и thanks for taking the time стоит тебе ноль усилий, а решает, придёт ли он со следующей находкой или молча уйдёт на другую библиотеку.

Как писать в чужой open-source-репозиторий

Мейнтейнер обычно ведёт проект вечерами после работы и разгребает десятки заявок в неделю. Всё, что ты можешь сделать за него, сделай за него. Чек-лист:

  1. Поищи существующую issue, включая закрытые. Дубликат раздражает больше всего.
  2. Прочитай CONTRIBUTING.md и заполни шаблон issue, если он есть.
  3. Укажи версию библиотеки, версию рантайма и ОС. Всегда.
  4. Дай минимальный воспроизводимый пример, а не ссылку на свой приватный проект.
  5. Не требуй сроков и не пингуй мейнтейнера через день.
  6. Предложи помощь: I'm happy to open a PR if you point me in the right direction.
### Environment
- library 2.4.1, Python 3.12.4, macOS 15.3 (arm64)

### What I expected
`serialize()` keeps the timezone of an aware datetime.

### What happens
The offset is dropped and the value is silently treated as UTC.

### Minimal example

```python
from tinyser import serialize
from datetime import datetime, timezone, timedelta

dt = datetime(2026, 7, 15, 10, tzinfo=timezone(timedelta(hours=2)))
print(serialize(dt))   # '2026-07-15T10:00:00Z'  ← expected +02:00
```

I looked at `encoders.py:88` and it seems `astimezone` is never called.
I'm happy to open a PR if that's the right place to fix it — just let me
know whether you'd prefer converting to UTC or preserving the offset.

Последний абзац делает две вещи разом. Показывает, что автор залез в исходники и назвал строку, а не просто пожаловался. И спрашивает, какое поведение считать правильным, вместо того чтобы объявить своё единственно верным. Такие заявки закрываются быстрее всех — иногда прямо PR-ом от того, кто их завёл.

Четыре жанра документации: Diátaxis

Diátaxis делит документацию на четыре жанра, которые нельзя смешивать. В основе — два вопроса о читателе: он сейчас учится или работает, ему нужна практика или теория. На пересечении — четыре разных текста:

ЖанрДля когоОтвечает на вопросКлючевой признак
TutorialНовичок, который учится«Проведи меня за руку»Гарантированный успех по шагам, без выбора
How-to guideПрактик с конкретной задачей«Как сделать X?»Рецепт для одной цели, предполагает базу
ReferenceТот, кому нужен факт«Какие параметры у функции?»Полнота и сухость, никакого обучения
ExplanationТот, кто хочет понять«Почему так устроено?»Контекст, альтернативы, история решений

Вся польза — в правиле «один текст = один жанр». Классическая беда: туториал, где на середине автора уносит объяснять архитектуру, а следом он вставляет таблицу всех параметров конфига. Новичок теряет нить. Практик не находит рецепт. Теоретик не дочитывает до объяснений. Текст один, недовольны трое.

Tutorial:    "Build your first bot in 10 minutes"
             — one path, fixed values, works end to end.

How-to:      "How to run the bot behind a reverse proxy"
             — assumes you already have a bot.

Reference:   "Configuration options"
             — an alphabetical table: name, type, default, description.

Explanation: "Why the bot uses long polling by default"
             — trade-offs, alternatives, when to switch to webhooks.

Структура README

README — витрина. У читателя секунд тридцать на решение, подходит ему проект или он идёт смотреть следующий в выдаче. Порядок разделов проверен временем:

  1. What it does — одно-два предложения, без маркетинга. Что это и какую задачу решает.
  2. Requirements — версии рантайма, база, внешние сервисы.
  3. Install — команды установки.
  4. Quickstart — минимальный работающий пример, который можно скопировать.
  5. Configuration — переменные окружения и настройки таблицей.
  6. Development / Contributing — как запустить тесты и прислать PR.
  7. License — одна строка.
# tinyser

Serialize Python objects to JSON without losing timezone information.

## Requirements

- Python 3.11+
- No runtime dependencies

## Install

```bash
pip install tinyser
```

## Quickstart

```python
from tinyser import serialize

serialize({"created_at": datetime.now(timezone.utc)})
# '{"created_at": "2026-07-15T10:00:00+00:00"}'
```

## Configuration

| Variable            | Type | Default | Description                     |
|---------------------|------|---------|---------------------------------|
| `TINYSER_INDENT`    | int  | `0`     | Indentation for pretty output   |
| `TINYSER_TZ_POLICY` | str  | `keep`  | `keep` or `utc`                 |

## Contributing

Run `make test` before opening a PR. See CONTRIBUTING.md.

## License

MIT

Главное здесь — порядок: quickstart выше конфигурации. Сначала работающие пять строк, потом тридцать настроек. Если код убедил, до таблицы читатель доберётся сам; если сначала таблица — не доберётся никуда.

Стиль технического письма

Google developer documentation style guide, Microsoft, Red Hat — писались независимо, а сошлись примерно на одном. Пять правил, которые дают девяносто процентов эффекта:

  1. Второе лицо. You configure the client, а не the user configures и не we configure. Документация обращается к читателю.
  2. Активный залог. The server rejects the request вместо the request is rejected by the server. Активный залог называет действующее лицо и короче.
  3. Настоящее время. The command prints the version, а не will print. Документация описывает, как система работает всегда, а не что случится потом.
  4. Короткие предложения. Одна мысль — одно предложение. Читателю с неродным английским это помогает даже сильнее, чем носителю.
  5. Явный субъект в инструкции. В шаге пиши, что именно нажать и где.
Before: It is recommended that the configuration file should be placed
        in the directory where it will be discovered automatically by
        the application at startup time.
After:  Put `config.yaml` in the project root. The app loads it at startup.

Before: The token will be validated by the middleware and if it is
        invalid an error is going to be returned.
After:  The middleware validates the token. If the token is invalid,
        the middleware returns 401.

Before: In order to be able to start the server, it is necessary for the
        user to have created a database beforehand.
After:  Create the database before you start the server.

Отдельная история — слова, которых стайлгайды прямо просят избегать: simply, just, obviously, easily, of course, trivial. Дело не в стилистике, а в том, что происходит с читателем, у которого «simply run the command» не сработало. Он сидит перед ошибкой, а текст сообщает ему, что задача-то была простая. Ни одного бита эти слова не несут: удали — предложение станет только лучше.

Не надоНадо
Simply run the migration.Run the migration.
This is obviously a bad idea.This causes a full table scan on every request.
Just add the header.Add the `X-Api-Key` header.
It's easy to configure TLS.To configure TLS, set `TLS_CERT` and `TLS_KEY`.
Please enter your password.Enter your password.

Последняя строка русскоязычных авторов обычно возмущает: как это — нельзя please? Нас же учили, что без него невежливо. Но инструкция не просьба: документация не уговаривает читателя ввести пароль, она описывает шаг. В письме коллеге please на месте, в шаге туториала оно только удлиняет строку. Правило касается инструкций, не переписки — в главе 9 please ещё пригодится.

Кейс из реального проекта: превращаем плохой баг-репорт в хороший

Пятница, вечер. QA-инженер заводит заявку. Вот она, дословно.

Title: Export broken!!!

Guys, export is completely broken again, same as last time. Nothing
works, users are complaining, this is critical. Please fix ASAP, we
already lost a client because of this. How is this even possible after
two weeks of testing?

Разбираем. Заголовок не говорит ни какой экспорт, ни что с ним. Версии нет, окружения нет, шагов нет, ожидаемого и фактического поведения нет, логов нет. «Same as last time» отсылает к контексту, которого у читателя в голове не осталось — прошло два месяца и сорок задач. Концовка про «how is this even possible» переводит разговор из технического в эмоциональный, и первое, что захочется сделать разработчику, — защищаться, а не чинить. «Critical» ничем не подкреплено. «Lost a client» невозможно ни проверить, ни использовать.

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

Title: PDF export times out for reports with more than ~5k rows (4.7.2)

## Summary
PDF export of the Orders report fails with a gateway timeout when the
report has roughly 5,000 rows or more. CSV export of the same report
works. This worked in 4.6.9.

## Environment
- App version: 4.7.2 (build 1183), production
- Browser: Chrome 139, macOS 15.3
- Workspace: `acme-eu`, admin role

## Steps to reproduce
1. Open **Reports → Orders**.
2. Set the date range to `2026-06-01 – 2026-07-31` (5,412 rows).
3. Click **Export → PDF**.
4. Wait.

## Expected behavior
A PDF file downloads, as it does for smaller ranges.

## Actual behavior
After 60 seconds the browser shows a 504 page. No file is downloaded.
With a range under ~4,000 rows the export completes in about 20 seconds.

## Logs
Request ID `a13f-77c2-0e91`:

    ERROR pdf-worker: context deadline exceeded after 60s
    INFO  pdf-worker: rendered 3812 of 5412 rows

## Impact
Three customers reported it this week. Two of them run monthly reports
above the threshold, so this blocks their month-end process.

## Workaround
Splitting the range into two exports works. It takes ~4 minutes per
report and has to be done by hand.

Мерять эти две версии удобно одним числом: сколько вопросов нужно задать, прежде чем начать работу. К первой — шесть. Ко второй — ни одного. В ней есть порог (около 5000 строк), версия, где всё работало, — а значит, это регрессия, и можно смотреть диф между релизами, — логи с request ID и понятный масштаб.

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

Типичные ошибки

Нет версии и окружения

Самая частая причина, по которой заявка висит неделями. Без версии не понять, не починили ли это ещё в прошлом релизе; без окружения — не воспроизвести. «Latest version» не спасает: у тебя «последняя» — та, что встала при апрельском обновлении, а у мейнтейнера — вчерашний мастер.

Как НЕ надо:
I'm on the latest version and it fails.

Как надо:
Version 4.7.2 (build 1183), Python 3.12.4, Ubuntu 24.04, running in Docker
(image `app:4.7.2-slim`). Also reproduced on 4.7.1.

Эмоции и обвинения вместо фактов

«This is unacceptable», «how did this pass review», «URGENT!!!» — ни одна из этих фраз не ускорила ещё ни одной починки, зато каждая надёжно включает у читателя защиту. Срочность передают фактами: сколько пользователей задето, что именно встало, есть ли обходной путь.

Как НЕ надо:
This is a disaster, how did nobody notice this?! Fix it now.

Как надо:
This blocks checkout for all EU customers since 09:40 UTC. No workaround
found so far. Request ID for a failing attempt: `a13f-77c2-0e91`.

«Simply» и «obviously» в документации

Тест простой: вычеркни simply, just, obviously и перечитай. Смысл не изменился — а он не изменится, — значит, слова были лишними с самого начала. То же самое проделай с very, really и basically: текст похудеет и перестанет звучать снисходительно.

Смешение жанров документации

Туториал, где на третьем шаге начинается разбор архитектуры, а на пятом — полный список параметров конфига. Новичок не дочитает, практик не найдёт рецепт. Один текст — один жанр, остальное уезжает ссылкой: For the full list of options, see Configuration reference.

Стена текста без структуры

Пятнадцать строк сплошным абзацем никто не читает — их сканируют глазами и ничего не находят. Заголовки, списки, нумерованные шаги, блоки кода: всё это опоры, за которые цепляется взгляд. Правило-минимум: абзац длиннее пяти строк почти всегда прячет в себе список.

Итог

Issue и документация пишутся для человека без твоего контекста, и вся работа — уложить этот контекст в текст. Заголовок заявки собирается по формуле «что не так + где + при каких условиях». Тело баг-репорта — Summary, Environment, Steps to reproduce, Expected, Actual, Logs, Workaround. А doesn't work меняется на глагол симптома: returns 500, hangs, times out, silently fails.

Feature request начинается с проблемы, а не с решения, иначе обсуждать будут кнопку. Метки (confirmed, cannot reproduce, regression, duplicate, works as intended, needs more info) — обязательный словарь: по ним видно, что с твоей заявкой происходит. В документации держи жанры Diátaxis раздельно — tutorial, how-to, reference, explanation — и общий стиль: второе лицо, активный залог, настоящее время, короткие предложения. И ни одного simply, just, obviously.

Дальше — переписка: Slack, письма коллегам и вендорам, статусы, дедлайны и часовые пояса, в которых «by Friday» у каждого своя.

Комментарии 0

Для добавления комментариев необходимо войти или зарегистрироваться.

Пока нет комментариев. Станьте первым!