UX, CJM, требования и user stories
Между «мы поняли проблему пользователя» и «команда выкатила фичу, которая эту проблему решает» лежит участок, где теряется больше всего ценности. Discovery дал знание. Приоритизация выбрала, что делать. Дальше это знание нужно передать команде без потерь и воплотить в интерфейс, который человек поймёт с первого раза. Оба перехода — узкие места, и оба продакт обязан держать.
Эта статья — про инженерию этих двух переходов. Не «как рисовать красивые макеты» (это работа дизайнера) и не «как правильно оформить тикет» (это бюрократия), а про то, какая именно информация должна дойти до команды, в какой форме она не искажается, и почему интерфейс, собранный без модели пользователя, ломается предсказуемым образом.
1. Задача с первых принципов
1.1. Передача намерения по шумному каналу
Формально ситуация выглядит так. В голове у продакта (а точнее — распределённо, в интервью, данных и разговорах) есть намерение: какое изменение в жизни пользователя мы хотим произвести. Команде нужно получить достаточно информации, чтобы построить систему, производящую это изменение. Между ними — канал: документы, макеты, встречи, тикеты.
У канала есть три свойства, которые определяют всё остальное:
- Он лоссовый. Естественный язык неоднозначен. «Пользователь должен видеть актуальные данные» — это раз в секунду или раз в час? «Быстро» — это 200 мс или 2 с?
- Он дорогой. Каждая страница спеки — время на написание, чтение, согласование и поддержание в актуальном состоянии. Спека на 60 страниц устаревает быстрее, чем её дочитают.
- Он двусторонний, но об этом забывают. Разработчик, увидевший требование, знает про систему то, чего не знает продакт: что дёшево, что дорого, где уже есть похожий механизм. Односторонняя передача («вот спека, реализуй») выбрасывает эту информацию.
Отсюда — центральный принцип: не пытайтесь описать решение полностью, опишите намерение точно и создайте дешёвый способ уточнять. Именно это, а не «формат тикета», объясняет, почему user stories устроены так, как устроены.
1.2. Цена поздней ошибки в требованиях
Второй фундамент — экономический. Ошибка в требованиях, найденная на этапе формулировки, стоит одного разговора. Та же ошибка, найденная после релиза, стоит переделки кода, данных, документации, обучения поддержки и репутации. Классические оценки Боэма («Software Engineering Economics», 1981) давали разброс на два порядка; современные данные мягче, но порядок сохраняется — исправление после релиза заметно дороже, чем на этапе анализа (Boehm, «Software Defect Reduction Top 10 List»).
Практический вывод не «пишите больше документации», а гораздо более полезный: инвестируйте в раннее обнаружение непонимания. Прототип, который дизайнер показал пяти пользователям за два дня, ловит те же ошибки, что и трёхнедельная разработка, но в 10 раз дешевле.
1.3. Что такое UX и чем он не является
UX — не «интерфейс» и не «красиво». UX — это весь опыт человека при достижении своей цели с помощью продукта, включая то, что происходит до входа в продукт и после выхода из него. Ожидание ответа поддержки, письмо о списании, статья в базе знаний — часть UX. Кнопка — маленькая часть последнего слоя.
Полезная декомпозиция — пять уровней Джесси Джеймса Гарретта («The Elements of User Experience»), от абстрактного к конкретному:
цели продукта, потребности пользователя"] C["Область охвата
функциональные и контентные требования"] ST["Структура
информационная архитектура, взаимодействие"] SK["Каркас
раскладка, навигация, интерфейсные элементы"] SF["Поверхность
визуальный дизайн, типографика, цвет"] S --> C --> ST --> SK --> SF S -.->|"ошибка здесь
обесценивает всё ниже"| SF classDef top fill:#4c8dd8,stroke:#4c8dd8,color:#fff classDef bot fill:#3fa66a,stroke:#3fa66a,color:#fff class S,C top class SK,SF bot
Ключевое свойство модели: решения нижних уровней обусловлены верхними, а не наоборот. Красивая поверхность поверх неверной структуры — это дорогое украшение неработающего продукта. Продакт отвечает в первую очередь за стратегию и охват, участвует в структуре и почти не лезет в поверхность. Классическая дисфункция — продакт спорит про цвет кнопки и не может ответить, какую задачу пользователь решает на этом экране.
2. Модель пользователя: почему интерфейсы непонятны
2.1. Три модели и два разрыва
Дон Норман в «The Design of Everyday Things» описывает ситуацию через три модели одной системы:
- Модель реализации — как система устроена на самом деле (таблицы, очереди, состояния).
- Модель дизайнера — как, по замыслу, пользователь должен её себе представлять.
- Ментальная модель пользователя — как он представляет её фактически, исходя из того, что видит на экране, и из прошлого опыта с другими продуктами.
Пользователь общается не с системой, а со своей моделью системы. Между его намерением и системой — два разрыва:
- Разрыв исполнения (gulf of execution): «я знаю, чего хочу, но не понимаю, что нажать».
- Разрыв оценки (gulf of evaluation): «я нажал, но не понимаю, что произошло и получилось ли».
Практическая ценность цикла в том, что любой баг юзабилити локализуется в конкретном шаге. «Пользователи не находят экспорт» — разрыв исполнения на шаге «План». «Пользователи жмут «Сохранить» дважды» — разрыв оценки: нет подтверждения. Это переводит расплывчатое «неудобно» в чинибельную формулировку.
2.2. Шесть принципов, закрывающих разрывы
| Принцип | Что делает | Провал в интерфейсе |
|---|---|---|
| Аффорданс | элемент подсказывает возможное действие | текст, который на самом деле кликабелен |
| Сигнификатор | явный признак этой возможности | иконка без подписи в незнакомой роли |
| Соответствие (mapping) | связь управления и результата естественна | ползунки в произвольном порядке |
| Обратная связь | система сообщает, что произошло | кнопка «Отправить» без реакции 3 секунды |
| Ограничения | невозможное запрещено физически | поле даты, куда можно ввести «вчера» |
| Концептуальная модель | целостная непротиворечивая картина | «проект», «пространство» и «папка» без границ |
Сильнейший из шести — ограничения. Валидировать ошибку после ввода дороже и хуже, чем сделать её невозможной: календарь вместо текстового поля, выпадающий список вместо строки. Это ровно тот же принцип, что «делай недопустимые состояния непредставимыми» в типизации — см. https://courses.digitable.life/post/typescript/00-overview/ и разбор доменных инвариантов в https://courses.digitable.life/post/ddd/00-overview/.
2.3. Эвристики Нильсена как чек-лист ревью
Десять эвристик Якоба Нильсена (NN/g, 1994, обновлены 2024) — не теория, а рабочий чек-лист. Продакту достаточно прогнать по ним макет за 15 минут:
- Видимость состояния системы — человек всегда знает, что происходит.
- Соответствие системы реальному миру — язык пользователя, а не язык базы данных.
- Свобода и контроль — отмена, выход, «назад» без потерь.
- Согласованность и стандарты — одно и то же называется одинаково везде.
- Предотвращение ошибок — см. ограничения выше.
- Узнавание вместо припоминания — не заставляйте держать данные в голове между экранами.
- Гибкость и эффективность — ускорители для опытных, не мешающие новичкам.
- Эстетичный минималистичный дизайн — каждый лишний элемент конкурирует с нужным.
- Помощь в распознавании и исправлении ошибок — что случилось, почему, как починить.
- Справка и документация — доступная в контексте, а не отдельным сайтом.
Типичная находка в B2B-продуктах — нарушения №2 и №9: «Ошибка валидации: constraint fk_account_owner violated» вместо «Нельзя удалить пользователя, пока на нём висят активные счета. Сначала передайте их другому».
3. Customer Journey Map
3.1. Зачем нужна карта, если есть воронка
Воронка (см. https://courses.digitable.life/post/product-management/02-metrics/) отвечает на вопрос «где падает конверсия». CJM отвечает на вопрос «почему» и, главное, показывает переходы между каналами, которые ни одна воронка внутри продукта не видит: человек написал в поддержку, три дня ждал, пошёл к конкуренту. В продуктовой аналитике этого нет вообще — он просто «перестал заходить».
CJM — это способ разложить опыт по этапам и связать в одной картине три обычно разъединённых источника: качественные данные (интервью), количественные (аналитика) и операционные (тикеты поддержки, звонки продаж).
3.2. Анатомия: строки, которые обязаны быть
- Персона и сценарий. Одна карта — один тип пользователя и одна цель. Карта «для всех пользователей» бесполезна: у бухгалтера и у аналитика разные пути через один продукт.
- Этапы — крупные фазы (осознание → выбор → онбординг → первая ценность → привычка). Их 4–7; больше — распадается на детали, меньше — теряет смысл.
- Действия — что человек делает, глаголами. Не «взаимодействует с системой», а «выгружает выписку из банк-клиента».
- Точки контакта — канал и конкретный экран/письмо/человек.
- Мысли — прямые цитаты из интервью. Кавычки обязательны: они не дают выдумывать.
- Эмоция — кривая. Нужна не для красоты: пики и провалы указывают, где вложение усилий даёт максимальный сдвиг.
- Боли — что именно ломается, с частотой и масштабом.
- Возможности — куда можно вмешаться. Это вход в приоритизацию.
- Метрика этапа — количественное подтверждение боли. Без этой строки карта остаётся красивым плакатом.
- Владелец этапа — кто в компании отвечает. Часто самое ценное открытие: у провала между маркетингом и онбордингом нет владельца.
Компактный вариант той же структуры в mermaid — удобно держать прямо в репозитории рядом с кодом:
Цифра 1–5 — уровень удовлетворённости, справа — участники шага. Провал на импорте виден мгновенно, и видно, что он вовлекает поддержку, то есть стоит компании денег.
3.3. Как строить CJM, чтобы она не была фантазией
Порядок работ, отличающий карту-инструмент от карты-плаката:
- Определить вопрос. «Почему 40% зарегистрировавшихся не доходят до первого отчёта?» Карта без вопроса не сходится.
- Собрать имеющиеся данные до всяких воркшопов: воронка по событиям, топ причин обращений в поддержку, записи сессий, результаты интервью (методики — в https://courses.digitable.life/post/product-management/01-discovery-and-research/).
- Провести воркшоп с командой — построить черновик «как мы думаем». Ценность — в обнаружении разногласий: маркетинг и разработка обычно рисуют разные карты.
- Проверить черновик на пользователях. 5–8 интервью, где человек рассказывает свой реальный последний путь, а не оценивает вашу схему.
- Разметить каждую ячейку источником. Правило: цветной маркер на ячейках без данных. Обычно после этой процедуры цветным оказывается больше половины карты — и это главный результат упражнения.
- Вывести возможности и передать в приоритизацию (https://courses.digitable.life/post/product-management/03-prioritization/).
3.4. Родственные артефакты: не путать
| Артефакт | Ось | Что показывает | Когда применять |
|---|---|---|---|
| User flow | шаги в UI | конкретные экраны и ветвления | проектирование одной фичи |
| CJM | этапы жизни клиента | опыт по всем каналам, эмоция | поиск системных провалов |
| Service blueprint | этапы + слои системы | что происходит «за кулисами» | опыт зависит от людей и процессов |
| Experience map | этапы поведения | путь вне привязки к вашему продукту | вход на новый рынок |
3.5. Service blueprint: то, что за кулисами
Как только опыт зависит не только от кода (доставка, модерация, поддержка, ручная проверка
документов), CJM недостаточно: провал случается в невидимой для пользователя части. Service
blueprint добавляет слои, разделённые «линиями видимости» и «внутреннего взаимодействия».
Последовательная природа этого артефакта отлично ложится на sequenceDiagram:
(экраны, письма) participant BS as Backstage
(операторы, модераторы) participant SYS as Системы
(биллинг, KYC, CRM) U->>FS: Загружает документы для верификации FS->>SYS: Создаёт заявку KYC Note over FS,SYS: линия видимости — ниже пользователь не видит ничего SYS->>BS: Ставит в очередь на ручную проверку BS-->>BS: Очередь 18 часов в пик Note over U,FS: Провал: пользователь не знает срок,
пишет в поддержку на 2-й час BS->>SYS: Решение «одобрено» SYS->>FS: Триггер письма FS->>U: «Аккаунт подтверждён» U->>FS: Возвращается через 3 дня, контекст потерян
Из такой схемы напрямую следуют требования, которые никогда бы не появились из макета: показывать ожидаемый срок и позицию в очереди, слать промежуточный статус, восстанавливать контекст при возврате (deep link на прерванный шаг), а также SLO на длину очереди модерации.
4. Требования: уровни и формы
4.1. Пять уровней, которые нельзя смешивать
Классификация из BABOK и книги Вигерса («Software Requirements»):
зачем компании этот продукт
снизить долю ручной сверки с 60% до 20%"] U["Пользовательские требования
что человек должен смочь сделать
бухгалтер импортирует выписку и видит расхождения"] F["Функциональные требования
что система делает
система сопоставляет платежи по сумме, дате и назначению"] N["Нефункциональные
с каким качеством
p95 импорта файла 10 МБ — не более 8 с"] C["Ограничения
что нельзя менять
данные не покидают ЕС; поддержка IE не требуется"] B --> U --> F U --> N C --> F C --> N classDef why fill:#8a7fbe,stroke:#8a7fbe,color:#fff classDef what fill:#4c8dd8,stroke:#4c8dd8,color:#fff classDef how fill:#3fa66a,stroke:#3fa66a,color:#fff class B why class U,F what class N,C how
Самая частая патология — функциональные требования без пользовательских: список того, что система делает, из которого невозможно понять, зачем. Такой список нельзя ни приоритизировать, ни сократить: непонятно, что можно выбросить, потому что неизвестно, какую задачу человека закрывает каждый пункт.
4.2. Нефункциональные требования: главный источник переделок
НФТ (производительность, доступность, безопасность, приватность, локализация, наблюдаемость) имеют неприятное свойство: их невозможно добавить потом дёшево, потому что они — свойства архитектуры, а не фич. «Добавим мультиарендность в следующем квартале» — почти всегда переписывание слоя данных.
Правило формулировки: НФТ должно быть измеримым и с указанием условий измерения.
# Плохо — непроверяемо
- "Система должна работать быстро"
- "Интерфейс должен быть удобным"
# Хорошо — превращается в тест и в дашборд
performance:
metric: "время от нажатия «Построить» до первого отрисованного байта отчёта"
target: "p95 ≤ 2.0 s, p99 ≤ 5.0 s"
conditions: "датасет ≤ 1 млн строк, 50 одновременных построений, регион eu-central-1"
measured_by: "RUM-метрика report_render_ttfb, окно 7 дней"
availability:
target: "99.9% успешных запросов к /api/reports за календарный месяц"
error_budget: "43 минуты недоступности в месяц"
accessibility:
target: "WCAG 2.2 уровень AA для критических сценариев: вход, импорт, экспорт"
measured_by: "axe-core в CI + ручная проверка клавиатурной навигации перед релизом"
privacy:
constraint: "персональные данные клиентов ЕС не реплицируются вне eu-*"
Такой формат делает НФТ частью инженерной практики: цифры попадают в SLO и алерты, а не в PDF-документ. Подробнее про бюджеты ошибок и SLO — в треке https://courses.digitable.life/post/architecture-patterns/00-overview/.
4.3. PRD, который читают
Живой PRD — 1–3 страницы, отвечающие на конкретные вопросы:
- Проблема и для кого — с доказательством: числами и цитатами.
- Почему сейчас — что изменилось; отсутствие ответа обычно означает отсутствие приоритета.
- Целевой результат — метрика и ожидаемый сдвиг, а не «улучшить опыт».
- Сценарии — 2–5 пользовательских историй верхнего уровня.
- Что вне охвата — самый полезный раздел документа.
- Ограничения и НФТ.
- Открытые вопросы — с именем ответственного и датой.
- Как поймём, что сработало — метрика, срок, критерий отката.
Всё остальное (макеты, схемы данных, API) живёт по ссылкам в источниках истины: Figma, схема, код. Дублирование содержимого в PRD гарантирует расхождение через две недели.
5. User stories
5.1. Что это на самом деле
История — не формат тикета, а обещание разговора. Рон Джеффрис описывает её тремя «C» (Ron Jeffries, «Essential XP: Card, Conversation, Confirmation»):
- Card — короткая запись-напоминание, помещающаяся на карточку;
- Conversation — разговор команды, где вскрывается контекст;
- Confirmation — критерии приёмки, фиксирующие итог разговора.
Ошибка, из-за которой истории «не работают»: команда оставляет только первую «C». Карточка без разговора — это плохая спецификация: слишком короткая, чтобы быть точной.
Канонический шаблон:
Как <роль>, я хочу <действие>, чтобы <ценность>.
Роль отвечает «для кого», действие — «что», ценность — «зачем». Ценность — самая важная и чаще всего профанируемая часть: «чтобы я мог нажать кнопку экспорта» — это не ценность, а тавтология. «Чтобы отправить бухгалтеру данные в формате, который принимает 1С» — ценность, и из неё сразу следуют требования к формату.
5.2. Job story: когда роль мешает
Альтернатива от Intercom (Alan Klement, «Replacing the User Story with the Job Story») убирает персону и добавляет ситуацию — это часто сильнее, потому что поведение определяется контекстом, а не демографией:
Когда <ситуация>, я хочу <мотивация>, чтобы <ожидаемый результат>.
Когда я получаю выписку с 3000 строк в конце квартала и вижу расхождение с учётом,
я хочу быстро найти непарные платежи,
чтобы закрыть период вовремя и не сверять построчно вручную.
Практическое правило: user story — когда важна роль и права доступа; job story — когда важен контекст срабатывания. Связь с JTBD разобрана в https://courses.digitable.life/post/product-management/01-discovery-and-research/.
5.3. INVEST: критерии годной истории
Билл Уэйк (INVEST in Good Stories):
| Буква | Свойство | Проверка |
|---|---|---|
| Independent | независима | можно ли выпустить её, не выпуская соседнюю? |
| Negotiable | обсуждаема | оставлено ли пространство для решения инженерами? |
| Valuable | ценна | смогу ли объяснить пользователю выгоду одной фразой? |
| Estimable | оценима | команда понимает объём хотя бы с точностью до порядка? |
| Small | мала | помещается в спринт с запасом (обычно ≤ 3–5 дней)? |
| Testable | проверяема | можно ли написать критерий «сделано»? |
Estimable и Small связаны: неоценимая история почти всегда велика. Если команда не может
оценить — это сигнал не «дожать оценку», а провести спайк: ограниченное по времени исследование
(«2 дня, ответ на вопрос: выдержит ли наш парсер XLSX с формулами»).
5.4. Критерии приёмки: два формата
Формат 1 — список правил. Хорош для правил вычислений и валидации:
История: Импорт банковской выписки
Критерии приёмки:
1. Поддерживаются форматы CSV (UTF-8, Windows-1251) и OFX.
2. Файл больше 20 МБ отклоняется с сообщением о лимите и подсказкой разбить по месяцам.
3. Строки с некорректной датой не блокируют импорт: они попадают в отчёт об ошибках,
остальные импортируются.
4. Повторный импорт того же файла не создаёт дублей: ключ дедупликации —
(номер счёта, дата, сумма, назначение).
5. Отчёт об ошибках доступен для скачивания в течение 7 дней.
Формат 2 — сценарии (Gherkin). Хорош, когда поведение зависит от состояния и когда критерии должны стать автотестами:
# language: ru
Функция: Дедупликация при импорте выписки
Предыстория:
Допустим пользователь "buh@example.com" авторизован
И в счёт "40702810" уже импортирована выписка за март
Сценарий: Повторная загрузка того же файла не создаёт дублей
Когда пользователь загружает файл "march.csv" в счёт "40702810"
Тогда количество транзакций за март не изменяется
И показано сообщение "Все 412 операций уже были загружены ранее"
Сценарий: Частичное пересечение периодов
Допустим файл "feb-mar.csv" содержит 200 февральских и 412 мартовских операций
Когда пользователь загружает файл "feb-mar.csv" в счёт "40702810"
Тогда импортировано 200 новых операций
И пропущено 412 операций как дубликаты
И отчёт об импорте содержит строку "пропущено дубликатов: 412"
Структура сценария: Некорректные строки не блокируют импорт
Когда пользователь загружает файл с <плохих> некорректными строками из <всего>
Тогда импортировано <ожидается> операций
И отчёт об ошибках содержит <плохих> строк
Примеры:
| всего | плохих | ожидается |
| 100 | 0 | 100 |
| 100 | 5 | 95 |
| 100 | 100 | 0 |
Тот же сценарий, исполняемый как тест (Python, behave/pytest-bdd):
# steps/import_steps.py — шаги, связывающие текст критериев с системой
from behave import given, when, then
@given('в счёт "{account}" уже импортирована выписка за март')
def step_preloaded(context, account: str) -> None:
context.result = context.api.import_statement(account, fixture("march.csv"))
assert context.result.imported == 412
@when('пользователь загружает файл "{filename}" в счёт "{account}"')
def step_upload(context, filename: str, account: str) -> None:
context.result = context.api.import_statement(account, fixture(filename))
@then("импортировано {expected:d} новых операций")
def step_check_imported(context, expected: int) -> None:
assert context.result.imported == expected, (
f"ожидали {expected}, получили {context.result.imported}"
)
Trade-off. Gherkin даёт живую документацию и защиту от регрессий, но стоит дорого: слой шагов нужно поддерживать, а плохо написанные сценарии («когда я нажимаю кнопку с id=submit») превращаются в хрупкие UI-тесты. Разумный компромисс, который держится в проде: Gherkin — только для бизнес-правил с высокой ценой ошибки (деньги, права доступа, расчёты), списки правил — для всего остального.
Про дедупликацию, кстати, стоит помнить о вычислительной стороне: наивная проверка «каждая новая строка против всех существующих» — это O(n·m) сравнений; правильная реализация строит хеш-множество ключей уже загруженных операций за период — O(n + m) времени и O(m) памяти. Такие вещи полезно фиксировать в критериях приёмки через НФТ: «импорт 100 000 строк ≤ 30 с».
6. Нарезка историй
6.1. Вертикально, а не горизонтально
Главное правило: история — сквозной срез через все слои системы, дающий пользователю работающий сценарий, пусть и узкий. «Сделать схему БД» — не история, потому что пользователь не может ничего с ней сделать и мы не узнаем ничего нового.
Экономическая суть вертикальной нарезки — опционы. Каждый выпущенный срез даёт информацию и право (но не обязанность) продолжать. Горизонтальная нарезка лишает вас этого права: пока не сделаны все слои, ценность равна нулю, и вся стоимость уже потрачена.
6.2. SPIDR: пять надёжных способов разрезать
Майк Кон (Mike Cohn, SPIDR):
- S — Spike. Не режется, потому что непонятна → выделить исследование, затем резать.
- P — Path. Разные пути через сценарий: оплата картой / по счёту / из баланса.
- I — Interface. Разные интерфейсы или платформы: сначала веб, потом мобильный; сначала одна валидация, потом продвинутая форма; сначала CSV, потом визуальный маппинг.
- D — Data. Подмножество данных: сначала одна валюта, один банк, одна схема файла.
- R — Rules. Ослабленные правила: сначала без учёта частичных возвратов и мультивалютности.
Дополнительные проверенные приёмы: разделить «happy path» и обработку ошибок (сначала первое), отделить ручное от автоматического (сначала оператор делает руками — это ещё и валидация спроса), отделить создание от редактирования и удаления.
6.3. Story mapping: от плоского бэклога к карте
Джефф Паттон («User Story Mapping») решает главную проблему бэклога: плоский список теряет последовательность и целостность. Карта историй возвращает двумерность.
источник"] --> A2["Проверить
данные"] --> A3["Построить
отчёт"] --> A4["Поделиться"] end subgraph R1["Срез 1 — walking skeleton"] direction LR B1["CSV одной
фиксированной схемы"] --- B2["Показать
10 первых строк"] --- B3["Одна таблица
без фильтров"] --- B4["Ссылка
только для чтения"] end subgraph R2["Срез 2 — расширение"] direction LR C1["Произвольные
колонки"] --- C2["Отчёт
об ошибках"] --- C3["Группировки
и фильтры"] --- C4["Экспорт
в XLSX"] end subgraph R3["Срез 3 — позже, если подтвердится"] direction LR D1["Прямая
интеграция с банком"] --- D2["Автоматическая
очистка данных"] --- D3["Кастомные
формулы"] --- D4["Расписание
рассылок"] end BACKBONE --> R1 --> R2 --> R3
Ключевое понятие — walking skeleton: тончайший сквозной срез через весь хребет, который уже позволяет человеку пройти сценарий от начала до конца. Он и есть кандидат в MVP (подробнее — https://courses.digitable.life/post/product-management/05-mvp-and-experiments/). Карта делает очевидным то, что плоский список скрывает: если из среза 1 выпал шаг «поделиться», сценарий не замыкается — пользователь построит отчёт и упрётся в тупик.
6.4. Жизненный цикл истории: DoR и DoD
Два состояния, которые обычно забывают:
- Measured. Без него «Done» означает «код в проде», а не «проблема решена». Именно здесь замыкается петля с https://courses.digitable.life/post/product-management/08-analytics-and-decisions/.
- Возврат из InProgress в Shaped. Разработчик, наткнувшийся на неучтённый случай, должен иметь право вернуть историю, а не выдумывать поведение самостоятельно. Отсутствие этого перехода — главная причина фраз «сделали не то, что просили».
В DoD стоит явно включить пункт «событие аналитики размечено и проверено». Иначе через месяц окажется, что измерить эффект нечем, — самая дорогая и самая распространённая организационная ошибка в продуктовой разработке.
7. Трассируемость: от боли до релиза
Когда продукт вырастает, возникает вопрос «почему мы это сделали?» — и на него нужно уметь ответить через год. Решение — модель трассируемости: явные связи между артефактами.
На практике это не отдельная система, а дисциплина полей в трекере: у истории есть ссылка на гипотезу, у гипотезы — на возможность и метрику, у релиза — на замер. Стоимость дисциплины — несколько минут на историю. Выгода — возможность ответить на вопросы «какие из наших гипотез вообще подтверждались» и «что можно удалить из продукта», не поднимая археологию в переписках.
8. Проверка: прототипы и юзабилити-тесты
8.1. Выбор верности прототипа
Правило выбора: берите самый дешёвый инструмент, дающий сигнал нужного типа. Для проверки понятности хватает кликабельного макета. Для проверки спроса макет бесполезен — нужна фейковая дверь или concierge, потому что слова о намерении систематически расходятся с поведением (подробнее — https://courses.digitable.life/post/product-management/05-mvp-and-experiments/).
8.2. Сколько пользователей нужно
Знаменитое «пяти достаточно» (Nielsen, «Why You Only Need to Test with 5 Users»)
— не догма, а следствие простой вероятностной модели. Если каждая проблема независимо
обнаруживается одним участником с вероятностью p, то доля найденных проблем при n
участниках:
found(n) = 1 − (1 − p)^n
"""Планирование юзабилити-теста: сколько участников нужно.
Сложность: O(1) на расчёт — это замкнутая формула, а не симуляция.
"""
from math import ceil, log
def discovered_share(n: int, p: float = 0.31) -> float:
"""Доля проблем, найденных при n участниках.
p = 0.31 — медианная оценка Нильсена и Ландауэра для типичных интерфейсов.
Для узкоспециализированных продуктов p ниже (0.10–0.20): сценарии разнообразнее,
и один участник задевает меньшую часть пространства проблем.
"""
if not 0 < p < 1:
raise ValueError("p должно быть в интервале (0, 1)")
return 1 - (1 - p) ** n
def participants_needed(target: float, p: float = 0.31) -> int:
"""Минимальное n, при котором ожидаемая доля найденных проблем ≥ target."""
if not 0 < target < 1:
raise ValueError("target должно быть в интервале (0, 1)")
return ceil(log(1 - target) / log(1 - p))
if __name__ == "__main__":
for n in (1, 3, 5, 8, 12, 15):
print(f"n={n:2d}: p=0.31 → {discovered_share(n):.0%} "
f"p=0.15 → {discovered_share(n, 0.15):.0%}")
print("\nдля 85% проблем нужно:",
participants_needed(0.85), "участников при p=0.31;",
participants_needed(0.85, 0.15), "— при p=0.15")
Вывод:
n= 1: p=0.31 → 31% p=0.15 → 15%
n= 3: p=0.31 → 67% p=0.15 → 39%
n= 5: p=0.31 → 84% p=0.15 → 56%
n= 8: p=0.31 → 95% p=0.15 → 73%
n=12: p=0.31 → 99% p=0.15 → 86%
n=15: p=0.31 → 100% p=0.15 → 91%
для 85% проблем нужно: 6 участников при p=0.31; 12 — при p=0.15
Три оговорки, без которых правило вредно:
- Работает только для однородной аудитории и одного сценария. Три сегмента — умножайте на три (или тестируйте по 3–4 на сегмент).
- Работает для поиска проблем юзабилити, а не для измерения. Чтобы утверждать «конверсия выросла», нужны сотни и тысячи наблюдений и A/B-тест.
- Лучше 3 итерации по 5 человек, чем один тест на 15. Первые 5 находят самые грубые проблемы, которые маскируют остальные; после исправления вторые 5 видят следующий слой.
8.3. Что мерить в тесте
Мнения («вам нравится?») почти бесполезны: люди вежливы и склонны к постфактум-рационализации. Мерить нужно поведение при выполнении задач:
- Task success rate — доля участников, дошедших до цели без помощи. Главная метрика.
- Time on task — медиана и p90, а не среднее (распределение тяжелохвостое).
- Число ошибок и точки, где человек застревал.
- Уровень запрошенной помощи — 0 (сам), 1 (подсказка), 2 (показали).
Стандартизированные опросники применимы, но после задач, а не вместо них: SUS (10 вопросов, шкала 0–100) и более короткий UMUX-Lite.
"""Расчёт SUS (System Usability Scale) с доверительным интервалом.
Сложность: O(n·k) по времени (n участников × 10 вопросов), O(n) по памяти.
"""
from statistics import mean, stdev
from math import sqrt
def sus_score(answers: list[int]) -> float:
"""Балл SUS одного участника: 10 ответов по шкале 1..5 → значение 0..100.
Нечётные вопросы сформулированы позитивно (вклад = ответ − 1),
чётные — негативно (вклад = 5 − ответ). Сумма вкладов × 2.5.
Чередование намеренное: оно ломает автоматическое проставление одной колонки.
"""
if len(answers) != 10 or not all(1 <= a <= 5 for a in answers):
raise ValueError("нужно ровно 10 ответов в диапазоне 1..5")
positive = sum(answers[i] - 1 for i in range(0, 10, 2))
negative = sum(5 - answers[i] for i in range(1, 10, 2))
return (positive + negative) * 2.5
def grade(score: float) -> str:
"""Перцентиль по нормативной базе Sauro & Lewis (~5000 тестов).
Важно: 68 — это средний по индустрии, а не «удовлетворительно» по школьной шкале.
Балл 70 означает лишь «немного лучше среднего продукта», а не «хорошо».
"""
if score >= 80.3:
return "A — верхние ~10%, вероятны рекомендации"
if score >= 68:
return "B/C — около среднего по индустрии"
return "D/F — заметно ниже среднего, есть системные проблемы"
def sus_summary(responses: list[list[int]], z: float = 1.96) -> dict:
"""Средний SUS по выборке и 95% доверительный интервал (нормальное приближение)."""
scores = [sus_score(r) for r in responses]
m = mean(scores)
half = z * stdev(scores) / sqrt(len(scores)) if len(scores) > 1 else None
return {
"n": len(scores),
"mean": m,
"ci": (m - half, m + half) if half else None,
"grade": grade(m),
}
if __name__ == "__main__":
sample = [
[4, 2, 5, 2, 4, 1, 5, 2, 4, 2],
[3, 3, 4, 2, 4, 2, 4, 3, 3, 2],
[5, 1, 5, 1, 5, 1, 5, 1, 5, 1],
[2, 4, 3, 4, 2, 4, 3, 3, 2, 4],
[4, 2, 4, 2, 4, 2, 5, 2, 4, 3],
]
print(sus_summary(sample))
На этой выборке результат — среднее 71.0 при 95% ДИ примерно (49, 93). Практический вывод: при n = 5 интервал занимает почти половину шкалы, то есть число «71» не говорит практически ничего — продукт с равным основанием может быть и заметно ниже среднего, и в верхних процентах. Используйте SUS для сравнения версий на выборках от 20 человек и для отслеживания динамики, а не для абсолютных заявлений на маленьком тесте.
9. Измерение UX в проде: HEART
Юзабилити-тест ловит проблемы на пяти людях в лаборатории. В проде нужен непрерывный сигнал. Фреймворк HEART от Google Research (Rodden, Hutchinson, Fu, «Measuring the User Experience on a Large Scale») даёт пять категорий и метод вывода метрик:
| Категория | Что означает | Пример метрики |
|---|---|---|
| Happiness | субъективная удовлетворённость | CSAT после сценария, SUS, доля жалоб |
| Engagement | глубина вовлечения | отчётов на активного пользователя в неделю |
| Adoption | освоение новой функции | доля активных, впервые применивших фичу за 30 дней |
| Retention | возвраты | W4-retention когорты, отток по фиче |
| Task success | успешность задачи | доля успешных импортов, медианное время до отчёта |
Механика вывода метрики — Goal → Signal → Metric: сначала цель («аналитик получает
первый отчёт без обращения в поддержку»), затем наблюдаемый сигнал («сессия завершилась
событием report_created без support_ticket_created»), только потом формула. Обратный
порядок («у нас есть события, посчитаем что-нибудь») даёт метрики, которые невозможно
интерпретировать.
Task success меряется из событийного лога:
-- Успешность сценария «первый отчёт» и время до него по когортам недели регистрации.
-- Сложность: O(n log n) — доминирует сортировка при оконных функциях и перцентилях.
WITH first_attempt AS (
SELECT
user_id,
DATE_TRUNC('week', MIN(occurred_at)) AS signup_week,
MIN(occurred_at) AS started_at
FROM events
WHERE event_name = 'import_started'
GROUP BY user_id
),
first_success AS (
SELECT user_id, MIN(occurred_at) AS succeeded_at
FROM events
WHERE event_name = 'report_created'
GROUP BY user_id
),
asked_help AS (
SELECT DISTINCT user_id
FROM events
WHERE event_name = 'support_ticket_created'
)
SELECT
a.signup_week,
COUNT(*) AS cohort_size,
-- доля дошедших до отчёта в течение 7 дней
AVG(CASE WHEN s.succeeded_at IS NOT NULL
AND s.succeeded_at <= a.started_at + INTERVAL '7 days'
THEN 1.0 ELSE 0.0 END) AS task_success_7d,
-- «чистый» успех: без обращения в поддержку
AVG(CASE WHEN s.succeeded_at IS NOT NULL AND h.user_id IS NULL
THEN 1.0 ELSE 0.0 END) AS unassisted_success,
-- время до ценности: медиана и хвост, среднее здесь бессмысленно
PERCENTILE_CONT(0.5) WITHIN GROUP (
ORDER BY EXTRACT(EPOCH FROM (s.succeeded_at - a.started_at)) / 3600
) AS ttv_hours_p50,
PERCENTILE_CONT(0.9) WITHIN GROUP (
ORDER BY EXTRACT(EPOCH FROM (s.succeeded_at - a.started_at)) / 3600
) AS ttv_hours_p90
FROM first_attempt a
LEFT JOIN first_success s ON s.user_id = a.user_id
LEFT JOIN asked_help h ON h.user_id = a.user_id
GROUP BY a.signup_week
ORDER BY a.signup_week;
Две детали, отличающие рабочий запрос от учебного: unassisted_success (успех с помощью
поддержки — это не успех продукта, а его субсидирование людьми) и перцентили вместо среднего
(время до ценности всегда распределено с тяжёлым хвостом, среднее в нём смещено выбросами).
10. Доступность как требование, а не как благотворительность
Доступность (a11y) — это НФТ, у которого есть измеримый критерий, юридические последствия в ряде юрисдикций и заметный побочный эффект: интерфейс становится удобнее для всех. Базис — принципы POUR из WCAG 2.2: воспринимаемость, управляемость, понятность, надёжность.
Минимум, который продакт обязан внести в критерии приёмки:
- Клавиатура. Весь критический сценарий проходится без мыши, фокус виден.
- Контраст. ≥ 4.5:1 для обычного текста, ≥ 3:1 для крупного (≥ 18.66px bold / 24px).
- Альтернативный текст для несущих смысл изображений; декоративные — скрыты от скринридера.
- Формы. У каждого поля есть программно связанная подпись; ошибка сообщается текстом, а не только красной рамкой.
- Не только цвет. Статус передаётся ещё и формой/иконкой/текстом (8% мужчин имеют нарушение цветового зрения).
Контраст считается по формуле относительной яркости WCAG — это O(1) и легко ставится в CI:
"""Проверка контраста по WCAG 2.x. O(1) по времени и памяти."""
def _channel(c: int) -> float:
"""Линеаризация канала sRGB (обратная гамма-коррекция)."""
s = c / 255
return s / 12.92 if s <= 0.04045 else ((s + 0.055) / 1.055) ** 2.4
def relative_luminance(rgb: tuple[int, int, int]) -> float:
"""Относительная яркость: веса отражают чувствительность глаза к каналам."""
r, g, b = (_channel(c) for c in rgb)
return 0.2126 * r + 0.7152 * g + 0.0722 * b
def contrast_ratio(fg: tuple[int, int, int], bg: tuple[int, int, int]) -> float:
"""Отношение контраста в диапазоне от 1:1 (одинаковые) до 21:1 (чёрный/белый)."""
l1, l2 = relative_luminance(fg), relative_luminance(bg)
lighter, darker = max(l1, l2), min(l1, l2)
return (lighter + 0.05) / (darker + 0.05)
def wcag_verdict(fg, bg, *, large_text: bool = False) -> dict:
ratio = contrast_ratio(fg, bg)
aa = 3.0 if large_text else 4.5
aaa = 4.5 if large_text else 7.0
return {
"ratio": round(ratio, 2),
"AA": ratio >= aa,
"AAA": ratio >= aaa,
"hint": "ок" if ratio >= aa else f"нужно ≥ {aa}:1 — затемните текст или осветлите фон",
}
if __name__ == "__main__":
print(wcag_verdict((119, 119, 119), (255, 255, 255))) # серый #777 на белом
print(wcag_verdict((153, 153, 153), (255, 255, 255))) # #999 на белом — провал AA
print(wcag_verdict((255, 255, 255), (0, 122, 204))) # белый на синем
Результат:
{'ratio': 4.48, 'AA': False, 'AAA': False, 'hint': 'нужно ≥ 4.5:1 — затемните текст или осветлите фон'}
{'ratio': 2.85, 'AA': False, 'AAA': False, 'hint': 'нужно ≥ 4.5:1 — затемните текст или осветлите фон'}
{'ratio': 4.51, 'AA': True, 'AAA': False, 'hint': 'ок'}
Показательно: #777 на белом даёт 4.48 — провал AA на 0.02, а белый на #007ACC даёт 4.51 —
проход с запасом в 0.01. Оба значения на глаз неразличимы, и таких «на границе» в реальных
палитрах десятки. Именно поэтому проверку надо автоматизировать (axe-core, pa11y в CI),
а не полагаться на глаз: автотесты ловят порядка 30–40% нарушений WCAG, остальное требует
ручной проверки, но эти 30–40% — самые массовые.
11. Типичные ошибки
- Спека вместо разговора. Документ на 40 страниц, который никто не дочитал, но все формально согласовали. Симптом: вопросы разработчиков, ответы на которые есть в тексте.
- Решение вместо проблемы в истории. «Как пользователь, я хочу выпадающий список» — в истории должна быть задача, а не элемент управления. Иначе команда лишена права найти решение дешевле и лучше.
- Ценность-тавтология. «…чтобы я мог экспортировать» в истории про экспорт. Правило: если «чтобы» повторяет «я хочу», значит, вы не знаете, зачем это нужно.
- CJM как плакат. Красивая карта на стене, построенная на воркшопе без единого пользователя. Лечится маркировкой ячеек источниками.
- Одна карта для всех сегментов. Усреднённый путь несуществующего человека.
- Горизонтальная нарезка. Спринты «сделали бэкенд», «сделали фронт»; ценность и обратная связь только в конце.
- Критерии приёмки только для happy path. Ошибки, пустые состояния, лимиты, права доступа, поведение при частичном отказе — не описаны, значит, будут реализованы наугад.
- Забытые пустые и предельные состояния. Экран, отлично выглядящий с 5 записями и разваливающийся на 0 и на 10 000. Правило: у каждого экрана есть четыре состояния — пустое, загрузка, ошибка, переполнение.
- НФТ без чисел. «Быстро», «надёжно», «безопасно» — нечего проверять и нечего мониторить.
- Аналитика после релиза. События размечают через месяц, когда нужно отчитаться; измерить эффект уже нельзя.
- Тестирование мнений вместо задач. «Вам нравится макет?» вместо «покажите, как вы сделали бы X».
- Дизайнер как рисовальщик. Дизайнера подключают после того, как решение уже выбрано, — теряется вся сила профессии, остаётся оформление.
- Доступность «во второй фазе». Второй фазы не бывает; переделка семантики и контрастов позже стоит в разы дороже.
- Смешение уровней требований. Бизнес-цель, сценарий и деталь реализации в одном списке — невозможно приоритизировать и невозможно сократить объём.
12. Как это устроено в проде
- Trio. Продакт, дизайнер и тимлид формулируют требования вместе, а не последовательно (product trio у Терезы Торрес). Дизайнер участвует в discovery, разработчик — в определении охвата; это дешевле, чем передавать артефакты через стену.
- Источник истины разнесён. Проблема и метрика — в PRD; макеты — в Figma; поведение API — в OpenAPI-схеме; правила — в критериях приёмки и тестах. PRD ссылается, а не копирует.
- Story-mapping-сессия перед крупной инициативой (2–3 часа) заменяет недели споров про объём: карта делает видимым, что можно выбросить, не сломав сценарий.
- Пример-воркшоп (Example Mapping, 25 минут). Команда раскладывает историю на правила, примеры и вопросы, используя карточки четырёх цветов (Matt Wynne, Cucumber). Обнаруженные вопросы обычно экономят несколько дней переделок.
- Флаги функциональности по умолчанию. Каждая заметная история выкатывается за флагом — это даёт постепенный раскат, быстрый откат и площадку для A/B-теста (https://courses.digitable.life/post/product-management/05-mvp-and-experiments/).
- Регулярный «разбор сессий». Час в неделю всей командой на просмотр записей реальных сессий и свежих тикетов поддержки. Самый дешёвый из известных способов поддерживать контакт с реальностью.
- Дизайн-система как требование. Компоненты с уже решёнными состояниями, контрастами и клавиатурной навигацией снимают целый класс требований с каждой истории. Экономия особенно заметна на фронтенде — см. https://courses.digitable.life/post/typescript/00-overview/.
- Ревью формулировок. Полезная практика: продакт пишет критерии приёмки, тестировщик их ревьюит до начала разработки. Тестировщик профессионально ищет незакрытые случаи, и делает это до того, как код написан, а не после.
13. Мини-итог
- UX — это весь опыт достижения цели, а не интерфейс; решения верхних уровней (стратегия, охват) определяют нижние, и ошибку наверху нельзя исправить внизу.
- Непонятный интерфейс — это разрыв исполнения или разрыв оценки; локализация проблемы по циклу действия Нормана превращает «неудобно» в чинибельную задачу.
- Сильнейший инструмент дизайна — ограничения: делать ошибку невозможной дешевле, чем её обрабатывать.
- CJM объединяет качественные, количественные и операционные данные в одну картину и показывает провалы между каналами, которых не видит воронка; ячейка без источника данных — гипотеза, и это надо помечать.
- Service blueprint нужен, когда опыт зависит от людей и процессов за линией видимости.
- Требования живут на пяти уровнях; смешение уровней делает список неприоритизируемым. НФТ должны быть числом с условиями измерения, иначе их не существует.
- User story — обещание разговора, а не формат тикета: карточка без разговора и критериев приёмки хуже, чем обычная спецификация.
- INVEST и SPIDR дают проверяемый способ резать истории вертикально; вертикальный срез — это опцион: право продолжить, а не обязанность.
- Story map возвращает бэклогу измерение последовательности и делает видимым walking skeleton.
- Пяти пользователей достаточно для поиска проблем юзабилити при однородной аудитории, но не для измерений; лучше три итерации по пять, чем один тест на пятнадцать.
- В проде UX измеряется непрерывно: HEART + Goal-Signal-Metric, с успехом задачи как главной метрикой и обязательным отделением «успеха без помощи поддержки».
- Доступность — обычное НФТ с численным критерием и автоматической проверкой в CI.
Источники
- Don Norman. The Design of Everyday Things, revised ed., 2013.
- Jakob Nielsen. 10 Usability Heuristics for User Interface Design — NN/g.
- Jesse James Garrett. The Elements of User Experience, 2000.
- Jeff Patton. User Story Mapping, O’Reilly, 2014.
- Mike Cohn. SPIDR: Five Simple but Powerful Ways to Split User Stories.
- Bill Wake. INVEST in Good Stories, and SMART Tasks.
- Ron Jeffries. Card, Conversation, Confirmation.
- Alan Klement. Replacing the User Story with the Job Story.
- Karl Wiegers, Joy Beatty. Software Requirements, 3rd ed.
- Rodden, Hutchinson, Fu. Measuring the User Experience on a Large Scale: HEART, Google, CHI 2010.
- Nielsen Norman Group. Why You Only Need to Test with 5 Users.
- Jeff Sauro, James Lewis. Quantifying the User Experience — про SUS, UMUX-Lite и статистику в UX.
- W3C. WCAG 2.2 Quick Reference.
- Matt Wynne. Introducing Example Mapping — Cucumber.
- Nielsen Norman Group. Journey Mapping 101 и Service Blueprints.
- Barry Boehm, Victor Basili. Software Defect Reduction Top 10 List, IEEE Computer, 2001.
Что дальше
Мы научились превращать знание о пользователе в работающий интерфейс и проверяемые требования. Но продукт, которым удобно пользоваться, ещё не зарабатывает. Как назначить цену, выбрать модель монетизации, упаковать тарифы и запустить механику роста — в следующей статье: