Files
progcode/editorial/agent-rewrites/091.json
T

8 lines
28 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 91,
"slug": "editorial-2025-06-field-developer-experience",
"title": "Когда внутренний инструмент заставляет разработчика ждать",
"excerpt": "Разбираем учебный кейс долгого запуска проекта: как отделить очередь, неясный маршрут и реальную ошибку, измерить путь до первого успешного изменения и выбрать проверяемое улучшение.",
"contentHtml": "<p>Разработчик клонирует репозиторий, запускает команду и получает сообщение «готово». Через час он всё ещё не сделал первое изменение: неясно, где взять тестовые данные, кто выдаёт доступ и какой результат считать успешным. Такой путь часто называют медленным, хотя в нём смешаны ожидание внешнего решения, ручные действия и отсутствие обратной связи.</p>\n<p>Цена ошибки — не только потерянные минуты. Человек повторяет команды, пишет в поддержку и создаёт обходной скрипт. Владелец инструмента видит среднее время выполнения, но не знает, на каком шаге пользователь остановился. Если в ответ добавить ещё одну кнопку или увеличить таймаут, можно ускорить уже быстрый участок и оставить настоящий блокер.</p>\n<p><strong>Рабочий тезис.</strong> Developer experience (DX, опыт разработчика) нужно проверять как путь конкретной задачи до наблюдаемого результата. Время — один сигнал. К нему нужны упорядоченные события, наблюдение самого разработчика и причина обращения в поддержку. Ниже — учебная модель, которую можно воспроизвести локально. Она не описывает реальный сервис и не выдаёт данные за production-измерение.</p>\n<h2>Сначала определите результат задачи</h2>\n<p>Начните не с вопроса «удобен ли инструмент», а с результата, который можно увидеть. Для локального запуска это может быть зелёная проверка и первое принятое изменение в тестовой ветке. Для внутреннего API — успешный запрос с ожидаемым ответом. Для шаблона проекта — старт приложения и прохождение smoke-теста.</p>\n<p>Граница должна включать одного пользователя, одну роль и одну задачу. «Запустить новый сервис» слишком широко: в него попадут доступ к репозиторию, секреты, база данных и CI. Возьмите меньший путь: «получить sandbox-доступ, изменить текст на странице, выполнить проверку». Тогда можно назвать начало, конец и условия остановки.</p>\n<table><caption>Минимальный контракт измерения для одной задачи</caption><thead><tr><th scope=\"col\">Поле</th><th scope=\"col\">Пример</th><th scope=\"col\">Зачем оно нужно</th></tr></thead><tbody><tr><td>Роль</td><td><code>new-contributor</code></td><td>Не смешивать новичка и владельца сервиса</td></tr><tr><td>Начало</td><td><code>task.started</code></td><td>Зафиксировать, когда человек действительно начал путь</td></tr><tr><td>Конец</td><td><code>check.passed</code></td><td>Отделить полезный результат от запуска команды</td></tr><tr><td>Ожидание</td><td><code>env.ready → change.applied</code></td><td>Проверить внешний или ручной блокер</td></tr><tr><td>Отрицательный исход</td><td><code>blocked: owner-unknown</code></td><td>Не считать незавершённую задачу быстрым обходом</td></tr></tbody></table>\n<p>Эти имена — проектное соглашение статьи, а не обязательный стандарт. В вашем проекте они могут быть другими. Важно, чтобы событие имело источник, время и понятного владельца; иначе одинаковое слово будет означать разные этапы в разных командах.</p>\n<figure><img src=\"/assets/editorial/2025/developer-experience-2025-task-journey.svg\" alt=\"Схема учебной задачи: старт, готовность окружения, применение изменения, проверка и отдельные источники сигнала\" loading=\"lazy\"><figcaption>Один путь разделён на события, человеческое наблюдение, сигнал поддержки и неизвестную причину. Эти слои нельзя заменять одним числом.</figcaption></figure>\n<h2>Разделите время на участки</h2>\n<p>Полное время до результата удобно представить как сумму участков: <code>TTFG = setup + waiting + work + verification</code>, где TTFG — время до первого успешного изменения. Формула помогает выбрать следующий вопрос, но не объясняет причину автоматически.</p>\n<p><code>setup</code> — действия до готового окружения: установка зависимостей, получение доступа, загрузка фикстур. <code>waiting</code> — время, когда следующий шаг зависит от владельца, очереди или внешней системы. <code>work</code> — действия разработчика после готовности среды. <code>verification</code> — проверка результата. Если записать только начало и конец, все четыре участка сольются в одну «медленную» операцию.</p>\n<p>Для инструментированной части полезна трассировка. В OpenTelemetry span представляет операцию с началом, концом и атрибутами, а span event — значимую точку времени внутри операции. Это позволяет связать серверное ожидание с одним путём, но не позволяет узнать, что человек делал в терминале до первого запроса. Человеческое наблюдение и системный span дополняют друг друга, а не подменяют.</p>\n<h2>Учебный кейс: доступ к sandbox</h2>\n<p>Представим фиксированную задачу для роли <code>new-contributor</code>: открыть проект, получить sandbox-доступ, изменить заголовок и пройти проверку. В 09:00 задача начата. В 09:06 окружение готово. В 09:24 изменение применено. В 09:29 проверка прошла. Между готовностью и изменением — 18 минут, но из одних временных меток нельзя узнать, были ли это ожидание доступа, чтение инструкции или исправление ошибки.</p>\n<p>К задаче добавлены два независимых сигнала: наблюдение «непонятно, кто выдаёт доступ» и категория поддержки <code>owner-unknown</code>. Они формируют гипотезу, а не доказывают её: возможно, владелец не указан в интерфейсе; возможно, доступ уже выдан, но команда запускается с неверным профилем. Следующая проверка должна различить эти объяснения.</p>\n<pre><code>const journey = {\n role: 'new-contributor',\n task: 'sandbox-first-change',\n events: [\n ['task.started', '09:00'],\n ['env.ready', '09:06'],\n ['change.applied', '09:24'],\n ['check.passed', '09:29']\n ],\n observation: 'access-owner-unclear',\n supportReason: 'owner-unknown',\n claim: 'hypothesis-only'\n};\n\nconsole.table(journey.events);</code></pre>\n<p>Поле <code>claim</code> намеренно ограничивает вывод. Одна учебная запись не показывает частоту, медиану, хвост распределения и причинность. Она нужна, чтобы проверить схему данных и не объявить случай улучшением. Если в реальном сервисе нельзя безопасно связать события с одной задачей, сначала решите проблему корреляции, а не стройте дашборд из несвязанных чисел.</p>\n<figure><img src=\"/assets/editorial/2025/developer-experience-2025-wait-time-histogram.svg\" alt=\"Учебные корзины времени ожидания: до пяти, от пяти до пятнадцати и больше пятнадцати минут с предупреждением о синтетических данных\" loading=\"lazy\"><figcaption>Корзины времени помогают найти участок для проверки. Они не измеряют удобство и не устанавливают причину задержки.</figcaption></figure>\n<h2>Что каждый сигнал может доказать</h2>\n<p>У сигнала должна быть узкая область применимости. Событие отвечает на вопрос «что произошло и когда». Наблюдение отвечает на вопрос «что понял или не понял человек». Обращение в поддержку показывает тему, которую пользователь посчитал препятствием. Решение владельца фиксирует действие. Ни один из них сам по себе не доказывает улучшение DX.</p>\n<table><caption>Сигнал, вывод и запрещённое обобщение</caption><thead><tr><th scope=\"col\">Сигнал</th><th scope=\"col\">Что он показывает</th><th scope=\"col\">Чего он не показывает</th><th scope=\"col\">Следующая проверка</th></tr></thead><tbody><tr><td>События <code>task.started</code> и <code>check.passed</code></td><td>Длительность пути для связанной записи</td><td>Почему человек ждал</td><td>Разложить путь на интервалы и источники</td></tr><tr><td>Span или span event</td><td>Время операции и её контекст в системе</td><td>Действия вне инструментированной системы</td><td>Сопоставить trace с задачей без лишних персональных данных</td></tr><tr><td>Наблюдение разработчика</td><td>Непонятный термин, шаг или владелец</td><td>Масштаб проблемы и причинность</td><td>Повторить сценарий с несколькими участниками</td></tr><tr><td>Категория поддержки</td><td>Повторяющийся тип обращения</td><td>Все случаи, включая молчаливый отказ</td><td>Считать обращения вместе с завершением задачи</td></tr><tr><td>Среднее время до результата</td><td>Агрегированное значение выбранной группы</td><td>Длинный хвост и смену состава группы</td><td>Сравнить медиану, p90 и долю завершивших</td></tr></tbody></table>\n<p>Это особенно важно для среднего. Один случай с ожиданием в два дня может исчезнуть в среднем значении, если девять задач завершились за минуту. Медиана показывает типичный путь, p90 — верхний хвост, а доля завершивших не даёт принять незавершённую задачу за быструю. Выбирайте показатель по вопросу, который задаёте, а не по тому, который уже есть в панели.</p>\n<h2>Воспроизводимая проверка на Node.js</h2>\n<p>Ниже — локальный расчёт без пакетов, сети и production-доступа. Сохраните JavaScript в файл <code>dx-check.mjs</code>, проверьте синтаксис командой <code>node --check dx-check.mjs</code>, затем запустите <code>node dx-check.mjs</code>. Нужна версия Node.js, поддерживающая ECMAScript modules; числа в примере искусственные.</p>\n<pre><code>const events = [\n ['task.started', '09:00'],\n ['env.ready', '09:06'],\n ['change.applied', '09:24'],\n ['check.passed', '09:29']\n];\n\nconst toMinutes = (clock) =&gt; {\n const [hours, minutes] = clock.split(':').map(Number);\n return hours * 60 + minutes;\n};\n\nconst duration = (from, to) =&gt;\n toMinutes(events.find(([name]) =&gt; name === to)[1]) -\n toMinutes(events.find(([name]) =&gt; name === from)[1]);\n\nconst result = {\n timeToFirstGreen: duration('task.started', 'check.passed'),\n setup: duration('task.started', 'env.ready'),\n waitingHypothesis: duration('env.ready', 'change.applied'),\n verification: duration('change.applied', 'check.passed')\n};\n\nconsole.log(result);\n// { timeToFirstGreen: 29, setup: 6, waitingHypothesis: 18, verification: 5 }</code></pre>\n<p>Результат означает только длительности учебной последовательности. Название <code>waitingHypothesis</code> напоминает, что 18 минут ещё нужно объяснить. Чтобы проверить гипотезу, добавьте источник ожидания: например, событие <code>access.requested</code> от сервиса доступа и отметку выдачи. Не добавляйте в telemetry содержимое секретов, токены, персональные данные или полный текст команд. Для идентификатора достаточно минимального технического ключа с понятным сроком хранения и контролем доступа.</p>\n<h2>Выберите вмешательство по причине</h2>\n<p>Одно и то же наблюдение может вести к разным решениям. Если владелец этапа неизвестен, исправьте маршрут и текст статуса. Если доступ выдан, но CLI читает не тот профиль, исправьте диагностику и сообщение об ошибке. Если запрос стоит в очереди, меняйте очередь или показывайте честное состояние ожидания. Если окружение ломается из-за отсутствующей зависимости, добавьте проверку prerequisites, а не инструкцию «попробуйте ещё раз».</p>\n<table><caption>Варианты исправления и их цена</caption><thead><tr><th scope=\"col\">Причина</th><th scope=\"col\">Малое изменение</th><th scope=\"col\">Цена и риск</th><th scope=\"col\">Критерий успеха</th></tr></thead><tbody><tr><td>Не виден владелец</td><td>Показать owner и следующий шаг до отправки</td><td>Нужно поддерживать актуальность маршрута</td><td>Меньше обращений <code>owner-unknown</code> при той же доле завершения</td></tr><tr><td>Нет prerequisites</td><td>Команда предварительной проверки с конкретным исправлением</td><td>Проверка может замедлить быстрый путь</td><td>Ошибка выявляется до длинного запуска</td></tr><tr><td>Очередь доступа</td><td>Статус, время обновления и ссылка на владельца очереди</td><td>Нельзя обещать срок, которым управляет другая команда</td><td>Ожидание видно, а повторные заявки не растут</td></tr><tr><td>Нет подтверждения результата</td><td>Явная проверка и ссылка на лог</td><td>Нужно выбрать стабильный smoke-тест</td><td>Разработчик сам отличает успех от частичного запуска</td></tr></tbody></table>\n<p>Не делайте все изменения сразу. Маленькая партия сохраняет причинную связь между изменением и наблюдением. Документация Google Cloud, описывая DevOps-возможности, отдельно связывает поддерживаемость кода, обратную связь, наблюдаемость и работу малыми партиями с улучшением доставки. Это ориентир для выбора практики, но не доказательство эффекта именно в вашей команде.</p>\n<figure><img src=\"/assets/editorial/2025/developer-experience-2025-feedback-loop.svg\" alt=\"Цикл DX: наблюдение, системное событие и сигнал поддержки ведут к узкому изменению, повторной проверке и безопасной остановке вывода\" loading=\"lazy\"><figcaption>Проверяемый цикл заканчивается повторным измерением или остановкой вывода, если свидетельства не сопоставимы.</figcaption></figure>\n<h2>Как сравнить результат до и после</h2>\n<p>Сравнивайте одну и ту же задачу, роль, ветку процесса и определение конца. Зафиксируйте период и версию инструмента. Считайте отдельно завершённые и незавершённые пути. Минимальный набор для учебного эксперимента: количество стартов, доля <code>check.passed</code>, медиана TTFG, p90 TTFG, медиана ожидания и частота причин поддержки.</p>\n<p>После добавления подсказки «владелец доступа» среднее время может уменьшиться случайно: в новую выборку попали опытные разработчики, очередь была короче или часть людей перестала создавать заявки. Поэтому корректная формулировка звучит так: «в этой выборке при этих границах показатель изменился». Утверждение «подсказка сократила время» требует более сильного дизайна сравнения — например, стабильных когорт или контролируемого эксперимента.</p>\n<p>Рекомендация GOV.UK применима здесь как методическая граница: performance metrics полезно сочетать с исследованием пользователей, а для целого пути смотреть на завершение задачи и время её выполнения. Она не задаёт универсальный KPI для внутренних инструментов. Ваши показатели должны следовать задаче и цене ошибки: иногда важнее доля успешного запуска, иногда — отсутствие ручного доступа к секретам.</p>\n<h2>Ограничения применимости</h2>\n<p>Учебные времена 09:00–09:29, роль, события, категории поддержки и ожидаемый вывод выдуманы. Их нельзя использовать как бенчмарк, KPI, прогноз или свидетельство работы конкретного продукта. Локальный скрипт проверяет арифметику четырёх событий, но не проверяет права, сеть, корректность telemetry, работу очереди и поведение реального клиента.</p>\n<p>Трассировка не видит молчаливый отказ: человек мог бросить задачу до первого запроса. Обращения в поддержку отражают только тех, кто написал. События могут потерять контекст при ретрае или повторном запуске. Агрегаты могут скрыть различия между ролями, операционными системами и уровнями доступа. Поэтому любые сравнения делайте с явной схемой семплирования, сроком хранения и правилами приватности.</p>\n<p>Если нельзя связать начало и конец одной задачи или неясно, кто владеет этапом, честный результат — «данных недостаточно для вывода». Можно исправить очевидную ошибку инструкции, но не приписывать ей измеренный эффект. Для публичного или критичного сервиса дополнительно нужны security review, нагрузочная проверка, план отката и согласование с владельцами данных.</p>\n<h2>Порядок действий</h2>\n<ol><li>Назовите одну роль, одну задачу и наблюдаемый результат.</li><li>Зафиксируйте события начала, конца и ключевых переходов; для каждого укажите источник и владельца.</li><li>Разделите setup, waiting, work и verification, не называя весь интервал «медленным инструментом».</li><li>Соберите отдельно системные события, наблюдения разработчиков и причины обращений.</li><li>Проверьте гипотезу маленьким экспериментом: изменить один маршрут, подсказку или диагностическую проверку.</li><li>Заранее определите метрики и границы сравнения: completion rate, медиана, p90 и выбранный участок ожидания.</li><li>Повторите тот же сценарий на сопоставимой группе и запишите отрицательный результат, если критерий не выполнен.</li><li>Передайте владельцу не общий score, а причину, изменение, свидетельство и следующий шаг.</li></ol>\n<h2>Критерий готовности</h2>\n<p>Разбор готов, когда для одной задачи можно восстановить путь от старта до результата, отличить системное ожидание от человеческой неопределённости, назвать владельца каждого перехода и показать повторную проверку. Если есть только красивый график времени, это ещё не доказательство улучшения DX.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://opentelemetry.io/docs/concepts/signals/traces/\" target=\"_blank\" rel=\"noopener noreferrer\">OpenTelemetry: Traces</a> — официальное описание trace, span, временных меток, атрибутов и span events. Источник подтверждает техническую модель наблюдения, но не измеряет человеческий опыт.</li><li><a href=\"https://www.gov.uk/service-manual/measuring-success/measuring-the-success-of-your-service\" target=\"_blank\" rel=\"noopener noreferrer\">GOV.UK Service Manual: Measuring the success of your service</a> — рекомендует сочетать performance metrics с user research и для сквозного пути измерять завершение задачи и время. Это методическая рекомендация, а не готовый KPI для каждой команды.</li><li><a href=\"https://docs.cloud.google.com/architecture/devops\" target=\"_blank\" rel=\"noopener noreferrer\">Google Cloud: DevOps capabilities</a> — официальная карта практик DORA, включая поддерживаемость кода, обратную связь, наблюдаемость и малые партии. Она помогает выбрать направление улучшения, но не доказывает причинность локального эксперимента.</li><li><a href=\"https://nodejs.org/api/cli.html#-c-check\" target=\"_blank\" rel=\"noopener noreferrer\">Node.js CLI: --check</a> — документация команды проверки синтаксиса JavaScript; используется в воспроизводимом локальном примере.</li></ul>"
}