Введение
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-репозиторий
Мейнтейнер обычно ведёт проект вечерами после работы и разгребает десятки заявок в неделю. Всё, что ты можешь сделать за него, сделай за него. Чек-лист:
- Поищи существующую issue, включая закрытые. Дубликат раздражает больше всего.
- Прочитай
CONTRIBUTING.mdи заполни шаблон issue, если он есть. - Укажи версию библиотеки, версию рантайма и ОС. Всегда.
- Дай минимальный воспроизводимый пример, а не ссылку на свой приватный проект.
- Не требуй сроков и не пингуй мейнтейнера через день.
- Предложи помощь: 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 — витрина. У читателя секунд тридцать на решение, подходит ему проект или он идёт смотреть следующий в выдаче. Порядок разделов проверен временем:
- What it does — одно-два предложения, без маркетинга. Что это и какую задачу решает.
- Requirements — версии рантайма, база, внешние сервисы.
- Install — команды установки.
- Quickstart — минимальный работающий пример, который можно скопировать.
- Configuration — переменные окружения и настройки таблицей.
- Development / Contributing — как запустить тесты и прислать PR.
- 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 — писались независимо, а сошлись примерно на одном. Пять правил, которые дают девяносто процентов эффекта:
- Второе лицо. You configure the client, а не the user configures и не we configure. Документация обращается к читателю.
- Активный залог. The server rejects the request вместо the request is rejected by the server. Активный залог называет действующее лицо и короче.
- Настоящее время. The command prints the version, а не will print. Документация описывает, как система работает всегда, а не что случится потом.
- Короткие предложения. Одна мысль — одно предложение. Читателю с неродным английским это помогает даже сильнее, чем носителю.
- Явный субъект в инструкции. В шаге пиши, что именно нажать и где.
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
Пока нет комментариев. Станьте первым!