Ext Ops Panel
Открыть панель

Главная/Документация/Обратная связь

Контракт обратной связи

Версия контракта 1.0.0 · бесплатно на всех тарифах

Небольшой диалог, который человек может открыть на любой странице вашего сайта или продукта и рассказать, что не работает или чего не хватает. Вставьте один тег скрипта или вызывайте эндпоинт из своего диалога. Присланное попадает в один блок панели рядом с опросом при удалении и Rate Us, а владелец получает уведомление во «Входящие».

↓ Скачать OpenAPI 3.1 (YAML) /feedback-openapi.yaml · info.version 1.0.0

1. Вставьте виджет

Один тег с defer, без inline-кода, поэтому работает при строгом CSP:

<script src="https://extops.dev/widget.js"
        data-extops-widget="wk_XXXXXXXXXXXXXXXXXXXXXXXXXXXX" defer></script>

Разрешите наш домен в script-src и connect-src. Файл self-hosted, без зависимостей и весит меньше 7 KB gzip; стили идут через CSSOM внутри shadow root, поэтому 'unsafe-inline' не нужен, а CSS вашей страницы не ломает диалог.

Необязательные атрибуты: data-locale, data-api, data-position="left" и data-button="false", если хотите открывать диалог сами через window.ExtOpsFeedback.open(). Ещё: data-button-style="icon" для круглой кнопки-пиктограммы 48px, data-surface="app", чтобы на телефоне кнопка стояла над нижней навигацией приложения, и data-color-preset, чтобы цвет кнопки задавала страница.

Светлая и тёмная тема

Каждый цветовой пресет рисуется в светлой и тёмной теме. По умолчанию (data-theme="auto") виджет следует за вашей страницей: <html data-theme>, класс dark или light на <html> или его color-scheme. Страница без своей темы получает тему ОС посетителя. Когда ваш переключатель темы меняет одно из этих свойств, виджет перекрашивается сразу, без перезагрузки.

Чтобы зафиксировать тему, поставьте data-theme="light" или data-theme="dark". Если страница хранит тему там, где виджет её не видит, вызовите window.ExtOpsFeedback.setTheme('dark') из своего переключателя, а setTheme('auto') вернёт следование за страницей.

2. Или вызывайте эндпоинт сами

Для сайта, который вообще не допускает сторонних скриптов, или продукта, которому нужна форма внутри своей дизайн-системы. Эндпоинт тот же, куда пишет наш виджет, так что выбирать между своим интерфейсом и нашим пайплайном не приходится.

await fetch('https://extops.dev/api/v1/feedback/widget', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    widget_key: 'wk_XXXXXXXXXXXXXXXXXXXXXXXXXXXX',
    topic: 'bug',
    comment: userText,
    page: location.href,
    locale: navigator.language,
    event_id: crypto.randomUUID(),
    dwell_ms: Date.now() - openedAt,
  }),
});

Темы, которые настроил владелец, читайте из GET /api/v1/feedback/widget/config?key=… – тогда ваш диалог предлагает ровно то же, что и панель. Один event_id используйте на все повторы: сервер дедуплицирует по нему, поэтому сбой сети не превратит одно сообщение в два.

3. Ключ виджета публичный намеренно

Он напечатан в исходнике страницы, и это нормально: его единственное право – создать одну строку обратной связи. Он не умеет писать события install/uninstall, не читает ничего кроме конфигурации диалога и не может назвать проект (поле project_id в теле не принимается). Именно поэтому у виджета свой ключ, а не webhook_token источника: тот умеет писать события метрик и в HTML попадать не должен. Ключ можно перевыпустить в любой момент – старый перестаёт приниматься сразу.

4. Что мы храним и чего не храним

Храним тему, текст, нормализованную страницу, локаль, необязательный контакт, время, вид фидбека и виджет, которым он собран.

Срок хранения – 180 дней, то же правило, что и для остальных видов обратной связи.

5. Ответы и коды ошибок

Успех выглядит так: {"status":"ok","recorded":true}. recorded: false – тоже успех: это повтор либо сработавшая анти-бот эвристика. Показывайте одно и то же «спасибо»: скрипт не должен узнать, какое правило его поймало, а человек, нажавший «Отправить» после таймаута, не должен увидеть ошибку по сообщению, которое у нас уже есть.

HTTPcodeКогда
400validation_errorнет ключа, нет темы, тема не из списка виджета, пустой комментарий, битое тело
403origin_not_allowedисточник страницы не в allowlist виджета
404not_foundнеизвестный ключ или выключенный виджет: ответ одинаковый, чтобы публичный ключ не служил зондом «существует ли проект»
429rate_limited60/час на ключ, 10/час на IP; с заголовком Retry-After
401source_token_invalidтолько серверный путь: неверный токен источника
500internal_errorнаша ошибка

6. Темы, виды и статусы

Тема – это id и label. Id – слаг, потому что он хранится на каждой строке и становится значением фильтра в панели; label – слова самого владельца. Виджет без настроенных тем предлагает дефолтные: bug, idea, question, other.

Цвет диалога – один из шести пресетов, общих с hosted-страницами: amber (по умолчанию), teal, indigo, violet, rose, slate. Список общий намеренно: виджет и hosted-страницы одного владельца должны выглядеть одинаково, а вторая палитра разъехалась бы с первой.

У сохранённой строки есть вид: uninstall, rate_us или widget. Вид – это ТЕГ, а не отдельный пайплайн: блок панели, уведомления и срок хранения общие. Статус разметки владельца – new, read или spam; пометка спамом убирает строку из рабочего списка и из уведомлений, не удаляя написанное человеком, и её можно вернуть.

7. Защита от абуза живёт на сервере

Раз ключ публичный, проверка на стороне браузера была бы советом, а не правилом. Её несёт сервер: allowlist источников виджета (пустой список = любой источник, ведь у клиента может быть десяток поддоменов), два независимых лимита (на ключ и на IP), honeypot-поле и минимальное время в диалоге, а также дедупликация по (widget_id, event_id) в базе, а не предварительной проверкой: два одновременных повтора прошли бы SELECT оба.

Оба лимитера fail-open: если наш Redis недоступен, обратная связь человека всё равно до вас дойдёт. Для ручки, которая пишет один абзац, это меньшее зло. Внешнюю капчу не добавляем – она ломает ваш CSP и приватность ваших посетителей.

8. Машиночитаемый контракт

Формальное описание обоих эндпоинтов приёма, чтения конфигурации, перечислений и кодов ошибок – файл OpenAPI 3.1. В нём та же версия контракта (1.0.0).

↓ Скачать OpenAPI 3.1 (YAML) /feedback-openapi.yaml