Как это устроено. Для разработчиков
Механизм анонсов новых фич
Назначение
Анонсы новых фич — отдельная подсистема рядом с турами, но не часть механизма туров.
Тур учит пользователя работать со страницей и запускается по кнопке ?.
Анонс новой фичи — разовое уведомление формата «здесь появилось новое». Он показывается автоматически на нужной странице, привязывается к конкретному элементу интерфейса и после просмотра больше не появляется.
Общее с турами у анонсов только:
-
data-tourкак способ поставить якорь на DOM-элемент; -
waitForElement()как механизм ожидания появления якоря; - Driver.js как библиотека показа.
Всё остальное у анонсов своё:
- отдельный реестр;
- отдельное localStorage-хранилище;
- отдельный Pinia-store прогресса;
- отдельная точка автозапуска;
- отдельный срок жизни.
Как использовать скилл feature-announcement для добавления анонса новой фичи.
Если этот skill еще не у тебя. Скажи Клауду установить скилл из репозитория. https://gitlab.com/MiroScopeGroup/skills Открой новую сессию Claude Code в проекте (или просто попроси в чате): «добавь анонс новой фичи для кнопки Х», «нужно анонсировать [фича]», «покажи хинт про новую фичу» — Claude сам распознает и вызовет скилл. Ответь на три вопроса по ходу диалога: на какой элемент повесить анонс (страница (компонент) + конкретный UI-элемент); card или hint — если не уверен, Claude подскажет критерий; текст — заголовок и описание, при желании отдельная подпись для меню «Что нового». Claude предложит id анонса — подтверди его (после продакшена он неизменяем), после чего Claude сам создаст файл, зарегистрирует его и прогонит тесты. Пример ключа в local storage: Ключ: sociometer:feat:marked-poll Значение: {"expiresAt":1789114576162,"seen":true} Важно, если значение "seen" = true, то это означает, что, анонс был уже просмотрен и больше не будет виден. Для тестов нужно удалять этот параметр вручную или в профиле пользователя есть кнопка. позволяющая просмотреть все анонсы заново "Сбросить обучение". Можно пройти в профиль и нажать на эту кнопку, для того, чтобы руками не изменять ничего в local storage. Но при этом будут показаны все туры заново. (в профиле «Сбросить обучение» → зайти на страницу фичи, убедиться, что анонс показался)
Явно называть команду/слэш не обязательно — достаточно попросить обычными словами, скилл триггерится по описанию.
Удаление анонса через скилл feature-announcement
Скилл умеет не только заводить анонсы, но и полностью убирать уже существующие (пользователь решил, что больше не нужно его отображать или удалил фичу, и т. п. ).
Как вызвать: попросить обычными словами — «удали анонс фичи X», «убери анонс с заголовком "Список избранных"», «удали feat-marked-poll».
Что нужно указать: ID анонса (feat-
Что произойдёт:
Скилл найдёт анонс и покажет его на подтверждение (id, заголовок, файл, route) — удаление необратимо в рабочем дереве.
После подтверждения удалит файл src/tour/features/
Что НЕ будет тронуто:
data-tour="
Ключевые правила
- Анонс показывается автоматически при попадании пользователя на маршрут, указанный в определении анонса.
- Анонс живёт для конкретного пользователя 14 дней с момента первой встречи, а не с даты релиза.
- Просмотренный анонс больше не показывается.
- Если анонс не удалось показать — например, не найден якорь или сейчас идёт тур — он не считается просмотренным и попробует показаться при следующем заходе.
- Анонсы не зависят от флага «Больше не показывать туры». Пользователь может отключить обучение, но это не отключает новости продукта.
- Просроченные анонсы из кода удаляются отдельным npm-скриптом.
Задействованные файлы
Основные файлы механизма
| Файл | Назначение |
|---|---|
/src/tour/features/README.md |
Документация для разработчика: чек-лист, формат анонса, правила регистрации и очистки |
/src/tour/features/index.js |
Реестр всех активных анонсов, функции отбора по маршруту и для будущего меню «Что нового» |
/src/tour/featureStorage.js |
Работа с localStorage: ключи sociometer:feat:*, чтение, запись, удаление, TTL 14 дней |
/src/tour/featureProgress.js |
Pinia-store прогресса анонсов: создание записи, проверка просмотра/истечения, отметка seen, сброс |
/src/tour/runPendingFeatureAnnouncements.js |
Основная политика автопоказа: отбор анонсов, ожидание якорей, запуск карточек/хинтов, отметка просмотра |
/src/tour/featureHints.js |
Показ анонсов типа hint через driver.js/hints |
/src/tour/featureHints.css |
Стили маячка Новое для hint-анонсов |
/src/tour/featurePrune.js |
Чистая логика определения просроченных анонсов и удаления регистрации из реестра |
/scripts/pruneFeatureAnnouncements.js |
CLI-скрипт npm run features:prune для удаления просроченных анонсов из кода |
/src/App.vue |
Единая точка запуска анонсов на смену маршрута и при появлении авторизации |
/src/pages/profile.vue |
Кнопка «Сбросить обучение», которая сбрасывает и прогресс туров, и прогресс анонсов |
Тесты
| Файл | Что проверяет |
|---|---|
/src/tour/featureStorage.test.js |
Префиксы ключей, TTL, чтение/запись/удаление записей анонсов |
/src/tour/featureProgress.test.js |
Поведение Pinia-store: ensureRecord, markSeen, isExpired, resetAll, revision |
/src/tour/features/registry.test.js |
Отбор анонсов по маршруту, сортировку по addedAt, данные для будущего меню |
/src/tour/features/featureIds.test.js |
Валидацию реальных анонсов: id, route, addedAt, kind, popover, label, регистрацию в реестре |
/src/tour/runPendingFeatureAnnouncements.test.js |
Политику показа карточек и хинтов |
/src/App.featureAnnouncements.test.js |
Интеграцию в App.vue: watch маршрута, запуск после авторизации, retry, снятие хинтов |
Пример текущего анонса
Файл:
/src/tour/features/marked-poll.js
Содержимое:
export default {
id: "feat-marked-poll",
addedAt: "2026-08-28",
route: "POLLS",
kind: "hint",
element: '[data-tour="marked-poll"]',
popover: {
title: "Список избранных",
description: "Теперь можно добавлять опросы в список избранных",
},
label: "Избранные опросы",
};
Якорь для него стоит в:
/src/pages/pollList.vue
<div
class="polls_filter__btn polls_list__hdr-btn"
data-tour="marked-poll"
:class="{ 'polls_list__hdr-btn--on': favoriteOnly }"
@click="toggleFavoriteOnly()"
>
Как работает механизм
1. Разработчик ставит якорь на элемент интерфейса
Анонс должен быть привязан к конкретному DOM-элементу. Для этого на элемент ставится атрибут:
data-tour="<slug>"
Например:
<div data-tour="marked-poll">
...
</div>
Важно: якорь нужно ставить именно на тот элемент, около которого должен появиться анонс, а не на случайную обёртку и не на внутренний элемент библиотеки Element Plus.
Обычно селектор в анонсе выглядит так:
element: '[data-tour="marked-poll"]'
2. Анонс описывается отдельным файлом
Каждый анонс — это отдельный файл в папке:
/src/tour/features/
Имя файла — slug анонса:
/src/tour/features/<slug>.js
Например:
/src/tour/features/marked-poll.js
Файл должен экспортировать только данные, без логики:
export default {
id: "feat-marked-poll",
addedAt: "2026-08-28",
route: "POLLS",
kind: "hint",
element: '[data-tour="marked-poll"]',
popover: {
title: "Список избранных",
description: "Теперь можно добавлять опросы в список избранных",
},
label: "Избранные опросы",
};
3. Анонс регистрируется в общем реестре
Реестр находится в файле:
/src/tour/features/index.js
Пример:
import { useFeatureProgress } from "@/tour/featureProgress.js";
import markedPoll from "./marked-poll.js";
export const featureAnnouncements = {
markedPoll,
};
Важно соблюдать формат регистрации:
import someFeature from "./some-feature.js";
export const featureAnnouncements = {
someFeature,
};
Почему это важно:
- скрипт очистки
npm run features:pruneудаляет просроченные анонсы из реестра построчно; - он ожидает понятный формат импорта и регистрации;
- если регистрация будет записана слишком сложно или растянута на несколько строк, скрипт откажется работать и ничего не удалит.
4. App.vue автоматически запускает проверку анонсов
Разработчику не нужно вручную вызывать запуск анонса в created() страницы.
Единая точка запуска находится в:
/src/App.vue
Там импортируется:
import {
runPendingFeatureAnnouncements,
FEATURE_SKIP_TOUR_ACTIVE,
} from "@/tour/runPendingFeatureAnnouncements.js";
App.vue следит за изменением имени маршрута:
watch: {
"$route.name": {
immediate: true,
handler(name, oldName) {
...
this.runFeatureAnnouncements(name);
},
},
}
Также есть отдельный watcher на авторизацию:
"useStoreProfile.isAuth"(isAuth) {
if (isAuth) this.runFeatureAnnouncements(this.$route?.name);
}
Это нужно для ситуации, когда пользователь попал в приложение сразу по ссылке, но на момент первого route-watch ещё не был авторизован.
5. runPendingFeatureAnnouncements() выбирает подходящие анонсы
Основная логика находится в:
/src/tour/runPendingFeatureAnnouncements.js
Функция получает имя маршрута:
runPendingFeatureAnnouncements(routeName, { router, now, registry })
Дальше она:
- берёт анонсы для текущего маршрута через
announcementsForRoute(); - отбрасывает уже просмотренные;
- отбрасывает истёкшие для пользователя;
- создаёт запись в localStorage, если пользователь встретил анонс впервые;
- ждёт появления DOM-якоря через
waitForElement(); - показывает анонс как
cardилиhint; - отмечает анонс просмотренным в нужный момент.
Отбор анонсов по маршруту
Файл:
/src/tour/features/index.js
Функция:
export function announcementsForRoute(routeName, registry = featureAnnouncements) {
return Object.values(registry)
.filter((announcement) => matchesRoute(announcement, routeName))
.sort((a, b) => String(a.addedAt).localeCompare(String(b.addedAt)));
}
route в анонсе может быть строкой:
route: "POLLS"
или массивом строк:
route: ["POLLS", "RESULTS"]
Если на одном маршруте несколько анонсов, они сортируются по addedAt: более старые показываются первыми.
Хранилище прогресса
Ключи localStorage
Файл:
/src/tour/featureStorage.js
Константы:
export const FEATURE_ID_PREFIX = "feat-";
export const FEATURE_KEY_PREFIX = "sociometer:feat:";
export const FEATURE_TTL_MS = 14 * 24 * 60 * 60 * 1000;
Для анонса:
id: "feat-marked-poll"
ключ в localStorage будет:
sociometer:feat:marked-poll
Значение:
{
"expiresAt": 1800000000000,
"seen": false
}
Почему id начинается с feat-
Префикс feat- — единственный признак, по которому приложение отличает анонс новой фичи от шагов обычных туров.
Правильно:
id: "feat-results-pdf-export"
Неправильно:
id: "results-pdf-export"
После выхода анонса в прод id нельзя переименовывать: это часть ключа в браузере пользователя. Переименование будет воспринято как новый анонс, и пользователи увидят его повторно.
Срок жизни анонса для пользователя
Анонс показывается конкретному пользователю максимум 14 дней с момента первой встречи.
Это делает featureProgress.ensureRecord():
ensureRecord(id, now = Date.now()) {
const existing = getFeatureRecord(id);
if (existing) return existing;
const record = { expiresAt: now + FEATURE_TTL_MS, seen: false };
setFeatureRecord(id, record);
this.revision++;
return record;
}
Важный момент: срок отсчитывается не от addedAt, а от момента, когда пользователь впервые оказался на маршруте, где мог увидеть анонс.
Например:
- фича вышла 1 августа;
- пользователь впервые зашёл на нужную страницу 10 августа;
- его личное окно показа закончится 24 августа.
Типы анонсов: card и hint
Поле kind определяет способ показа:
kind: "card"
или:
kind: "hint"
card
card — это карточка Driver.js поверх страницы. Используется, когда фичу нужно объяснить текстом.
Пример:
export default {
id: "feat-results-pdf-export",
addedAt: "2026-08-28",
route: "RESULTS",
kind: "card",
element: '[data-tour="results-pdf-export"]',
popover: {
title: "Экспорт в PDF",
description: "Теперь результаты опроса можно выгрузить в PDF.",
},
label: "Экспорт результатов в PDF",
};
Показывается через runTour() из tourEngine.js, но как одношаговый тур:
await runTour(
{
id: announcement.id,
steps: [
{
id: announcement.id,
element: announcement.element,
popover: announcement.popover,
optional: announcement.optional,
},
],
},
{
router,
onShown: () => {
progress.markSeen(announcement.id);
shown.push(announcement.id);
},
}
);
Анонс типа card считается просмотренным в момент появления карточки на экране, а не при закрытии.
Почему так: если пользователь уйдёт со страницы или произойдёт программный редирект, promise runTour() может не завершиться. Если ждать закрытия, анонс мог бы показываться снова и снова.
hint
hint — это маячок «Новое» около элемента. Используется, когда фича понятна сама по себе и не нужно перекрывать экран карточкой.
Пример:
export default {
id: "feat-marked-poll",
addedAt: "2026-08-28",
route: "POLLS",
kind: "hint",
element: '[data-tour="marked-poll"]',
popover: {
title: "Список избранных",
description: "Теперь можно добавлять опросы в список избранных",
},
label: "Избранные опросы",
};
Показывается через:
/src/tour/featureHints.js
и driver.js/hints.
Визуально это бейдж:
Новое
Стили находятся в:
/src/tour/featureHints.css
Анонс типа hint считается просмотренным только после закрытия конкретного хотспота — например, после нажатия «Понятно».
Причина: сам факт появления маячка ещё не гарантирует, что пользователь его увидел. Его могла перекрыть модалка, открывшаяся следующим тиком.
Ожидание якоря
Анонс ждёт DOM-элемент через:
/src/tour/waitForElement.js
В runPendingFeatureAnnouncements.js используются таймауты:
export const ANCHOR_TIMEOUT_MS = 8000;
export const OPTIONAL_ANCHOR_TIMEOUT_MS = 100;
Обычный анонс ждёт якорь до 8 секунд.
Если у анонса есть:
optional: true
якорь ждётся только 100 мс.
optional: true нужен для ситуаций, когда элемент может структурно отсутствовать:
- пустой список;
- отключённый режим;
- элемент доступен только при определённом состоянии страницы.
Если якорь не найден, анонс не помечается просмотренным.
Защита от конфликтов с турами и хинтами
Если уже идёт тур
Для hint-анонсов есть проверка:
if (pendingHints.length > 0 && isTourActive()) {
return markSkippedBecause(shown, FEATURE_SKIP_TOUR_ACTIVE);
}
Если сейчас активен тур, хинт-анонс не показывается и не помечается просмотренным.
В App.vue для такого случая есть retry: если анонс был пропущен из-за активного тура, запуск повторяется позже.
Если открыт мобильный drawer
featureHints.js содержит:
pauseFeatureHintsForDrawer()
resumeFeatureHintsForDrawer()
Это нужно, чтобы маячки анонсов не просвечивали поверх мобильного бокового меню.
Если пользователь нажал кнопку ?
В App.vue перед показом обычных подсказок/туров вызывается:
dismissFeatureHintsBeforeHelp()
Если на экране есть анонс-хинт, он сначала снимается. Иначе два разных набора хинтов могли бы конфликтовать.
При смене маршрута
App.vue гасит показанный анонс-хинт при уходе со страницы, потому что у анонса нет компонента-владельца страницы, который мог бы сделать teardown в beforeUnmount.
Формат определения анонса
Обязательные поля:
| Поле | Тип | Назначение |
|---|---|---|
id |
string |
Идентификатор в формате feat-<slug> |
addedAt |
string |
Дата релиза в формате YYYY-MM-DD; используется для очистки из кода |
route |
string | string[] |
Имя маршрута из src/routes.js, где показывать анонс |
kind |
"card" | "hint" |
Тип показа |
element |
string |
CSS-селектор якоря |
popover.title |
string |
Заголовок карточки/хинта |
popover.description |
string |
Описание; пустым быть не должно |
label |
string |
Короткое название для будущего меню «Что нового» |
Необязательные поля:
| Поле | Тип | Назначение |
|---|---|---|
pruneAfterDays |
number |
Сколько дней анонс живёт в коде; по умолчанию 28 |
optional |
boolean |
Если true, якорь может отсутствовать, и его ждут коротким таймаутом |
beacon |
object |
Дополнительные настройки beacon для hint |
popover.popoverClass |
string |
Дополнительный CSS-класс popover |
Что нужно сделать разработчику, чтобы опубликовать анонс
Шаг 1. Выбрать slug
Slug должен быть стабильным и понятным.
Примеры:
marked-poll
results-pdf-export
dashboard-new-chart
Из slug формируются:
- имя файла;
-
data-tour; -
id; - localStorage-ключ.
Например для slug:
results-pdf-export
получается:
Файл: src/tour/features/results-pdf-export.js
id: feat-results-pdf-export
data-tour: results-pdf-export
localStorage: sociometer:feat:results-pdf-export
Шаг 2. Поставить якорь на элемент
В нужном Vue-компоненте добавить:
data-tour="results-pdf-export"
Пример:
<el-button data-tour="results-pdf-export">
Экспорт в PDF
</el-button>
Важно:
- не ставить якорь на скрытый элемент;
- не ставить якорь на нестабильную внутреннюю разметку Element Plus;
- убедиться, что элемент реально есть на маршруте, указанном в
route.
Шаг 3. Создать файл анонса
Создать файл:
/src/tour/features/results-pdf-export.js
Пример для карточки:
export default {
id: "feat-results-pdf-export",
addedAt: "2026-08-28",
route: "RESULTS",
kind: "card",
element: '[data-tour="results-pdf-export"]',
popover: {
title: "Экспорт в PDF",
description: "Теперь результаты опроса можно выгрузить в PDF-файл.",
},
label: "Экспорт результатов в PDF",
};
Пример для хинта:
export default {
id: "feat-results-pdf-export",
addedAt: "2026-08-28",
route: "RESULTS",
kind: "hint",
element: '[data-tour="results-pdf-export"]',
popover: {
title: "Экспорт в PDF",
description: "Теперь результаты опроса можно выгрузить в PDF-файл.",
},
label: "Экспорт результатов в PDF",
};
Шаг 4. Зарегистрировать анонс
Открыть:
/src/tour/features/index.js
Добавить импорт:
import resultsPdfExport from "./results-pdf-export.js";
Добавить запись в featureAnnouncements:
export const featureAnnouncements = {
markedPoll,
resultsPdfExport,
};
Рекомендуемый формат:
import resultsPdfExport from "./results-pdf-export.js";
export const featureAnnouncements = {
resultsPdfExport,
};
Не растягивать одну регистрацию на несколько строк.
Плохо:
export const featureAnnouncements = {
"feat-results-pdf-export":
// комментарий
resultsPdfExport,
};
Шаг 5. Проверить тестами
Запустить все тесты:
npm test
Или только тесты анонсов:
npx vitest run src/tour/features/
Минимально полезная проверка механизма:
npx vitest run src/tour/featureStorage.test.js src/tour/featureProgress.test.js src/tour/features/registry.test.js src/tour/features/featureIds.test.js src/tour/runPendingFeatureAnnouncements.test.js src/App.featureAnnouncements.test.js
featureIds.test.js поймает:
- забытый префикс
feat-; - дублирующийся
id; - несуществующий
route; - неправильный
addedAt; - неизвестный
kind; - пустой
element; - пустой
label; - пустой
popover.title; - пустой
popover.description; - файл анонса, который забыли зарегистрировать в
index.js.
Шаг 6. Проверить руками
- Перейти в профиль.
- Нажать кнопку «Сбросить обучение».
- Открыть страницу, указанную в
route. - Убедиться, что анонс появился.
- Обновить страницу или зайти снова.
- Убедиться, что анонс больше не появляется.
Кнопка «Сбросить обучение» находится в:
/src/pages/profile.vue
Она вызывает:
useTourProgress().resetAll(Object.keys(tours));
useFeatureProgress().resetAll();
То есть сбрасывает и туры, и анонсы новых фич.
Очистка просроченных анонсов из кода
Анонс живёт в коде по умолчанию 28 дней с даты addedAt.
Это задаётся в:
/src/tour/featurePrune.js
export const DEFAULT_PRUNE_AFTER_DAYS = 28;
Почему 28 дней:
- 14 дней — чтобы пользователь вообще успел зайти в систему после релиза;
- ещё 14 дней — его личное окно показа с момента первой встречи.
Если нужно изменить срок жизни конкретного анонса в коде:
export default {
id: "feat-some-feature",
addedAt: "2026-08-28",
pruneAfterDays: 45,
...
};
Проверить просроченные анонсы
npm run features:prune
Без --apply это dry-run:
- печатает список просроченных анонсов;
- ничего не удаляет;
- если есть просроченные анонсы, выходит с ненулевым кодом.
Удалить просроченные анонсы
npm run features:prune -- --apply
Скрипт:
- находит просроченные файлы в
/src/tour/features/; - проверяет, что может безопасно удалить регистрацию из
index.js; - удаляет файл анонса;
- удаляет импорт и запись из
featureAnnouncements.
Если формат регистрации непонятный, скрипт откажется работать и ничего не удалит.
Что делать не нужно
Не нужно добавлять вызов анонса в created() страницы.
Не нужно трогать:
/src/tour/tours/
Не нужно добавлять анонс в ROUTE_TOURS.
Не нужно вызывать:
autoRunPendingSteps()
Не нужно придумывать новый ключ localStorage.
Не нужно переименовывать id анонса после выхода в прод.
Не нужно удалять просроченные анонсы руками — для этого есть:
npm run features:prune -- --apply
Если анонс не показался
Проверять по порядку:
- Пользователь авторизован? На экране логина анонсы не показываются.
- Совпадает ли
routeс текущим$route.name? - Есть ли элемент с нужным
data-tourв DOM? - Не скрыт ли элемент?
- Не стоит ли в localStorage запись
sociometer:feat:<slug>сseen: true? - Не истёк ли
expiresAt? - Не идёт ли сейчас обычный тур?
- Не забыли ли зарегистрировать файл в
/src/tour/features/index.js? - Проходит ли
npx vitest run src/tour/features/?
Для ручной перепроверки проще всего нажать «Сбросить обучение» в профиле.
Краткий чек-лист публикации
- Выбрать slug.
- Поставить
data-tour="<slug>"на элемент фичи. - Создать файл
/src/tour/features/<slug>.js. - Указать
id: "feat-<slug>". - Указать корректный
addedAt. - Указать существующий
routeизsrc/routes.js. - Выбрать
kind: "card"илиkind: "hint". - Заполнить
element,popover.title,popover.description,label. - Зарегистрировать анонс в
/src/tour/features/index.js. - Запустить тесты.
- Сбросить обучение в профиле и проверить показ руками.
- После истечения срока жизни в коде удалить через
npm run features:prune -- --apply.