Всем привет! Сталкивались ли вы с ситуацией, когда в вашей админке пользователям трудно интуитивно разобраться, возможности быстро исправить это нет, а существующую документацию никто не читает? Знакомы ли вам частые вопросы вида "А как это настроить?" или "А можно ли сделать настройками X?", ответы на которые уже описаны?

С подобным столкнулся я, и подумал, а что легко и быстро можно попробовать в качестве эксперимента. В статье расскажу вариант решения, как все это сделать без доработок самого продукта, что у меня в итоге вышло (и что не вышло), какие ошибки допустил и как можно сделать лучше.

Проблематика

Все началось очень просто (и довольно давно - описанное происходило в начале этого года).

В продукте, где я работаю, есть административная панель, или же попросту админка. И этой админкой пользуются сотрудники компании при выполнении разного вида настроек. А поскольку необходимостей настроить что-то много, то и действий там производится немало. И все бы ничего, но не все настройки такие простые, не весь интерфейс полностью очевиден, да и вообще с большой силой приходит большая ответственность с большой гибкостью приходит бОльшая сложность настроек.

Соответственно у нас были довольно частыми вопросы вида "А как что-то здесь настроить?", "А что делает эта кнопка?", "А можно ли настройками сделать вот так?" и прочее. При всем этом у нас была и наша внутренняя база знаний в Confluence. Далее мы улучшили и ее, но на тот момент она была еще в процессе донаполнения и причесывания. Статьи были не конца упорядочены, но тем не менее, достаточно большое количество материалов уже имелось.

Из абзаца выше нам видно две ключевые проблемы:

  1. Админка продукта сейчас не позволяет интуитивно разобраться в ней и не нуждаться в дополнительной информации для работы;

  2. Пользователи не обращаются к статьям в базе знаний за информацией.

Можем ли быстро исправить первую из них? Кажется, нет. В целом мы развиваем и улучшаем нашу административную панель, однако это отдельный фронт работ, требующий ресурсов и времени.

А что насчет второй?
Очевидно, что, несмотря на наличие в базе знаний статей, ими пользовались далеко не всегда и не все. Я видел следующие причины тому:

  • Незнание того, что статья про это есть вообще;

  • Лень заходить в Confluence и искать нужную статью, потому что это долго/неудобно;

  • Нелюбовь к статьям вообще и нежелание их читать (допускаем, что и такое бывает).

И если с третьим пунктом мы здесь пока ничего поделать не можем, то первые два попробуем поправить. По тому, как создавать базу знаний, наполнять ее и делать так, чтобы ей реально пользовались, есть отдельные статьи, а сегодня я хочу поговорить об еще одном интересном шаге, который может повысить ее ценность и добавить удобства пользователям.

Видим проблему - ищем решение

Сформулируем образ результата: хотим сделать так, чтобы пользователи могли быстрее и эффективнее выполнять свои задачи в административной панели, используя существующие материалы в базе знаний.

Какую метрику успеха мы можем использовать? Мне показалось, что простой и подходящей будет количество вопросов по работе в админке в рабочих чатах, и снижение этого самого количества покажет, что мы сделали верный шаг.

Вводные:

  1. Нужно, чтобы пользователи при работе с нашей админкой могли быстрее разобраться, как выполнить в ней свою задачу;

  2. Имеем пул статей по возможностям и принципам работы админки в Confluence, эти статьи могут располагаться в разных местах и быть труднодоступными;

  3. Наше решение должно быть максимально простым и дешевым (помним, что это пока только наша гипотеза);

  4. Наше решение должно быть легкодоступным для пользователей разного уровня подготовки;

  5. Нам надо обойтись без доработки продукта вообще.

Откуда пятый пункт? Здесь надо сказать, что в компании я не выполняю роль разработчика, в продукт своими руками изменения не вношу (и не должен). В продуктовых планах подобного не было, поэтому решил сделать сам MVP, обкатать его, а уже в случае успеха предложить перенести идею более организованно и качественно в сам продукт.

По какому пути пойти

Для начала скажу, что для меня задача расширения функциональности "чужих" веб-приложений снаружи является не является новой. Обычно для этого я использую UserJS (пользовательские скрипты).

UserJS - это сторонний (не являющийся частью искомого сервиса) JavaScript-код, выполняющийся в браузере в контексте страницы (обычно до ее полной загрузки).

Внедряя свой скрипт на страницу мы можем самостоятельно доработать ее под свои нужды. В последние годы про это уже и не говорят, однако, если вспомнить, более десяти лет назад они были достаточно популярны, особенно в Опере.

В чем минусы использования UserJS в данном случае?

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

  • Для его использования нужно устанавливать какое-либо дополнительное расширение, добавлять туда скрипт - звучит сложновато, да и есть простор для того, чтобы совершить ошибку.

  • В случае каких-либо необработанных ошибок мы еще и будем гадить из JS-консоли в свой собственный Sentry и смущать честных фронтенд-разработчиков тем, что они видят ошибки в несуществующем для них коде.

Поэтому я подумал, а что еще используется для той же цели? Ответ пришел быстро - браузерные расширения.

  • Распакованное расширение можно легко поставить локально.

  • Расширение не будет взаимодействовать с содержимым страницы, а значит конфликты в эту сторону исключены.

  • Расширение так же легкодоступно и находится в пределах клика.

К тому же до этого мне не доводилось создавать браузерные расширения, и я решил, что будет интересным опытом разобраться в этом и познакомиться с ними поближе. В данном случае у нас появляется одно ограничение, которое мы принимаем: наше расширение будет заточено под один конкретный движок браузеров - Chromium.

А что нужно, чтобы сделать расширение?

Раз до этого расширения я не создавал, то первично решил разобраться, а что вообще для этого нужно.

Google сам говорит нам, что для основа для расширения - файл manifest.json. В нем мы задаем основной конфиг нашего расширения: название, описание, иконки, права доступа и прочее. Кроме этого, у нас есть базовые возможность использования логики, из них нас интересует popup - та логика, которую пользователь может использовать, взаимодействуя с расширением, - нажимая на него.

Для нас подходящим видится использование popup-части, поскольку участие пользователя мы ожидаем и взаимодействовать с DOM'ом страницы не хотим. Фоновые скрипты нам пока ни к чему, поэтому их опустим, и получим минимальную начальную файловую структуру вида:

manifest.json
popup.html
popus.js

Наполним наш manifest.json:

{
    "name": "Make admin panel great again",
    "description": "Ссылки на документацию по разделам в Confluence",
    "version": "1.0",
    "manifest_version": 3,
    "icons": {
        "16": "16.png",
        "48": "48.png",
        "128": "128.png"
    },
    "host_permissions": [
        "<all_urls>"
    ],
    "permissions": [
        "activeTab"
    ],
    "action": {
        "default_popup": "popup.html"
    }
}

По формату файла имеется исчерпывающая документация, поэтому подробно останавливаться на нем не буду.

Придумаем концепцию

С расширением начально разобрались, теперь надо продумать, как именно мы его будем делать.

Наша админка имеет разные разделы, и в зависимости от того, где сейчас пользователь находится, ему актуальны будут разные статьи. Попробуем изначально простой принцип: по клику на иконку будем показывать пользователю список статей, которые подходят для данного раздела.

Звучит здорово, но что со списком статей? В начальной версии, допустим, соберем его руками, но где его хранить? В самой первой итерации я сделал его локальным - то есть лежащим рядом с прочими файлами расширения, но в таком варианте мне сразу не понравилось, что у каждого пользователя будет своя копия, и мы не сможем централизованно управлять изменениями. Решение я выбрал простое - сделал конфиг удаленным, разместил его у себя на сервере, а в расширение он уже подтягивался "на лету", а со стандартными возможностями HTTP-кэширования работает довольно шустро.

Уточним детали

Сперва надо понять, как мы можем определить, в каком разделе сейчас находится пользователь, для того чтобы показать ему соответствующие статьи.

Все названия и URL-адреса изменены и являются вымышленными.

В случае нашего продукта это можно сделать на основе URL-адресов, а точнее их паттерна. Пусть вся наша админка находится по адресу /panel/.*.

Введем два понятия, они нам далее помогут:

  1. Главная секция - первичный раздел: company, global, admin;

  2. Секция - раздел с определенной функциональностью, идет после главной секции.

При этом у нас есть три типа главных секций:

  • настройки компаний - /panel/company/{company_id}/{section}/.*

  • общие настройки - /panel/company/global/{section}/.*

  • администрирование системы - /panel/{section}/.*

То же самое, но картинкой

Значит, на основе пути URL мы сможем первично определить тип главной секции. Что касается самих секций - каждая из них имеет свое название, при этом названия могут пересекаться между типами главных секций.

Изображение

Здесь надо отметить, что я в момент реализации не подумал развязать такую связь многие-ко-кногим и избавиться от нее. Поэтому в приведенном примере имеем ее.

Разделы идут после главной секции, а за ними может лежать еще какой-то контент. Так, например, пользователю, который находится по адресу /panel/company/1/users/.* мы хотим показать все статьи, которые связаны с работой с пользователями.

На момент разработки часть информации в текущей структуре базы знаний была не разделена: одна и та же статья могла быть актуальная для работы с секцией в разных местах, поэтому я решил начально определять статьи исходя из секции, а на основании главной секции уже определять, есть ли в ней эта секция или нет.

Изобразим небольшое дерево решений, которое позволяет более наглядно увидеть принцип.

От слов к делу

Разработку вел на ванильном JS.

В popup.js для начала работы мы должны получить URL текущей активной вкладки. Здесь важно отметить, что из расширения у нас просто так нет доступа к объекту window текущей вкладки, для работы с ней нужно использовать API, предоставляемый Chrome:

let [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (tab?.url === undefined) {
    return false;
}

Проверим полученный URL на предмет того, что мы действительно в нашей админке. Такая реализация обусловлена тем, что в моем случае, кроме продуктивного окружения, необходима была работа еще и в других, находящихся на поддоменах.

let url = new URL(tab.url);
let hostSplitted = url.hostname.split('.');
let pathnameSplitted = url.pathname.split('/');
if (pathnameSplitted[1] !== 'panel'
    || hostSplitted[hostSplitted.length - 1] !== "com"
    || hostSplitted[hostSplitted.length - 2] !== "site"
) {
    throw new Error('wrongURL');
}

Далее определим на основании URL главную секцию и секцию, помня наш разбор паттернов URL выше.

Код
let adminSectionsArray = ['companies', 'users',]; // array of all the admin sections availiable

...

function getMainSectionType(pathnameSplitted) {
    // for /panel/company/1/something
    if (pathnameSplitted[2] === 'company' && Number.isInteger(+pathnameSplitted[3])) {
        return 'company';
    }

    // for /panel/company/global/something
    else if (pathnameSplitted[2] === 'company' && pathnameSplitted[3] === 'global') {
        return 'global';
    }

    // for /panel/something
    else if (adminSectionsArray.includes(pathnameSplitted[2])) {
        return 'admin';
    }
    else return false;
}

function getSection(pathnameSplitted, mainSectionType) {
    switch (mainSectionType) {
        case 'company':
            return pathnameSplitted[4];
        case 'global':
            return pathnameSplitted[4];
        case 'admin':
            return pathnameSplitted[2];
        default:
            return false;
    }
}

Время работы с конфигом для хранения списка статей. Сам конфиг реализуем в виде JSON, содержащего на верхнем уровне ключи с названиями секций (совпадающие с тем, как секции называются в URL), а в качестве значения каждого из ключей - массив объектов со статьями. Конфиг пока наполняем вручную.

{
    "users": [
        {
            "title": "Управление пользователями", // название статьи для отображения
            "URL": "https://product.atlassian.net/wiki/spaces/test/pages/123456789" // ссылка на статью в базе знаний
        },
        ...
    ],
      ...
}

Из удаленного конфига получаем доступные секции и массив статей для каждой из них.

В зависимости от главного типа у нас есть жестко определенный набор секций, который пока оставим на уровне расширения, т.к. он меняется реже (далее можем вынести также).

Код
const CONFIG_URL = 'https://site.com/files/config.json'; // remote config URL

...

async function getSectionDocs(mainSectionType, section) {
    let configData = await getConfig(CONFIG_URL);
    let sectionVars = await configData.json();
    let companySections = {
        main: sectionVars?.main,
        users: sectionVars?.users,
        dictionaries: sectionVars?.dictionaries,
        analytics: sectionVars?.analytics,
    };
    let globalSections = {
        dictionaries: sectionVars?.dictionaries,
        notifications: sectionVars?.notifications,
    };
    let adminSections = {
        companies: sectionVars?.companies,
        users: sectionVars?.users,
    };
    let sectionDocs;
    switch (mainSectionType) {
        case 'company':
            sectionDocs = companySections[section];
            break;
        case 'global':
            sectionDocs = globalSections[section];
            break;
        case 'admin':
            sectionDocs = adminSections[section];
            break;
        default:
            sectionDocs = false;
    }
    return sectionDocs;
}

Для отображения не используем ничего хитрого: выводим все статьи в список со ссылками "target=_blank", еще добавил прелоадер на время получения данных конфига с другого ресурса. Для страницы чуть подкрутил стили, чтобы смотрелась поопрятнее и добавил максимальную высоту-ширину, чтобы поп-ап не разъехался на весь экран. Получилось вот так:

Отдельно пару слов про обработку ошибок: включил только самый минимальный набор, который был показан выше в дереве решений: при клике на расширение на другом сайте, если раздел еще не поддерживается, если не можем получить конфиг, если для раздела не добавлены статьи.

Отображение ошибок в окне расширения
Отображение ошибок в окне расширения

Ну вот и все, наше расширение готово и работает. Исходники приложены в конце статьи.

Что из всего этого вышло (и что не вышло)

Протестировав работу расширения на тот момент, я презентовал его коллегам, снабдив инструкцией по установке и использованию и предложил писать, если нужно будет обновить список статей в конфиге. Напомню, что идея была в том, чтобы проверить, будут ли люди вообще им пользоваться, насколько это зайдет и как повлияет на выбранную метрику.
Как обычно бывает, все порадовались и сказали: "О, круто". А что дальше?

Дальше мне было бы нужно суметь ответить хотя бы на вопросы: "Сколько пользователей попробовало решение в работе? Сколько продолжило использование?" (а еще было бы неплохо узнать, почему кто-то не попробовал или почему кто-то попробовал, но забросил). Когда мы работаем с маленькой выборкой пользователей, как было у меня, получить такие данные гораздо проще. Однако даже этого я не сделал. По факту я так и не узнал, пользовался ли кто-то расширением или нет. Я знал, что несколько людей как минимум установили его, и на этом все. Еще в самом начале у меня была мысль по использованию в расширении гугл-аналитики, но я, увы, подумал, что это необязательно и решил повременить.

Что там с нашей метрикой успеха? Фактически количество вопросов на дистанции не сократилось. их также продолжали задавать. Да и чтобы быть откровенным, я даже не попытался это численно замерить, а хотел опереться на "визуальное" изменение. Так что за этот пункт ставим тоже минус.

Далее со временем статьи в базе знаний стали сильно изменяться: менялись существующие, появлялись новые, менялась структура базы знаний. И, поскольку в течение этого времени ни один человек так ни разу и не обратился за обновлением конфига, я сделал предположение, что, скорее всего, реальных пользователей сейчас нет. Я решил удалить конфиг с сервера и проверить, заметит ли кто-то и придет ли спросить, что случилось и почему расширение не работает, но и в этот раз обращений не было.
В моем случае затея, конечно, похожа на провал, но настоящим провалом была, увы, моя работа по анализу результатов.

Даже для самого себя у меня нет рационального объяснения, почему я ничего не сделал и даже не попробовал подвести итоги, однако из данного примера постарался извлечь уроки (довольно банальные).

Как можно этого избежать и сделать лучше?

Запустив эксперимент (это касается в общем и других изменений) даже внутри компании, важно не забить забыть про мониторинг и анализ результатов. Поэтому полезно заранее придумать, как и на основании чего мы сможем определить успех/неудачу, по каким метрикам сможем что-то понять детальнее. В данном случае:

  1. Если выборка действительно маленькая, то достаточно запустить небольшой опрос и попросить пользователей пройти его. Опросом можно будет и померить количество, и сразу в нем, либо после, собрать качественную обратную связь.

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

Поскольку, как описано выше, в моем случае затея оказалась неуспешной, дальнейшего развития история не увидела. Здесь приведу свои мысли под дальнейшие итерации, либо другие пути применения.

Что можно сделать дальше или идеи под развитие
  1. Собирать конфиг руками - круто, но кто захочет поддерживать его актуальность на постоянной основе? Поэтому уместным развитием может быть автоматизация формирования конфига со статьями. На примере Confluence это может быть реализовано, например, так:

    1. Добавляем у статей какой-то признак, позволяющий однозначно связать статью с соответствующими разделами админки, например, метку(и).

    2. Периодически обращаемся к API Confluence, получаем нужные нам статьи, обрабатываем данные и формируем конфиг.

  2. Можно улучшить формат конфига: вынести туда связь главных секций и секций, заодно развязав их.

  3. Если говорить про расширение, то можно пересмотреть подход к работе: еще в бэкграунде проверять доступные статьи для текущей страницы и в бейдже (badge) показывать, например, число доступных статей, чтобы уже до клика было понятно, есть ли статьи для такого раздела или нет.

  4. Если ваш продукт внедряют партнеры-интеграторы, у них возникают подобные проблемы и при этом у вас есть база знаний для них, можно дать аналогичное решение им (на месте партнера я был бы очень рад такому, за неимением большего).

  5. Если продукт какой-то нишевый и интерфейс действительно такой сложный, что сами пользователи без инструкций не могут в нем работать, как один из простых шагов на пути к улучшению их пользовательского опыта может быть подобная практика.

Вместо заключения

Конечно, описанный пример как есть не годится в качестве серьезного продуктового решения, однако его можно использовать в качестве быстрой проверки перед разработкой чего-то более серьезного. Было бы очень интересно услышать мнение других: сталкивались ли с похожими проблемами у себя, и если да, как решали.

Исходники на Github

Комментарии (0)