Русский
Русский
English
Статистика
Реклама

Блог компании ispsystem

От библиотеки компонентов к дизайн-системе

23.06.2020 12:07:45 | Автор: admin


Разработка UI-компонентов одна из самых затратных задач при разработке фронтенда. Чтобы оптимизировать процессы, некоторые компании поручают её выделенной команде. Мы же решили пойти другим путём: сделать библиотеку, чтобы компоненты туда могли добавлять разработчики из разных команд. В этой статье расскажу, как наша библиотека организована.


Задачи библиотеки компонентов


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

Интерфейсы продуктов для управления инфраструктурой и сайтами от ISPsystem

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


  • Поддержка версионности
  • Простое добавление компонентов
  • Совмещение кода компонента с визуальными требованиями к нему.

Как мы эти требования реализовали, подробно расскажу дальше. Если коротко, то использовали Verdaccio, Stencil, импорт svg-файлов из Фигмы.


А давайте сделаем не одну, а две библиотеки


Несколько лет назад мы начали писать новый интерфейс для всей линейки продуктов ISPsystem. Переделку начали с биллинга, а когда встал вопрос о старте ещё одного проекта, задумались о переиспользовании написанного кода. Тогда и пришла мысль о библиотеке компонентов.


Один из наших разработчиков был покорен идеей веб-компонентов, в ту пору не так известных, и заразил нас. Идея и правда отличная: веб-компоненты можно использовать в любых веб-приложениях и на любых фреймворках. А Angular мы только начинали использовать и не были уверены (или душу грела мысль о возможности выбора), что новый проект будем делать именно на нем. Поэтому мы решили сделать две библиотеки: одну на веб-компонентах, вторую на Angular. Первую назвали ispui, вторую ngispui.


Задумали, что компоненты из ispui будем использовать где угодно, как на сайте, так и в проектах на разных фреймворках; а ngispui будет состоять из оберток для веб-компонентов, для удобства использования в ангуляр-проектах.



Светлая идея разбилась о реальность. Писать на чистых веб-компонентах не так удобно и быстро, как на Angular, поэтому мы их не разрабатывали. К тому же поддерживать три проекта (основной, плюс две библиотеки) было не очень удобно. Добавили в ispui пару веб-компонентов и несколько CSS-компонентов, и оставили. Сосредоточились на компонентах на ngispui.


Так основная работа стала вестись в библиотеке для Angular. За время существования мы добавили туда более 50 компонентов. И это действительно принесло плоды: новые проекты стартовали меньше, чем за год, тогда как первые мы делали почти два года.


Библиотека компонентов на Angular: как работает


Первая версия библиотеки публиковалась в Git. О её устройстве мырассказывалиещегодназад. Схема рабочая, но не идеальная: все версии были прибиты железно, поэтому воспользоваться преимуществами версионности было нельзя. Как следствие, возникали проблемы при сборке Angular, если у пакетов из библиотеки был один и тот же зависимый пакет, но разных версий.


В комментариях под той статьей нам посоветовали локальный npm-регистр пакетов verdaccio. Мы развернули его локально, с авторизацией через GitLab. Жить стало проще: можно пользоваться всеми фишками версионности. И скрипт публикации был переписан.


При публикации в verdaccio мы смотрим на бранч, из которого происходит публикация, если это не master, то в момент публикации к версии добавляем суффикс dev-текущая дата и время (например 1.0.0-dev12.12.2019). В package.json в репозитории версия остается без префикса. Поднимаем версию и пишем changelog руками. А когда бранч с новой версией компонента мёржится в master, в CI запускается публикация стабильной версии. И ничего в этот момент не нужно править в исходниках. Что удобно.


Требования к добавлению компонентов в библиотеку просты, это тесты + демо-страница. Для быстрого добавления компонентов в библиотеку у нас есть скрипт на plop, который генерирует шаблон демо-страницы и добавляет его в демо-приложение. Разработчику остаётся только сделать пример UI-компонента с описанием API.


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


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


Каждому компоненту своя версия


Библиотека сделана как одно приложение с демо, но каждый компонент публикуется отдельно. То есть каждый компонент это отдельный пакет со своими версиями.


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


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


  • собирать всю библиотеку, проходить все тесты;
  • если есть зависимые пакеты, добавлять их в демо-приложение;
  • прописывать зависимости компонента в package.json.

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




Собираем демо-версию компонента для быстрого ревью


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


Сборку демо-версии мы тоже автоматизировали: во время мерж-реквеста в CI запускается задача, которая собирает демо-приложение и загружает его в папку на веб-сервере с nginx. Папка создается по названию бранча, это позволяет хранить много демо-версий компонента одновременно. Разработчик получает готовую ссылку и добавляет к описанию мерж-реквеста.


Превращаем библиотеку в дизайн-систему


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


Дизайнеры сделали в Фигме страницы с описанием компонентов. Мы договорились об едином наименовании. И теперь при сборке библиотеки мы просто скачиваем страницы из Фигмы и вставляем их в виде SVG-файлов в демо страницы с компонентами. Решение не идеальное: на SVG-картинке текст не выделишь и поиском по странице не найдёшь, но прочитать можно.


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



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

Считаем, как часто используется компонент


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


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


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


  1. первый скрипт забирает информацию о зависимостях из package.json;
  2. второй скрипт парсит html на использование UI-компонентов и атрибутов этих компонентов;
  3. компонент-виджет отображает собранную статистику.


Чем больше пользователей у компонента, тем страшнее разработчику с ним хулиганить

Обновляем библиотеку на новую версию Angular


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


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


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


Возрождаем библиотеку веб-компонентов


Когда с базовой функциональностью новых продуктов закончили, пришло время добавлять возможности, для которых Angular было уже недостаточно. Встала новая задача плагины на фронтенде. Появилась идея делать их на веб-компонентах и встраивать в приложение Angular. Так получится не привязываться к версии Angular и не понадобится пересобирать плагины при обновлении приложения. Идея отличная, только вот для разработки плагинов требуются UI-компоненты.


Так мы вернулись к библиотеке веб-компонентов ispui. Чтобы не писать на чистых веб-компонентах, мы стали искать библиотеки и фреймворки, которые могли упростить нашу задачу. Angular Elements отпал сразу, из-за привязки к версии Angular, и сама реализация пока сыровата для использования в качестве библиотеки. LitElement довольно простой, но на тот момент не был совместим с Typescript, а Typescript мы любим. Наши ребята в свободное время написали свой класс для написания веб-компонентов AbstractElement, но ресурсов на его разработку и поддержку у нас нет, а на сдачу делать не вариант. А вот Stencil нас покорил: поддержка Typescript, TSX, ангуляр-подобный синтаксис с декораторами, готовая экосистема с тестами, генерацией документации и лоадер для использования в Angular и в других фреймворках.


Сейчас активно разрабатываем библиотеку ispui, учитывая предыдущий опыт и пожелания. Что уже сделали:


  • Монорепозиторий управляемый lerna. С ним можно независимо запускать сборку, тесты и публикацию отдельных пакетов.
  • Для каждого компонента своя демка в виде html-файла. При сборке она добавляется к демо приложению.
  • Для stencil-компонентов генерируется документация, которая вставляется в демо-приложение с описанием API компонента, используемых CSS-переменных.
  • Для автоматического поднятия версий и генерации чейнджлога используем написание коммитов по конвенции коммитов. Для этого удобно использовать утилиту git-cz

Заключение: как разрабатывать дизайн-систему, когда ресурсы ограничены


Проект в активной разработке, но подвести промежуточные итоги уже можно. Вот, на что стоит обращать внимание, если вам нужна дизайн-система, а ресурсы ограничены.


  • Используйте готовые инструменты по управлению инфраструктурой. Например, lerna.
  • Используйте автоматический листинг, настроенные tslint/eslint, stylelint, commitlint. Они позволяют валидировать проект автоматически.
  • Автоматизируйте рутинные процессы. Например, создание демо-страниц.
  • Используйте фреймворки и библиотеки с развитой инфраструктурой с настроенной тестовой средой, и автоматической генерации документации.
Подробнее..

Увидеть истинное лицо продукта и выжить. Данные о пользовательских переходах как повод написать пару новых сервисов

31.07.2020 10:05:30 | Автор: admin


В интернете сотни статей о том, какую пользу приносит анализ поведения клиентов. Чаще всего это касается сферы ритейла. От анализа продуктовых корзин, ABC и XYZ анализа до retention-маркетинга и персональных предложений. Различные методики используются уже десятилетиями, алгоритмы продуманы, код написан и отлажен бери и используй. В нашем случае возникла одна фундаментальная проблема мы в ISPsystem занимаемся разработкой ПО, а не ритейлом.
Меня зовут Денис и на данный момент я отвечаю за бэкенд аналитических систем в ISPsystem. И это история о том, как мы с моим коллегой Данилом ответственным за визуализацию данных попытались посмотреть на наши программные продукты сквозь призму этих знаний. Начнем, как обычно, с истории.


В начале было слово, и слово было Попробуем?


В тот момент я работал разработчиком в R&D отделе. Все началось с того, что здесь, на Хабре, Данил прочитал про Retentioneering инструмент для анализа переходов пользователей в приложениях. Идею его применения у нас я воспринял несколько скептически. В качестве примеров разработчики библиотеки приводили анализ приложений, где целевое действие было четко определено оформление заказа или иная вариация того, как заплатить компании-владельцу. У нас же продукты поставляются on-premise. То есть пользователь сначала покупает лицензию, и только после начинает свой путь в приложении. Да, у нас есть демо-версии. В них можно опробовать продукт, чтобы не брать кота в мешке.


Но большая часть наших продуктов ориентирована на рынок хостинга. Это крупные клиенты, и о возможностях продукта их консультирует отдел бизнес-девелоперов. Из этого же следует, что на момент покупки наши клиенты уже знают в решении каких задач им поможет наше ПО. Их маршруты в приложении должны совпадать с заложенным в продукт CJM, а UX-решения помогут с них не сбиваться. Спойлер: так происходит не всегда. Знакомство с библиотекой было отложено но ненадолго.


Все изменилось с релизом нашего стартапа Cartbee платформы для создания интернет-магазина из аккаунта в Instagram. В этом приложении пользователю предоставлялся двухнедельный период на бесплатное использование всей функциональности. После нужно было принять решение, оформлять ли подписку. И это как нельзя хорошо ложилось в концепцию маршрут-целевое действие. Было решено: пробуем!


Первые результаты или откуда брать идеи


Мы с командой разработки буквально за день подключили продукт к системе сбора событий. Скажу сразу, что в ISPsystem используется своя система сбора событий о посещении страниц, однако ничто не мешает применять для тех же целей Яндекс.Метрику, которая позволяет бесплатно выгружать сырые данные. Были изучены примеры использования библиотеки, и спустя неделю сбора данных мы получили граф переходов.
Граф переходов. Основная функциональность, остальные переходы убраны для наглядности
Граф переходов. Основная функциональность, остальные переходы убраны для наглядности


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


  • Вместо крупного CJM, который охватывает с десяток сущностей, активно используются всего две. Необходимо дополнительно направлять пользователей в нужные нам места при помощи UX-решений.
  • На некоторых страницах, задуманных UX-проектировщиками как сквозные, люди проводят неоправданно много времени. Нужно выяснять, что является стоп-элементами на конкретной странице и корректировать это.
  • После 10 переходов 20% людей начинали уставать и бросать сессию в приложении. И это с учетом того, что у нас в приложении было целых 5 страниц онбординга! Нужно выявлять страницы, на которых пользователи регулярно бросают сессии, и сокращать путь до них. Еще лучше: выявлять любые регулярные маршруты и позволять совершать быстрый переход из страницы-источника в страницу-назначение.
    Что-то общее с ABC-анализом и анализом брошенных корзин, не находите?

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


О разочарованиях и воодушевлениях


Разочарование #1
Это был конец рабочего дня, конец месяца и конец года одновременно 27 декабря. Данные были накоплены, запросы написаны. Оставались секунды до того, как все обработается, и мы сможем взглянуть на результат своих трудов, чтобы узнать, с чего начнётся следующий рабочий год. R&D отдел, продакт-менеджер, UX-дизайнеры, тимлид, разработчики собрались перед монитором, чтобы увидеть как выглядят пути пользователей в их продукте, но мы увидели это:
Граф переходов, построенный библиотекой Retentioneering
Граф переходов, построенный библиотекой Retentioneering


Воодушевление #1
Сильно-связный, десятки сущностей, неочевидные сценарии. Понятно было лишь только то, что новый рабочий год начнется не с анализа, а с изобретения способа упростить работу с таким графом. Но меня не покидало чувство, что все намного проще, чем кажется. И после пятнадцати минут изучения исходников Retentioneering удалось экспортировать построенный граф в формат dot. Это позволило выгрузить граф в другой инструмент Gephi. А уже там раздолье для анализа графов: укладки, фильтры, статистики только и делай, что настраивай в интерфейсе нужные параметры. С этой мыслью мы ушли на новогодние выходные.


Разочарование #2
После выхода на работу оказалось, что пока все отдыхали, наши клиенты изучали продукт. Да так усердно, что в хранилище появились события, которых раньше не было. Это означало то, что нужно актуализировать запросы.


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


/host/item/24/ip(modal:modal/host/item/ip/create)


значит, что на странице IP-адреса пользователь добавлял IP-адрес. И здесь видны сразу две проблемы:


  • В URL есть какой-то path parameter ID виртуальной машины. Нужно его исключать.
  • В URL есть идентификатор модального окна. Нужно как-то распаковывать такие URL.
    Другая проблема заключалась в том, что в тех самых размеченных нами событиях были параметры. Например, попасть на страницу с информацией о виртуальной машине из списка можно было пятью различными способами. Соответственно, событие отправлялось одно, но с параметром, которые указывал, каким из способов пользователь осуществил переход. Таких событий было множество, и все параметры разные. А у нас вся логика извлечения данных на диалекте SQL для Clickhouse. Запросы на 150-200 строк начинали казаться чем-то привычным. Проблемы окружали нас.

Воодушевление #2
Одним ранним утром Данил, грустно скроля запрос вторую минуту, предложил мне: А давай пайплайны обработки данных напишем? Мы подумали и решили, что раз уж делать, то нечто вроде ETL. Чтобы и фильтровало сразу, и из других источников нужные данные подтягивало. Так родился наш первый аналитический сервис с полноценным бэкендом. Он реализует пять основных стадий обработки данных:


  1. Выгрузка событий из хранилища сырых данных и подготовка их к обработке.
  2. Уточнение распаковка тех самых идентификаторов модальных окон, параметров событий и прочих уточняющих событие деталей.
  3. Обогащение (от слова стать богатым) дополнение событий данными из сторонних источников. На тот момент сюда входила только наша биллинговая система BILLmanager.
  4. Фильтрация процесс отсеивания событий, которые искажают результаты анализа (события со внутренних стендов, выбросы и т.д.).
  5. Выгрузка полученных событий в хранилище, которое мы назвали чистыми данными.
    Теперь поддерживать актуальность можно было добавляя правила обработки события или даже группы похожих событий. Например, c того момента мы ни разу не актуализировали распаковку URL. Хотя, за это время добавилось несколько новых вариаций URL. Они соответствуют уже заложенным в сервис правилам и корректно обрабатываются.

Разочарование #3
Как только мы приступили к анализу, мы осознали, почему граф был настолько связным. Дело в том, что практически каждая N-грамма содержала в себе переходы, которые невозможно осуществить через интерфейс.


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


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


Воодушевление #3
Коллеги из фронтед-разработки научили систему сбора событий различать вкладки. Можно было приступать к анализу. И мы приступили. Как и ожидалось, CJM не совпадали с реальными путями: пользователи проводили много времени на страницах-каталогах, бросали сессии и вкладки в самых неожиданных местах. При помощи анализа переходов мы смогли найти проблемы в некоторых билдах Mozilla. В них, из-за особенностей реализации, пропадали элементы навигации или отображались полупустые страницы, которые должны быть доступны только администратору. Страница открывалась, но контент с бэкенда не приходил. Подсчет переходов позволял оценить, какие фичи реально используются. Цепочки давали возможность понять, как пользователь получил ту или иную ошибку. Данные позволяли проводить тестирование на основе поведения пользователей. Это был успех, затея была не напрасной.


Автоматизация аналитики


На одной из демонстраций результатов мы показывали, как применяется Gephi для анализа графов. В этом инструменте данные о переходах можно вывести в таблицу. И руководитель отдела UX сказал одну очень важную мысль, которая повлияла на развитие всего направления аналитики поведения в компании: А давайте сделаем так же, но в Tableau и с фильтрами так будет удобнее.


Тогда я подумал: а почему бы и нет, Retentioneering хранит все данные в pandas.DataFrame структуре. А это уже, по большому счету, таблица. Так появился еще один сервис: Data Provider. Он не только делал из графа таблицу, но и рассчитывал, насколько страница и привязанная к ней функциональность пользуются популярностью, как влияет на удержание пользователей, насколько пользователи задерживаются на ней, с каких страниц чаще всего уходят пользователи. А использование визуализации в Tableau настолько сократило затраты на изучение графа, что время итерации анализа поведения в продукте сократилось практически вдвое.


О том, как данная визуализация применяется и какие выводы позволяет сделать расскажет Данил.


Больше таблиц богу таблиц!


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


Рисовать ориентированный граф в Tableau не очень-то хотелось. Да и в случае успеха выигрыш, по сравнению с Gephi, представлялся неочевидным. Нужно было что-то гораздо проще и доступнее. Таблица! Ведь граф легко представить в виде строк таблицы, где каждая строка ребро вида источник-назначение. Более того, такая таблица у нас уже была заботливо подготовлена средствами Retentioneering и Data Provider. Дело оставалось за малым: вывести таблицу в Tableau и пошарить отчет.
К слову о том, как все любят таблицы
К слову о том, как все любят таблицы


Однако здесь мы столкнулись еще с одной проблемой. Что делать с источником данных? Подключить pandas.DataFrame было нельзя, такого коннектора у Tableau нет. Поднимать отдельную базу для хранения графа казалось слишком радикальным решением с туманными перспективами. А варианты локальных выгрузок не подходили из-за необходимости постоянных ручных операций. Мы полистали список доступных коннекторов, и взгляд упал на пункт Web Data Connector, который сиротливо ютился в самом низу.


У Tableau богатый выбор коннекторов. Нашли и тот, который решил нашу задачу
У Tableau богатый выбор коннекторов. Нашли и тот, который решил нашу задачу


Что за зверь? Несколько новых открытых вкладок в браузере и стало понятно, что этот коннектор позволяет получать данные при обращении по URL. Backend для расчета самих данных уже был почти готов, оставалось подружить его с WDC. Несколько дней Денис изучал документацию и воевал с механизмами Tableau, а затем скинул мне ссылку, которую я вставил в окно подключения.


Форма подключения к нашему WDC
Форма подключения к нашему WDC. Денис сделал свой фронт и позаботился о безопасности


Через пару минут ожидания (данные рассчитываются динамически при запросе) появилась таблица:


Так выглядит сырой массив данных в интерфейсе Tableau
Так выглядит сырой массив данных в интерфейсе Tableau


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


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


Как правило, при анализе данных человек хочет получить ответы на вопросы. Отлично. С них и начнем.


  • Какие переходы самые частые?
  • Куда уходят с конкретных страниц?
  • Сколько в среднем проводят на этой странице до того, как ушли?
  • Как часто делают переход из A в B?
  • А на каких страницах заканчивается сессия?

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


Что же у нас получилось?


Куда чаще всего расходятся с дашборда?


image
Фрагмент нашего отчета. После дашборда все уходили либо на список ВМ либо на список нод


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


Откуда приходят в список кластеров?


Список страниц, с которых пользователи попадали в страницу с кластерами
Фильтры в отчетах работают в обе стороны: можно узнать откуда ушли, а можно куда шли


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


Спросим кое-что посложнее.


Откуда пользователи чаще всего бросают сессию?


Пользователи VMmanager часто работают в отдельных вкладках
Пользователи VMmanager часто работают в отдельных вкладках


Для этого нам нужен отчет данные которого агрегированы по источникам переходов. А в качестве назначений были взяты так называемые breakepoints события, которые послужили концом цепочки переходов.


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


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


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


Таблица с переходами пользователей
Таблица получилась довольно объемной, но как мы надеемся, не перестала быть читаемой


Отдельно стоит сказать про обучение наших внутренних клиентов: продуктологов и UX-проектировщиков. Для них специально были подготовлены мануалы с примерами анализа и подсказками при работе с фильтрами. Ссылки на мануалы мы вставили прямо в страницы отчетов.


Мануал по использованию отчета
Мануал мы сделали просто в виде презентации в Google Docs. Средства Tableau позволяют отображать веб-страницы прямо внутри книги с отчетами.


Вместо послесловия


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


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

Подробнее..

Ленивая подгрузка переводов с Angular

22.07.2020 12:20:51 | Автор: admin

image


Если вы когда-нибудь участвовали в разработке крупного angular-проекта с поддержкой локализации, то эта статья для вас. Если же нет, то возможно, вам будет интересно, как мы решили проблему скачивания больших файлов с переводами при старте приложения: в нашем случае ~2300 строк и ~200 Кб для каждого языка.


Немного контекста


Всем привет! Я Frontend-разработчик компании ISPsystem в команде VMmanager.


Итак, мы имеем крупный frontend-проект. Под капотом angular 9-й версии на момент написания статьи. Поддержка локализации осуществляется библиотекой ngx-translate. Сами переводы в проекте лежат в json-файлах. Для взаимодействия с переводчиками используется сервис POEditor.


Что не так с большими переводами?


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


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


Что делать с этими проблемами?


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


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


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


На основании перечисленных хотелок получается примерно такая структура файлов:


<projectRoot>/i18n/  ru.json  en.json  HOME/    ru.json    en.json  HOME.COMMON/    ru.json    en.json  ADMIN/    ru.json    en.json

Тут файлы json в корне это основные файлы, они будут скачиваться всегда (нужный, в зависимости от выбранного языка). Файлы в HOME переводы необходимые только обычному пользователю. ADMIN файлы необходимые только администратору. HOME.COMMON файлы необходимые и пользователю, и администратору.


Каждый json-файл внутри должен иметь структуру, соответствующую его namespace:


  • корневые файлы просто содержат {...};
  • файлы внутри ADMIN содержат { "ADMIN": {...} };
  • файлы внутри HOME.COMMON содержат { "HOME": { "COMMON": {...} } } ;
  • и т.д.

Пока что это можно воспринимать как мою причуду, далее это будет обоснованно.


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


ngx-translate из коробки этого всего не умеет, но предоставляет достаточный функционал, чтобы это можно было реализовать своими силами:


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

Реализация


Скачиватель переводов: TranslateLoader


Чтобы сделать свой скачиватель переводов, необходимо создать класс реализующий один метод abstract getTranslation(lang: string): Observable<any>. Для семантики можно унаследовать его от абстрактного класса TranslateLoader (импортируется из ngx-translate), который мы далее будем использовать для провайдинга.


Так как наш класс будет не просто скачивать переводы, но и как-то должен их объединять в один объект, кода будет чуть больше, чем один метод:


export class MyTranslationLoader extends TranslateLoader implements OnDestroy {  /** Глобальный кэш с флагами скачанных файлов переводов (чтобы не качать их повторно, для разных модулей) */  private static TRANSLATES_LOADED: { [lang: string]: { [scope: string]: boolean } } = {};  /** Сортируем ключи по возрастанию длины (маленькие куски будут вмердживаться в большие) */  private sortedScopes = typeof this.scopes === 'string' ? [this.scopes] : this.scopes.slice().sort((a, b) => a.length - b.length);  private getURL(lang: string scope: string): string {    // эта строка будет зависеть от того, куда и как вы кладете файлы переводов    // в нашем случае они лежат в корне проекта в директории i18n    return `i18n/${scope ? scope + '/' : ''}${lang}.json`;  }  /** Скачиваем переводы и запоминаем, что мы их скачали */  private loadScope(lang: string, scope: string): Observable<object> {    return this.httpClient.get(this.getURL(lang, scope)).pipe(      tap(() => {        if (!MyTranslationLoader.TRANSLATES_LOADED[lang]) {          MyTranslationLoader.TRANSLATES_LOADED[lang] = {};        }        MyTranslationLoader.TRANSLATES_LOADED[lang][scope] = true;      })    );  }  /**    * Все скачанные переводы необходимо объединить в один объект    * т.к. мы знаем, что файлы переводов не имеют пересечений по ключам,    * можно вместо сложной логики глубокого мерджа просто наложить объекты друг на друга,   * но надо делать это в правильном порядке, именно для этого мы выше отсортировали наши scope по длине,   * чтобы наложить HOME.COMMON на HOME, а не наоборот   */  private merge(scope: string, source: object, target: object): object {    // обрабатываем пустую строку для root модуля    if (!scope) {      return { ...target };    }    const parts = scope.split('.');    const scopeKey = parts.pop();    const result = { ...source };    // рекурсивно получаем ссылку на объект, в который необходимо добавить часть переводов    const sourceObj = parts.reduce(      (acc, key) => (acc[key] = typeof acc[key] === 'object' ? { ...acc[key] } : {}),      result    );        // также рекурсивно достаем нужную часть переводов и присваиваем    sourceObj[scopeKey] = parts.reduce((res, key) => res[key] || {}, target)?.[scopeKey] || {};    return result;  }  constructor(private httpClient: HttpClient, private scopes: string | string[]) {    super();  }  ngOnDestroy(): void {    // сбрасываем кэш, чтобы при hot reaload переводы перекачались    MyTranslationLoader.TRANSLATES_LOADED = {};  }  getTranslation(lang: string): Observable<object> {    // берем только еще не скачанные scope    const loadScopes = this.sortedScopes.filter(s => !MyTranslationLoader.TRANSLATES_LOADED?.[lang]?.[s]);    if (!loadScopes.length) {      return of({});    }    // скачиваем все и сливаем в один объект    return zip(...loadScopes.map(s => this.loadScope(lang, s))).pipe(      map(translates => translates.reduce((acc, t, i) => this.merge(loadScopes[i], acc, t), {}))    );  }}

Как можно заметить, scope здесь используется и как часть url для скачивания файла, и как ключ для доступа к необходимой части json, именно поэтому директория и структура в файле должны совпадать.


Как это использовать, описано чуть дальше.


Докачиватель переводов: MissingTranslationHandler


Чтобы реализовать эту логику, необходимо сделать класс, имеющий метод handle. Проще всего унаследовать класс от MissingTranslationHandler, который импортируется из ngx-translate.
Описание метода в репозитории ngx-translate выглядит так:


export declare abstract class MissingTranslationHandler {  /**   * A function that handles missing translations.   *   * @param params context for resolving a missing translation   * @returns a value or an observable   * If it returns a value, then this value is used.   * If it return an observable, the value returned by this observable will be used (except if the method was "instant").   * If it doesn't return then the key will be used as a value   */  abstract handle(params: MissingTranslationHandlerParams): any;}

Нас интересует как раз второй вариант развития событий: вернуть Observable на скачивание нужного куска переводов.


export class MyMissingTranslationHandler extends MissingTranslationHandler {  // кэшируем Observable с переводом, т.к. при входе на страницу, для которой еще нет переводов,  // каждая translate pipe вызовет метод handle  private translatesLoading: { [lang: string]: Observable<object> } = {};  handle(params: MissingTranslationHandlerParams) {    const service = params.translateService;    const lang = service.currentLang || service.defaultLang;    if (!this.translatesLoading[lang]) {      // вызываем загрузку переводов через loader (тот самый, который реализован выше)      this.translatesLoading[lang] = service.currentLoader.getTranslation(lang).pipe(        // добавляем переводы в общее хранилище ngx-translate        // флаг true говорит о том, что объекты необходимо смерджить        tap(t => service.setTranslation(lang, t, true)),        map(() => service.translations[lang]),        shareReplay(1),        take(1)      );    }    return this.translatesLoading[lang].pipe(      // вытаскиваем необходимый перевод по ключу и вставляем в него параметры      map(t => service.parser.interpolate(service.parser.getValue(t, params.key), params.interpolateParams)),      // при ошибке эмулируем стандартное поведение, когда нет перевода  возвращаем ключ      catchError(() => of(params.key))    );  }}

Мы в проекте всегда используем только строковые ключи (HOME.TITLE), но ngx-translate также поддерживает ключи в виде массива строк (['HOME', 'TITLE']). Если вы этим пользуетесь, то в обработке catchError необходимо добавить проверку вроде такой of(typeof params.key === 'string' ? params.key : params.key.join('.')).


Используем все вышеописанное


Чтобы использовать наши классы, необходимо указать их при импорте TranslateModule:


export function loaderFactory(scopes: string | string[]): (http: HttpClient) => TranslateLoader {  return (http: HttpClient) => new MyTranslationLoader(http, scopes);}// ...// app.module.tsTranslateModule.forRoot({  useDefaultLang: false,  loader: {    provide: TranslateLoader,    useFactory: loaderFactory(''),    deps: [HttpClient],  },})// home.module.tsTranslateModule.forChild({  useDefaultLang: false,  extend: true,  loader: {    provide: TranslateLoader,    useFactory: loaderFactory(['HOME', 'HOME.COMMON']),    deps: [HttpClient],  },  missingTranslationHandler: {    provide: MissingTranslationHandler,    useClass: MyMissingTranslationHandler,  },})// admin.module.tsTranslateModule.forChild({  useDefaultLang: false,  extend: true,  loader: {    provide: TranslateLoader,    useFactory: loaderFactory(['ADMIN', 'HOME.COMMON']),    deps: [HttpClient],  },  missingTranslationHandler: {/*...*/},})

Флаг useDefaultLang: false необходим для корректной работы missingTranslationHandler.
Флаг extend: true (добавлен в версии ngx-translate@12.0.0) необходим, чтобы дочерние модули работали с переводами главного модуля.


Также, чтобы избавиться от копирования подобных импортов для каждого модуля с отдельными переводами, можно сделать пару утилит:


export function translateConfig(scopes: string | string[]): TranslateModuleConfig {  return {    useDefaultLang: false,    loader: {      provide: TranslateLoader,      useFactory: httpLoaderFactory(scopes),      deps: [HttpClient],    },  };}@NgModule()export class MyTranslateModule {  static forRoot(scopes: string | string[] = [], config?: TranslateModuleConfig): ModuleWithProviders<TranslateModule> {    return TranslateModule.forRoot({      ...translateConfig([''].concat(scopes)),      ...config,    });  }  static forChild(scopes: string | string[], config?: TranslateModuleConfig): ModuleWithProviders<TranslateModule> {    return TranslateModule.forChild({      ...translateConfig(scopes),      extend: true,      missingTranslationHandler: {        provide: MissingTranslationHandler,        useClass: MyMissingTranslationHandler,      },      ...config,    });  }}

Такие импорты должны быть только в корневых модулях отдельных частей приложения, далее (чтобы использовать translate пайпу или директиву) надо просто импортировать TranslateModule.


В данный момент (на версии ngx-translate@12.1.2) можно заметить, что при переключении языка, пока происходит скачивание переводов, пайпа translate выводит [object Object]. Это ошибка внутри самой пайпы.


POEditor


Как я упоминал ранее, мы выгружаем наши переводы в сервис POEditor, чтобы там с ними мог работать переводчик. Для этого в сервисе есть соответствующее API:



Оба этих хэндлера работают с полными файлами переводов, но у нас все переводы лежат в разных файлах. Значит, перед отправкой надо все переводы склеить в один файл, а при скачивании разложить по файлам.


Эту логику мы реализовали в python3 скрипте.
В общих чертах он использует тот же принцип объединения переводов, что и в MyTranslateLoader. Разделение происходит по той же схеме, только из большого файла, мы вычитаем куски.


В скрипте реализовано несколько команд:


  • split принимает на вход файл и директорию, в которой у вас подготовлена структура для переводов, и раскладывает переводы согласно этой структуре (в нашем примере это директория i18n);
  • join делает обратное действие: принимает на вход путь до директории с переводами и кладет склеенный json либо в stdout, либо в указанный файл;
  • download скачивает переводы из POEditor, затем либо раскладывает их по файлам в переданной директории, либо кладет в один файл, переданный в аргументы;
  • upload соответственно загружает в POEditor переводы либо из переданной директории, либо из переданного файла;
  • hash считает md5 сумму всех переводов из переданной директории. Пригодится в том случае, если вы подмешиваете хеш в параметры для скачивания переводов, чтобы они не кэшировались в браузере при изменении.

Также там используется пакет argparse, который позволяет удобно работать с аргументами и генерирует --help команду.


Исходник скрипта в рамках статьи рассматривать не будем, потому что он достаточно большой, но вы можете найти его в репозитории по ссылке ниже.
Также в репозитории представлен демо проект, в котором реализовано все, что описано в статье. А также этот проект залит на платформу stackblitz, где можно все потыкать.


GitHub Репозиторий
Демо на Stackblitz


К чему мы пришли


Сейчас такой подход уже используется в VMmanager 6. Конечно, все наши переводы за один раз мы не стали разделять, потому что их достаточно много. Постепенно отделяем их от основного файла, а новый функционал стараемся реализовывать уже с разделением переводов.


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


А как вы решаете проблему больших файлов локализации? Или почему не стали этого делать?

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru