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

Frontendconf

Эффективное тестирование верстки

24.08.2020 12:15:17 | Автор: admin
Тестировать полезно. Тесты позволяют в автоматическом режиме безопасно рефакторить код и гарантируют его работу. Тесты это живая документация: если информация в Wiki или в Confluence может устареть, то тесты всегда актуальны. Также многие крутые практики связаны с тестированием. Например, самотестирующийся код или разработка через тестирование (TDD), когда тесты пишутся перед кодом, а некоторые практики DevOps и Extreme Programming применимы только в условиях хорошего покрытия проекта тестами.



Но написать простые тесты, которые будут помогать в написании кода и не срывать дедлайны, задача сложная. Она становится ещё сложнее, если учесть, что нам приходится тестировать вёрстку. Это не два JSON сравнить: здесь не работают простые подходы вызову функцию, проверю результат тестирование UI сложнее. Как эффективно и правильно тестировать верстку и писать для неё тесты, чтобы они были полезны, а дедлайны не горели, расскажет Максим Соснов (crazymax11), ведущий разработчик в СКБ Контур.



Пирамида тестирования


Если театр начинается с вешалки, то тестирование начинается с пирамиды тестирования.



Пирамида это концепция, которая говорит, что в проекте есть 3 вида тестирования:

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

Примечание. В классической пирамиде тестирования Майка Кона эти уровни называются Unit, Service и UI. Но в современном варианте чаще упоминаются Unit, интеграционные и E2E.

Чем выше тесты на пирамиде, тем они ценнее они дают больше уверенности в том, что приложение работает так, как ожидается. Но при этом их дороже писать и поддерживать. Чем ближе тесты к основанию пирамиды, тем быстрее эти тесты написать и тем быстрее они исполняются.

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

Применим пирамиду тестирования


Посмотрим, как это работает проверим пирамиду на небольшой функциональности, например, на простом поиске. У нас есть input для ввода пользовательского запроса и кнопка Найти, которая отправляет запрос на бекенд.



Для реализации подобного функционала поделим приложение на стандартные, для фронтенд-архитектуры, слои:

  • Первый слой Component, реализованный на одном из популярных фреймворков. Его задача рендерить вёрстку.
  • Component подключен к Store, который реализует бизнес-логику приложения.
  • Store, в свою очередь, использует Service, который инкапсулирует в себе знания о том, как обращаться в API поиска.


Component, Store и Service и есть наши модули минимальные Unitы.

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

Unit-тесты


Чтобы покрыть наш сценарий юнит-тестами напишем пять тестов.

  1. Component умеет рендерить input и кнопку. При этом не будем брать настоящий браузер эмулируем.
  2. При клике на кнопку вызывается правильный callback. Также не будем брать настоящий браузер, а только эмулируем его.
  3. Наш Store обрабатывает callback, вызывает сервис и обновляет свое внутреннее состояние.
  4. Service правильно обращается к API и правильно отдает данные, которые получает от API.
  5. Component может отрендерить результаты поиска.

Что можно сказать о получившихся тестах?

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

Тесты позволяют безопасно рефакторить только внутри модуля. Если поменять публичное API, например, Service, то также придется менять тесты на Store.

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

Интеграционные тесты


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

Рефакторинг почти свободен. Если захотим как-то перекомпозировать наш код, например, по другому поделить ответственность в коде Store, это можно сделать не поменяв ни строчки теста.

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

E2E-тесты


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

  • С E2E-тестами достаточно одного теста. Мы также проверим реальное взаимодействие между модулями и будем уверены в том, что они работают вместе.
  • Рефакторинг полностью свободен. Нам ничего не помешает, например, поменять Vue на React, а React на Vue.
  • E2E-тесты эмулируют HTTP-взаимодействие с API нельзя быть до конца уверенным, что мы правильно интегрированы с API.

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

Сравнение




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

Классическая пирамида тестирования работает, но не всегда. Её нужно правильно адаптировать к контексту. Также у пирамиды есть проблема с терминологией. Разные люди по-разному понимают термины Unit и E2E. Это часто приводит к холиварам в онлайн-чатах и в оффлайн обсуждениях: у кого-то тесты недостаточно E2E, или Unitы не Unitы.

Большинство классических подходов отлично подходятдля бэкенд-разработки, но для фронтенда их надо адаптировать. Но как?

Пирамида фронтенд-тестирования


Для фронтенда Kent C. Dodds вывел отдельную пирамиду тестирования, которую назвал Трофей тестирования.



Вместо пирамиды у нас есть трофей.

  • Основа трофея это множество статических проверок: ESLint, Prettier, TypeScript.
  • К статическим проверкам мы пишем много интеграционных тестов.
  • Там, где мы не можем писать интеграционные тесты, допустимы Unit-тесты.
  • E2E тесты следует писать для критичных и важных сценариев.

Универсальная формула тестирования


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


Универсальная формула тестирования.

Но у этой формулы есть одна большая проблема субъективность.

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

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

Звучит слишком по-философски. Давайте разберемся, как это применять.

Инструменты во фронтенде


Давайте посмотрим, какие инструменты для тестирования есть во фронтенде.


На картинке представлены не все инструменты: только популярные и те, у которых есть логотипы.

И столько же подходов к тестированию.



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

Скриншот-тесты через Storybook


Storybook позволяет разрабатывать компоненты в изолированной песочнице и поставлять им разные входные данные.



Добавим Storybook в наш проект с компонентом поиска напишем простую команду:

npx -p @storybook/cli sb init

Команда сама добавит Storybook в проект, сама настроит все конфиги и Storybook будет готов к запуску. Запускаем:

npm run storybook

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

Для нашего компонента поиска целесообразно описать три истории:

  • как компонент работает в начале показывается кнопка input;
  • как компонент грузит данные показывается loader;
  • как компонент показывает поисковые результаты.



Теперь, если запустить Storybook, увидим следующую картину.



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

Истории в Storybook:

  • Можно писать на любом фреймворке. Storybook поддерживает практически все популярные фреймворки: Angular, React, Vue. Можно писать истории на чистом HTML и CSS.
  • Storybook гарантирует, что компоненты всегда запускаются в изолированной песочнице и не могут афектить друг на друга.
  • В Storybook очень просто описать все возможные состояния компонента.

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

Получается что истории в Storybook это идеальная основа для скриншот-тестов. Существует множество решений для автоматизации использования историй как скриншот-тестов (а также есть возможность написать свой велосипед, но не делайте так это намного сложнее, чем кажется). Из бесплатных вариантов рассмотрим два инструмента, с которыми у меня положительный опыт использования Loki.js и Creevey.

Loki.js


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



Loki.js:

  • Быстрый, относительно своих функциональных аналогов.
  • Нативно интегрируется с Docker вам будет легче настроить его в CI.
  • Необязательно поднимать отдельный веб-сервис Storybook. Loki.js умеет работать со Storybook, собранным в статику.

Интеграция. Интегрировать скриншот-тесты Loki.js в проект можно за пару минут.

Открываем консоль и ставим Loki.js как зависимость:

npm i -D loki

Инициализируем:

npx loki init

Loki.js сам интегрируется в проект и сам все настроит для своей работы.
После этого запускаем Storybook.

npm run storybook

Запустим Loki.js и посмотрим, как он делает скриншот-тесты. Открываем вторую консоль при открытом Storybook и пишем:

npx loki test



Loki.js с помощью puppeteer запустит Chrome в headless-режиме, пройдет по всем историям запущенного Storybook и сохранит скриншоты на файловую систему в папку .loki.

Работа с Loki.js. Попробуем что-то изменить в нашем компоненте, например, уберем Material UI кнопку и поставим нативную HTML-кнопку. Снова запустим.

npx loki test



Loki.js сообщает в консоль, что компонент изменился. Чтобы посмотреть изменения заходим в папку .loki/difference, куда Loki.js сохраняет удобные для просмотра различия между эталонным скриншотом и текущим.



Loki.js отмечает розовым разницу между двумя скриншотами. Не идеально, но помогает увидеть отличия.

Минус Loki.js. Он работает только в Chrome. Мы его быстро настроили, он хорошо работает в Docker, делает скриншоты, но, к сожалению, только в Chrome. Поэтому если вам нужно поддерживать IE11, попробуйте Creevey.

Creevey


Creevey это молодой, но интересный проект, который разрабатывает Kiichiro. Проект находится в стадии активной разработки и его API может меняться.

Creevey использует Selenium, поэтому поддерживает практически все браузеры, в том числе и мобильные. Но, как следствие, для больших проектов придется поднять Selenium Grid. Кроме того, что Creevey делает скриншоты, он позволяет писать тесты прямо в Storybook рядом с историями.

Как работает. Добавим истории немного метаинформации для Creevey.

export const Simple^ CSFStory<JSX.Element> = () => <MyComponent />;Simple.story = {parameters: {creevey: {captureElement: #root,tests: {async click() {await this.browser.actions().click(this.captureElement).perform();await this.expect(await this.takeScreenshot()).to.matchImage(clicked component);;},},},},}

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

Как это выглядит в реальной жизни? Запускаем Creevey (и Storybook заодно). Интерфейс (похожий на Storybook) позволяет выбрать компоненты для тестирования, браузеры и тест-кейсы. Нажимаем кнопку СТАРТ: Creevey быстро делает скриншоты всех выбранных тест-кейсов и показывает их в своем интерфейсе.

Creevey показывает изменения. Например, если мы поменяли текст истории, Creevey покажет слева компонент до, справа после изменений, а посередине сами изменения.

Как это работает
Ссылка на GIF, потому что не влезло.

Изменения удобнее изучать, чем в Loki.js. В Creevey есть несколько режимов просмотра: не только как в Loki.js, но и вSWAP-режиме, когда окна просмотра переключаются в слайдовый режим, когда есть шторка, которую можно двигать.

Платные инструменты автоматизации




Кроме Loki.js и Creevey есть платные инструменты, например, Percy, Chromatic, Happo, которые поддерживают всё многообразие браузеров.

Платные инструменты просты в настройке и использовании. С Loki.js и Creevey нужно что-то делать в конфигах, уметь работать в консоли, желательно уметь настраивать Docker и Selenium Grid. Платные инструменты этого не требуют. Это просто Plug and Play поставил и запустил.

В платных инструментах удобнее смотреть изменения. В Loki.js и Creevey мы много работаем в консоли это может быть неудобно для не-разработчиков. Например, в Chromatic, это выглядит так.



Оригинал видео
Your browser does not support HTML5 video.


Все видно наглядно. В сервис может зайти дизайнер и посмотреть изменения в компонентах в своей ветке, а затем подтвердить или отклонить. После этого в CI-систему, например, в GitHub вам в pull request придет подтверждение. Это, конечно, намного удобнее, чем Loki.js и Creevey.

Доступны по цене. При этом у этих инструментов есть бесплатные тарифы для Open Source и достаточно дешевые платные тарифы, которые начинаются от 30$ в месяц.

Функциональные тесты


Скриншот-тесты хорошо работают. Но они покрывают только статичные сценарии. А нам интересно протестировать весь сценарий, когда пользователь зашел, ввёл текст, кликнул на кнопки НАЙТИ, подождал и получил результаты. Скриншот-тесты так не могут. Для этого, вместе со скриншот-тестами, нужно писать функциональные тесты.

Пример функционального теста


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

  • для мока браузера возьмем jsdom и testing-library;
  • для мока сетевых запросов axios-mock-adapter;
  • как тестовый фреймворк будем использовать Jest.

Вместо jsdom, testing-library, axios-mock-adapter и jest можно взять любые другие инструменты. Выбор конкретных инструментов не важен главное, чтобы вам и вашей команде было удобно с ними работать.



Настраиваем мок. Начнём тест с настройки сети.

const searchSpy = jest.fn();mock.onGet("/api/v1/search").replyOnce((request) => {searchSpy(request.params);return [200, { title: "TITLE", description: "DESCRIPTION" }];});

В первой строке кода создаем spy. Spy функция, которая запоминает все свои вызовы. В этом spy мы будем сохранять запросы к API поиска. Во второй строке настраиваем axios-mock-adapter: говорим ему, что в рамках теста придет запрос на /api/v1/search, на который нужно ответить 200 кодом и определенными данными. При этом нужно сохранить параметры запроса в spy.

Рендерим компонент. После настройки сети мы отрендерим компонент через testing-library. Через него же заполняем input поисковым запросом и кликаем на кнопку НАЙТИ. После этого ждем, когда все перерендерится.

render(<Search />);const inputEl = screen.getByPlaceholderText("Что ищешь?");fireEvent.change(inputEl, { target: { value: "ТЕСТ" } });const buttonEl = screen.getByText("Найти");fireEvent.click(buttonEl);await waitForRerender();

Теперь проверим был ли вызван поиск с тем текстом, который мы вводили с помощью testing-library и отобразил ли компонент результаты поиска в DOM-дереве.

expect(searchSpy).toHaveBeenCalledWith({ search: "ТЕСТ" });expect(screen.getByText("TITLE")).toBeInTheDocument();

Вот мы и написали функциональный тест. У него можно выделить следующие фазы:

  • Настраиваем окружение (API в нашем случае)
  • Рендерим компонент
  • Делаем какие-то действия в DOM
  • Ждём ререндера
  • Проверяем что окружение было вызвано так, как мы ожидали (в нашем случае проверяем вызов API).
  • Проверяем, что в DOM-дереве находится контент, который мы ожидали увидеть.

const searchSpy = jest.fn();mock.onGet("/api/v1/search").replyOnce((request) => {searchSpy(request.params);return [200, { title: "TITLE", description: "DESCRIPTION" }];});render(<Search />);const inputEl = screen.getByPlaceholderText("Что ищешь?");fireEvent.change(inputEl, { target: { value: "ТЕСТ" } });const buttonEl = screen.getByText("Найти");fireEvent.click(buttonEl);await waitForRerender();expect(searchSpy).toHaveBeenCalledWith({ search: "ТЕСТ" });expect(screen.getByText("TITLE")).toBeInTheDocument();

Плюсы и минусы


Это полноценный тест на UI. Он проверяет, что продукт работает: если ввести текст в input и нажать кнопку Найти, то приложение сделает запрос в API и выведет результаты поиска в интерфейсе.

С этим тестом можно рефакторить почти всё. Например, перенести логику из Store в компонент (или обратно), или заменить Redux на MobX.

Мы написали тесты без UI.


Немного комичный, но правдивый факт.

Но с этим тестом всё не так гладко.

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

Мы покрыли только позитивный сценарий, а у нас есть и другие. Например, API может ответить ошибкой 400, 500 или 404. Для каждого случая должна быть своя реакция приложения.

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

Повторяющийся код


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

const searchSpy = jest.fn();mock.onGet("/api/v1/search").replyOnce((request) => {searchSpy(request.params);return [200, { title: "TITLE", description: "DESCRIPTION" }];});

Код с сетевым запросом мы вынесем в объект, который назовем ApiMock.

export const createApiMock = (mock: MockAdapter) => ({search(searchResult: SearchResult) {const spy = jest.fn();mock.onGet("/api/v1/search").replyOnce((request) => {spy(request.params);return [200, searchResult];});return spy;},});

У этого объекта есть метод search, который настраивает axios-mock-adapter на поисковый запрос, используя аргумент метода как результат поиска.Также метод создаст для нас spy и вернет его.

Также мы знаем, что в каждом тесте будем вводить в input какой-то текст и нажимать на кнопку Найти. Часть с заполнением input и кликом на кнопку вынесем в объект, который назовем pageObject.

export const pageObject = {search(searchString: string) {const inputEl = screen.getByPlaceholderText("Что ищешь?");fireEvent.change(inputEl, { target: { value: searchString } });const buttonEl = screen.getByText("Найти");fireEvent.click(buttonEl);},getResult() {const resultEl = screen.getByTestId("search-result");return {title: resultEl.querySelector("h3")!.textContent,description: resultEl.querySelector("div")!.textContent,};},};

В нем сделаем метод search, который принимает только один аргумент поисковую строку. Он сам найдет input, введет в него значение, найдет кнопку и кликнет на нее.

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

Отрефакторенные тесты


Теперь тест занимает гораздо меньше места, при этом читается совершенно по-другому.

const spy = apiMock.search({ title: "TITLE", description: "DESCRIPTION" });render(<Search />);pageObject.search("ТЕСТ");await waitForRerender();expect(spy).toHaveBeenCalledWith({ search: "ТЕСТ" });expect(pageObject.getResult()).toEqual({title: "TITLE",description: "DESCRIPTION",});

Если раньше тест читался очень низкоуровнево настраиваем API, проставляем HTTP-код ответа, взаимодействуем с input, то теперь выглядит так:

  • Ожидаем, что будет сделан поиск через API, который вернет определенные данные.
  • Рендерим компонент.
  • Совершаем поиск по строке ТЕСТ.
  • Ждем ререндера.
  • Проверяем, что поиск был вызван с нужными параметрами, а на странице есть результаты поиска.

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

Тесты теперь высокоуровневые. Они описывают не работу кода, а сценарий пользователя.

Новые тесты писать проще меньше кода. Не нужно помнить наизусть, какие есть селекторы у компонента, по каким путям API ожидает запросы. Достаточно помнить практический смысл, а дальше просто написать pageObject, и посмотреть что предлагает автозаполнение.

Если мы рефакторим верстку, достаточно поправить только pageObject. Например, мы верстаем всю вёрстку на , а потом кто-то в команде посмотрел доклад Вадима Макеева и все решили, что проекту нужна семантичная верстка. В этом случае вместе с заменой на правильные элементы, не нужно будет править весь тест достаточно поправить только pageObject. Это следствие того что тесты теперь описывают сценарии, а не имплементацию.

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

Ещё раз взглянем на отрефакторенные тесты прочтем сверху вниз.

const spy = apiMock.search({ title: "TITLE", description: "DESCRIPTION" });render(<Search />);pageObject.search("ТЕСТ");await waitForRerender();expect(spy).toHaveBeenCalledWith({ search: "ТЕСТ" });expect(pageObject.getResult()).toEqual({title: "TITLE",description: "DESCRIPTION",});

Здесь нет ни слова об используемых инструментах и библиотеках. В этом тесте нет ничего ни об axios-mock-adapter, ни о testing-library или React. В коде теста участвует jest, но его несложно заменит на mocha + chai.

Подход с функциональными тестами работает с любыми инструментами.

А это значит, что если бы мы писали честный E2E-тест с использованием cypress, puppeteer или Selenium, то тест остался бы примерно таким же. Подход написания функциональных тестов с PageObjectами гибок и отлично масштабируется.

Как в итоге тестировать


  • Пирамида тестирования работает, но не во фронтенде. У фронтенда своя пирамида, в которой требуется больше интеграционных тестов.
  • Заводите Storybook он ускоряет разработку.
  • Скриншотные тесты очень легко внедрить, но при этом они хорошо работают.
  • Одних скриншот-тестов не хватит, нужны еще функциональные тесты.
  • Frontend инфраструктура позволяет с легкостью мокать окружение (браузер, сеть и тд). Используйте это. Но старайтесь не мокать внутреннюю имплементацию своего кода.
  • Для тестов поведения отлично подходит связка testing-library, инструмент для мока сетевых запросов и паттерн pageObject.


Ссылка на твит

На Frontend Live 2020 мы уделим тестированию отдельный трек. Это 2 дня полного погружения в тематику: доклады, мастер-классы, панельные дискуссии со спикерами и участниками. Обсудим, как обстоят дела с тестированием сейчас, какие наметились тренды, кому и чего не хватает, где взять знания, навыки и инструменты. И конечно, участники получат карту и пирамиду тестирования фронтенда с типами тестирования и применяемыми технологиями.

Бронируйте билеты 14 сентября повышение цены. Подписывайтесь на рассылку, в которой присылаем новости, анонсы и промокоды:)
Подробнее..

Будни такси ВКонтакте

17.12.2020 10:10:32 | Автор: admin
Артем Пулявин руководит фронтенд-разработкой в Ситимобил и на конференции FrontendConf 2020 рассказал про такси ВКонтакте: как проект начинался и каким он стал.

Проект задумывался внутри Ситимобил как экспериментальный, но за год по количеству поездок вырос в 30.000 раз. Артем поделился своими выводами о том, как удалось добиться таких результатов. Вы узнаете про процесс разработки, который теперь удовлетворяет ненасытных product-менеджеров, а еще как команда Такси ВКонтакте обходится без алертов, чем логирует ошибки и как расследует инциденты.



О продукте


Такси ВКонтакте это сервис на платформе VK Mini Apps, который позволяет заказывать такси внутри приложения ВКонтакте. Такси ВКонтакте официальный суббренд Ситимобил, то есть такой же бренд наравне с Ситимобил.

Сейчас мы делаем где-то 20% от общих поездок Ситимобила. Но есть города, где мы лидируем например, в Казани среди всех агрегаторов мы занимаем 40-50% рынка. Почему-то в Казани очень любят ВК и все сидят на ВК такси.

В целом, до Ситимобила нам еще далеко пока это 1 к 5, но если считать от общего трафика, то это очень даже много. На старте никто не верил в нас: Такси ВКонтакте кто этим будет пользоваться, зачем на это тратить ресурсы и время? И когда мы делали 30 поездок, нас серьезно никто не воспринимал. Но мы захватили потерянную часть аудитории, которой нет в Ситимобил (там люди от 25 до 45) молодых людей от 16 до 23 лет. Сейчас мы делаем больше ста тысяч поездок и мы официальная большая платформа.

Такси ВКонтакте это:
  • ~ 4 миллиона пользователей в vk.cm/taxi;
  • ~ 250 тысяч подписчиков в vk cm/vktaxi;
  • >100 тысяч поездок в день;
  • ~ 250 тысяч DAU (активных пользователей в день);
  • ~ 2.2 миллиона MAU (активных пользователей в месяц).

Наша команда сейчас состоит из 12 человек:
  • 2 product-менеджера;
  • 5 фронтенд-разработчиков;
  • 1 бэкенд-разработчик;
  • 1 дизайнер;
  • 2 аналитика;
  • 1 QA.

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



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

Мы нашли аутсорсеров, которые за несколько месяцев сделали нам первую версию проекта. Тогда мы назывались ВК такси. Мы разместили в нашей официальной группе Ситимобил новость про новый сервис и ссылку на него. Никакой рекламы не делали, но в течение месяца мы увидели, что сервис генерирует 30 поездок в день. И эта маленькое, но хорошее число было показателем того, что сервис интересен мы поняли, что сервис можно развивать. И весной 2019 года мы сформировали для этого внутри Ситимобил маленькую команду из 3 человек:
  • 2 фронтенд-разработчика;
  • 1 product-менеджер.

VK Mini APPs


ВКонтакте движется в сторону SuperApp, где пользователь может удовлетворить все свои насущные проблемы: заказать еду, такси, химчистку, вызвать клининг, поиграть в игрушки и т.д. В результате весной 2018 года была анонсирована платформа VK Mini APPs. По сути это WebView, который запускает URL c вашим сайтом, а вы в нем что-то показываете. Для того, чтобы вы могли коммуницировать с родительским приложением в ВКонтакте, ВК предоставляет библиотеку VK Bridge (раньше она называлась VK Connect). Через нее можно сделать запрос к VK API, получить данные пользователя, а помимо этого:
  • Включить / отключить нотификации Mini Apps;
  • Получить консистентные данные из VK Storage это хранилище, которое закрепляется за пользователем ВК, и неважно, на каком девайсе он авторизован.
  • Платить через VK Pay внутреннюю платежную систему внутри ВК;
  • Запросить геопозицию через VK Bridge, используя нативные инструменты самого девайса:



Наш эксперимент


Итак, на момент старта у нас было три человека, 30 поездок в день и legacy аутсорс кодовая база. Перед нами стояло две задачи:
  1. Вырастить функционал проекта ВК такси до уровня Ситимобил. На старте у нас было маленькое примитивное приложение. Всё, что оно могло делать построить маршрут: после нажатия кнопки Заказать такси, мы могли увидеть на карте, как к вам едет машина, а после поездки оценить её. И на этом всё. Это очень сильно отставало от того, что было в большом Ситимобил.
  2. Рост O2R до уровня Ситимобил. При заказе такси вы указываете точку А и точку В и видите цену заказа. После этого вы нажимаете ЗАКАЗАТЬ, к вам приезжает машина и вы уезжаете. Так вот время от момента, когда вы только посмотрели калькуляцию и до момента, как уехали это наша внутренняя метрика Order2Ride. И нам нужно было показать, что проект ВК Такси конкурентоспособен, и для этого поднять эту метрику до уровня Ситимобил.

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

VK UI


Нашей команде из трех человек product поставлял задачи, оформленные в виде mock-up у нас не было дизайнера. Поэтому нас очень сильно выручила библиотека VK UI, которую вместе с VK Bridge предлагает Mini Apps.

VK UI это набор компонентов, написанных на ReactJS (именно поэтому наше приложение написано на ReactJS), и они очень схожи с компонентами самого ВК. Поэтому, хотя пользователь уже вовсю использует Mini Apps, ему кажется, что он и не выходил за пределы ВК всё так же нативно, как будто он пользуется внутренним сервисом ВКонтакте.



Много тестирования


Из-за того, что у нас был единственный Mini App, мы тратили очень много времени на тестирование.

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

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

Quality Assurance


Понятно, чтобы разгрузить нас, нужен был QA. Но перед наймом QA мы решили, что нам нужно больше тестовых площадок нельзя же давать QA возможность переключать режимы. Мы пообщались с командой ВК, и они нам предложили классное решение создать для нас больше отдельных Mini Apps, без включения общего доступа (но вручную администраторов мы туда могли добавлять). Для каждого Mini App было несколько веток и свой отдельный url, который подгружал этот Mini App. При пуше запускался наш GitLab CI и релизил пуш на url, смотрящий на одну из тестовых Mini App.

Таких тестовых площадок мы сделали три (сейчас их гораздо больше):
  1. Stage был предназначен для разработчиков. Они разрабатывали (и до сих пор так делают) всё локально. И когда им нужно поработать с реальным VK Bridge внутри ВК, они релизят приложение на stage и смотрят, как оно реально работает.
  2. Test эту площадку мы отдали тестерам.
  3. Демо-площадка для PM, чтобы он мог показывать наши эксперименты директору по продукту и давать нам фидбэк.

У нас по-прежнему один QA, он по-прежнему всё делает руками. Но мы сейчас работаем над автоматизацией хотим прогонять UI тесты на Cypress + PhantomJS. Чтобы при пуше или создании MergeRequest запускался пайплайн, генерил видео и закидывал его в MergeRequest,. И по результату можно посмотреть по видео, что пошло не так. Постепенно мы к этому придем.

Оптимизация devs и QA


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

Мы создали маленькую утилиту, которая автоматически выкатывала ветку на тестовую площадку. В JIRA появлялись ссылки на MergeRequest, QA запускал скрипт и выкатывал нужную фичу-ветку на свой стенд и там ее тестировал. Так наш QA не зависел от разработчика (и снял с него часть работы), а разработчик не тратил время на раскатку для QA.

Но почему-то процесс разработки у нас все равно не ускорялся.

Мы проанализировали наш flow: разработчик берет задачу, переводит ее в IN PROGRESS, далее CODE REVIEW и TESTING. Эту ветку мы деплоили на DEMO и показывали productу, и если product говорил, что все ОК, мы релизились. Если нет, то возвращали в IN PROGRESS и дорабатывали:



И стало понятно, что мы тратим много времени на подготовку демо, и это в условиях, когда у нас нет дизайнера. Хотя разработчики сами решали какие-то проблемы из-за этого, все равно не все use cases были описаны и дизайн не был детализирован, а об edge cases вообще никто не думал. Поэтому мы поставили этап DEMO после IN PROGRESS:



И получили профит! Разработчик выкладывал задачу productу на демо и получал от него фидбэк. Они могли даже вместе сесть и начать что-то править по ходу. Например, забавно было с фичей Разделить поездку с друзьями. Мы ее пилили в спокойном режиме, а product, увидев, загорелся: Классная фича! Давайте быстрее. Будем показывать ее в рекламе. И нам пришлось максимально подключить все ресурсы, чтобы ее срочно зарелизить.

И как только product говорил, что это то, что он хочет, разработчик переводил задачу на CODE REVIEW, и там получали код, который готов идти на production без каких-либо правок. Тестировщик это все тестировал, и мы релизились. Это очень сильно сократило наш Time То Market, и так мы работаем до сих пор.

А что со стабильностью?


Логи


С первого дня, как мы вытащили кодовую базу наших аутсорсеров и подключили New Relic Browser, начали сыпаться ошибки. Ошибок было очень много и разных, начиная от того, что у нас не было полифилов на старых девайсах и заканчивая ошибками какой-то бизнес-логики. Мы это все быстро пофиксили и все приложение покрыли New Relic логами. Логи сделали трех уровней info, warning, critical:



Мы используем обычную rest-ручку и кидаем эвенты с контекстом на бэкенд. Все эвенты обрабатываются Filebeat через Logstash в Elasticsearch, а мы в итоге видим всё это в Kibana:



Мы можем задать конкретный id пользователя и посмотреть все эвенты, которые произошли, чтобы понять, на каком этапе что отвалилось:



Вначале, когда мы дергали эту рестовую ручку и кидали по логу за раз, мы уперлись в наш rate limit, так как сисадмины выставили rate limit на бэкенде на количество запросов. Поэтому мы сгруппировали логи в несколько стримов:
  • Apps logs пачка по 5 штук. Например, пользователь пришёл на какой-то экран, нажал на какую-то кнопку, сделал запрос, получил данные.
  • Analytics logs пачка по 10 штук. Таких логов очень много наши аналитики строят по ним дашборды.
  • Important logs (например, событие открытия приложения app_open). Это важные логи, которые мы сбрасываем моментально. Например, если приложение не загрузилось, то мы смотрим, а произошел app_open или нет. И если произошёл, то ищем, в чем же реально возникла проблема у пользователя.
  • По time out каждую секунду, даже если не набрали пачку из 5 или 10 логов.

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

У нас нет алертов


Сейчас New Relic мы используем как дашборд фона наших ошибок. Но и не только ошибок. Например, мы можем увидеть, сколько открытий приложения было за полчаса или сколько раз мы обращались в storage ВК за какое-то время (и если обращений стало меньше, то это сигнал, что что-то идет не так).

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

Ребята на бэкенде используют New Relic алерты строят графики, base line, и если график выходит за base line, им приходит СМС (или звонит девушка из Алабамы) о том, что все плохо. Но мы не видим в этом смысла, потому что у нас нагрузка и так всегда большая, поэтому как раз по девиации фона ошибок мы и видим, если что не так пошло во время релиза.

Например, мы использовали фон ошибок New Relic, когда Google Tag Manager перевели в асинхронный режим и на 10% ускорили загрузку приложения. Еще был момент, когда у аутсорсеров в bundle грузилось много ненужных библиотек, и мы это увидели благодаря New Relic вырезали и вычистили все ненужные. Но он все же не покрывает все наши потребности, поэтому мы используем и другие инструменты.

Дополнительно мы проводим Smoke-тестирование на production когда релизим, тестировщик проверяет базовое flow поездки. И здесь нам важнее, чтобы сама процедура заказа работала. Может отвалиться история заказа, рейтинг водителя это не страшно. Главное, чтобы человек мог зайти, выбрать точки А и В, нажать ЗАКАЗАТЬ, и к нему бы приехал водитель.

Sentry


Если New Relic дает общую картину, то Sentry мы используем для групповых вещей. Он позволяет группировать ошибки по версии ВК, по платформе, показывает их частотность и помогает своими breadcrumbs. Например, пришел какой-то запрос на сервер, потом еще один, а потом пользователь куда-то кликнул и произошел exception. Для разбора таких инцидентов мы и используем Sentry.

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



По id клиента в ВКонтакте нам нужно понять, что у него случилось: не запускается приложение, не загрузился bundle, VK Bridge не инициализировался, поездка не совершилась или что-то другое. Чтобы понять конкретную проблему, мы используем логи.

В заключение


Выводы, которые я сделал для себя за год работы над этим проектом:
  1. Следите за Time То Market. Для нас Time То Market была метрикой выживаемости и ее сокращение дало нам лучший профит. Мы зарелизили большое количество фич, нагнали аудиторию, в нас поверили и команда ВК, и внутри компании, и мы достигли тех результатов, которые сейчас имеем. Поэтому экспериментируйте. Если видите какие-то блокеры, меняйте процесс ничего страшного в этом нет.
  2. Данных для расследования инцидентов мало не бывает. Используйте различные инструменты, покройте все ваше приложение логами, и тогда вы можете четко понимать, что же пошло не так у конкретного пользователя. VK Mini Apps это круто. Очень рекомендую эту платформу, потому что аудитория в приложении ВК порядка 200 млн. Делая Mini Apps вы можете попасть в ротацию, в саджесты вы сразу, без какой-либо рекламы, будете доступны своей аудитории. Здесь можно свой pet-project замутить или проект в рамках компании перевести на Mini Apps.

Конференция FrontendConf 2021 пройдет 29 и 30 апреля 2021 года (да, офлайн!) в Москве, в бизнес-центре Radisson Slavyanskaya.

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

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

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

Присоединяйтесь сами, зовите коллег, готовьте вопросы для спикеров.
До встречи на митапах!
Подробнее..

Как разрабатывать сотни AB экспериментов

12.01.2021 10:14:24 | Автор: admin
А/Б-тестирование это способ измерить эффективность нового функционала путем сравнения. Вы создаете новый заголовок, кнопку или изображение и показываете их только части аудитории сайта. В течение нескольких недель собираете статистику об использовании нового функционала и на основании этого принимаете решение об открытии новой фичи для 100% пользователей.

Senior Frontend Developer ЦИАН Иван Бабков, который разрабатывал приложения для регистрации доменов, интернет-банкинга и поиска по жилой недвижимости в своем докладе на конференции Frontend Conf рассказал об инфраструктуре компании для работы с А/Б-экспериментами, проблемах и путях их решения.

image

Что такое A/B тестирование


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

В ЦИАН A/B тестирование больше, чем просто сравнение. Это целый процесс, который начинается с того, что стейкхолдер и аналитики продумывают дизайн эксперимента. Не тот дизайн, к которому мы привыкли, не дизайн нарисованный, а дизайн, позволяющий понять, сколько будет групп в эксперименте, сколько вариантов наших кнопок, сколько придется ждать результаты проведения A/B эксперимента и т.д.

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

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

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

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

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



Спустя какое-то время, когда накоплены данные, которые отправлялись с разных вариантов интерфейса, можно сделать вывод, что конверсия в клики по первому варианту интерфейса 17%, по второму 25%. Логично, что на сайте предпочли оставить второй вариант. Это поможет в дальнейшем улучшить user experience.

Последний шаг, который необходимо сделать: пустить все 100% пользователей на новый вариант интерфейса.

Пример разработки frontend A/B эксперимента в ЦИАН


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

В какой-то момент продакт-менеджер ЦИАН совместно с дизайнером и аналитиком создали задачу следующего вида:



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

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



Дизайнер рисует два варианта макета:



А. Старый вариант, где есть один блок акций.

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

Первое, что необходимо сделать: перенести описание эксперимента в код. В ЦИАН есть админка, позволяющая добавлять и редактировать новые эксперименты:



В ней нужно заполнить форму следующего содержания:

  • Название эксперимента;
  • Время, когда он начнется;
  • Количество A/B групп;
  • Процентное соотношение в A/B группах.

При нажатии кнопки СГЕНЕРИРОВАТЬ, вся информация попадает в базу данных, откуда бэкенд может ее запрашивать.

Далее можно запросить c бэкенда только что сгенерированную информацию по актуальным для пользователя экспериментам из бэкендового микросервиса.

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



Вариант ответа это список, в котором есть объекты с именем эксперимента и A/B группа, в которую попадает пользователь в этом отдельном эксперименте.

Нужно внести полученную с бэкенда информацию об эксперименте в Redux store и проверить в компонентах, какой вариант интерфейса в коде нужно отрисовывать в A/B группе.

В проекте есть всего две директории:



  1. GKCard это директория, в которой лежит компонент жилого комплекса, который вы видели на скриншоте;
  2. PromoLabel компонент акции (в примере: скидка на квартиры до 7%).

В компоненте GKCard отрисовывается PromoLabel.

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



Добавляем директорию ab, в нее директорию с названием эксперимента (в нашем примере: newbuilding_promos), а туда все файлы.

В компоненте жилого комплекса добавляем всего несколько строк:



  • connect, чтобы получать данные из store;
  • PromoLabel с новой версткой, где будет 3 акции;
  • Флаг isABEnabled для того, чтобы отрисовывать либо старый, либо новый вариант акции.

В connect берем список всех экспериментов, ищем в них эксперимент под именем newbuilding_promos, проверяем, есть ли такой эксперимент, и в этом объекте находим поле A/B групп, которое получили с бэкенда.



Если его значение 1, то отрисовываем новый вариант акции. Если нет, то старый:



То есть пользователь попадает в группу 1, если он получает новый вариант интерфейса.

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



Для отправки в примере использована библиотека ReactGA, но можно использовать множество других библиотек в NPM. В ЦИАН используют библиотеку собственной разработки, дублируя туда отправку:





Мы зарелизили задачу, все хорошо. Спустя две недели приходит новая.

Важный момент: аналитики провели статистические тесты, подтвердили правильность полученных результатов.



В задаче были данные, что 13% пользователей кликают на блок акций в старом варианте, 21% в новом. Стейкхолдер вместе с аналитиками попросили Ивана раскатать вариант В на 100% пользователей. Это повысит конверсию в клике: пользователи видят эти акции, все хорошо.

Чтобы сделать это, нужно воспользоваться админкой:



Нужно перекинуть пользователей в БД в группу В. Теперь пользователи с ID от 0 до 99 увидят второй вариант интерфейса. Первый вариант не увидит никто.



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



В ЦИАН хранят не только актуальные, но и уже завершенные эксперименты. Никогда не знаешь, когда может понадобиться информация о них.

Теперь нужно перенести файлы из этой директории на нужный уровень вложенности проекта (в нашем случае рядом со старым блоком акций) и удалить старый блок:



Все! Следы эксперимента остались лишь в git log и в БД.

Немного про инфраструктуру


Есть запрос. Он пришел из браузера на наш внешний Nginx:



На Nginx мы запускаем скрипт, который генерирует ID от 0 до 99. Есть функция примерно следующего вида:



Функция возвращает число от 0 до 99. Она принимает строчку в виде уникального идентификатора пользователя. В нашем примере это будет:

uid = abfd-4843

Эта строчка передается функции md5 для генерации хэша:



Хэш получается шестнадцатеричный:

hash = FD029AAD2251AD74F8223B4F4A80B6EA

Мы преобразуем его в десятичное число и делим на 100:



parseInt(hash, 16)=3.363082047354731e+38

Остатком от этого деления будет число от 0 до 99. В нашем примере 68:

parseInt(hash, 16) % 100 = 68

График, который иллюстрирует равномерность распределения сгенерированных ID на 10 млн уникальных идентификаторов пользователя:



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

Мы запрашиваем страницу с ID, который только что сгенерировали, и передаем его как HTTP заголовок:



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



Request body


Запрос выглядит примерно так:



Мы передаем ID, только что нами сгенерированный на Nginx, и список экспериментов (их имен, которые фронтенд хочет получить от бэкенда). Бэкенд должен запросить этот список из БД, что он и делает:



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



Но тут есть важный момент: мы должны вернуть не все эксперименты, а только часть.



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

Это мы делаем следующим образом: берем сгенерированный ID (например, 50) и информацию, которую храним в БД.



Среди прочей информации (название эксперимента, время его начала, насколько эксперимент актуален, идет ли он сейчас, номер задачи) есть два важных элемента. Это списки, которые были сгенерированы при создании. Они содержат числа от 0 до 99. Числа уникальны, то есть в первом и втором списке не может быть числа 0.

Почему их два? Потому что при создании эксперимента мы создавали две A/B группы.
Берем наше число (в примере AB_ID = 50) и ищем в каждом из списков. Находим во втором:



Это говорит нам о том, что пользователь с ID = 50 попадает во вторую A/B группу и увидит второй вариант интерфейса.

Но одновременно у нас идет множество экспериментов:



В одном из них пользователь может попасть в третью группу, в другом во вторую, в третьем в первую.



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

Ответ выглядит следующим образом:



Это список, в котором есть объекты следующего содержания: имя эксперимента и A/B группа, в которую попадает пользователь.

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



Теперь у нас есть два варианта интерфейса, на которые приходят две разные группы пользователей:



Озвученный подход к проведению A/B экспериментов не единственный. Можно проводить их не только в приложениях сервера рендерингом, но и исключительно на клиенте. Например, делать это на бэкенде, подмешивая дополнительные результаты в поисковую выдачу только для определенной группы пользователей. И даже на Nginx (но мы так не поступаем, так как считаем его неподходящим инструментом для бизнес-логики).

Как принудительно попасть в определенную A/B группу?


Понятие A/B тестирования отчасти пришло к нам из медицины. Есть понимание, что в каждом эксперименте должна быть контрольная группа.

В ЦИАН ею является нулевая группа. То есть пользователи при делении попадают, допустим, в две группы, и нулевая всегда избавлена от интерфейсов.

Чтобы автотесты или ручное тестирование, когда тестировщик проверяет задачу, могли попасть в определенную группу, передаем с фронта HTTP заголовок с ID.



Это ID от 0 до 99. Nginx настроен таким образом, что он игнорирует генерацию нового идентификатора и принимает тот, который пришел с клиента, пробрасывая его дальше. Соответственно, все дальнейшие предложения видят ID, которые мы передали с клиента.

Когда Nginx проигнорировал генерацию нового ID, он передает запрос на фронтовый микросервис.

Планы


Система ЦИАН не идеальна. И есть некоторые моменты, которые Иван планирует улучшить:

  • Таргетировать эксперименты по определенным параметрам (пол, геопозиция, дата регистрации и т.п.).
    В компании хотят научиться проводить эксперименты, например, для 17% мужчин из Петербурга, которые зарегистрировались у нас не ранее года назад.
  • Добавить возможность таргетировать по дополнительным параметрам в админку, чтобы не делать этого руками.
  • Разрабатывать больше экспериментов.

Но уже сегодня A/B эксперименты приносят свои плоды.

ЦИАН это крупнейший сервис по поиску недвижимости в России и один из самых больших в мире. Компания входит в мировой ТОП-10 крупнейших сайтов по недвижимости по версии Similar Web.

В подобных условиях каждое продуктовое или архитектурное решение должно быть продуманным и обоснованным, и постоянное проведение A/B экспериментов помогает этого достичь.

Frontend Conf 2021 пройдет 29 и 30 апреля. Но билеты на нее по самой выгодной цене вы можете приобрести уже сегодня.
Подробнее..

Вам показалось! Все о Perceived Performance

21.01.2021 14:15:10 | Автор: admin

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

В большинстве случаев с ростом реальной производительности улучшается и Perceived Performance. А когда реальная производительность не может быть с легкостью увеличена, существует возможность поднять видимую. В своем докладе на Frontend Live 2020 бывший разработчик Avito Frontend Architecture Алексей Охрименко рассказал о приемах, которые улучшают ощущение скорости там, где ускорить уже нельзя.

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

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

  • беспокойство;

  • неуверенность;

  • дискомфорт;

  • раздражение;

  • скуку.

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

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

Не игнорируйте этот момент: вы обязательно должны его учитывать.

Для этого есть множество разнообразных техник, в том числе:

  • lighthouse;

  • devTools profiler.

Но даже если все сделать идеально, этого может оказаться недостаточно.

Есть интересная история про аэропорт Huston. Она отлично вписывается и в современные реалии разработчиков.

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

Люди часто жаловались на долгое ожидание багажа в аэропорту. И инженеры аэропорта Huston не спали ночами, работали сверхурочно, но реализовали выдачу багажа всего за 8 минут. Однако клиенты все равно продолжали жаловаться.

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

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

Perceived Performance

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

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

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

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

Все дальнейшие примеры будут для Angular приложений, но, несмотря на это, они применимы к любому современному фреймворку. Приступим!

Не блокируйте пользователя

Первый совет: ни в коем случае не стоит блокировать пользователя. Вы можете сейчас сказать: Я и не блокирую!.

Попробуйте узнать себя в этом примере:

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

Все равно не узнаете? А так?

Спиннеры это не выход! Хоть они и могут стать ленивым способом разобраться с неудобной ситуацией.

Что можно предложить в качестве альтернативы спиннеру?

Можно нажать на кнопку УДАЛИТЬ и показать статус этой кнопки (item удаляется только для одного элемента), не демонстрируя спиннер. В дальнейшем можно отправить запрос на сервер, и когда он придет, показать, что элемент удалился, либо при ошибке передать данные о ней. Вы можете возразить: Но я могу делать только 1 запрос за раз! это ограничение бэкенда. С помощью RxJs и оператора concat можно буквально одной строчкой кода создать минимальную очередь:

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

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

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

В Angular есть ngx-spinner, который поддерживает такой функционал.

Это, как минимум, уменьшит время ожидания, и пользователь сможет в этот момент сделать хоть что-то.

Обманывайте

Обман зачастую базируется на иллюзиях скорости.

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

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

Где можно применить такую методологию?

  • Progress Bar;

Есть исследование, показывающее, что если добавить полоски, которые идут в обратном направлении внутри Progress Bar, то визуально он выглядит как более быстрый. В такой настройке можно получить до 12% ускорения, просто применив дополнительный скин. И пользователи воспримут работу, прошедшую под этим Progress Bar, на 12% быстрее. Вот пример того, как можно реализовать такой слайдер на CSS.

  • Скелетон;

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

Скелетон это некое схематическое отображение сайта до момента его загрузки:

В этом примере мы видим, где будут расположены чаты.

Существует исследование, которое показывает, что люди воспринимают скелетоны быстрее от 10 до 20%.

То есть по ощущениям пользователей, сайты со скелетоном работают быстрее.

Существует огромное количество нужных компонентов для Angular, React, View. К примеру, для Angular есть skeleton-loader, в котором можно прописать внешний вид и сконфигурировать его. После чего мы получим наш скелетон:

  • Экспоненциальная выдержка.

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

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

Это одна из best practice в энтерпрайз-приложениях, потому что бэкенд может не работать по разным причинам. Например, происходит деплой или хитрая маршрутизация. В любом случае очень хорошее решение: попробовать повторить. Ведь никакое API не дает 100% гарантии, 99,9% uptime.

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

Но даже с этим сценарием мы сами себе можем сделать DDOS (Distributed Denial of Service). На это попадались многие компании. Например, Apple с запуском своего сервиса MobileMe.

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

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

Best practice: применять exponential backoff. В rxjs есть хороший дополнительный npm пакет backoff-rxjs, который за вас имплементирует данный паттерн.

Имплементация очень простая, 10 строчек кода. Здесь вы можете обойтись одной. Указываете интервал, через который начнутся повторы, количество попыток, и сбрасывать ли увеличивающийся таймер. То есть вы увеличиваете по экспоненте каждую следующую попытку: первую делаете через 1 с, следующую через 2 с, потом через 4 с и т.д.

Играя с этими настройками, вы можете настраивать их под ваше API.

Следующий совет очень простой добавить Math.random() для initialInterval:

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

Предугадывайте!

Как уменьшить ожидание, когда невозможно ускорить процесс?

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

  • Предзагрузка картинок;

Один из самых простых примеров это предзагрузка картинок. К примеру, у вас есть SPA-приложение. Вы открываете одну страничку, а на следующей есть набор иконок. Вы можете очень простым скриптом написать preload и загружать на конкретной странице картинки, необходимые для другой:

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

  • Предзагрузка 18+

Наверняка вы сталкивались в HTML со стеком link, который позволяет переподключить stylesheets:

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

Можно указать атрибут rel ="preload", сослаться в ссылке на наш элемент (href="styles/main.css"), и в атрибуте as описать тип предзагружаемого контента.

  • prefetch.

Еще один вариант это prefetch:

Главное запомнить, что preload и prefetch два самых полезных инструмента. Отличие preload от prefetch в том, что preload заставляет браузер делать запрос, принуждает его. Обычно это имеет смысл, если вы предзагружаете ресурс на текущей странице, к примеру, hero images (большую картинку).

ОК, это уже лучше, но есть одна маленькая проблема.

Если взять какой-нибудь среднестатистический сайт и начать префетчить все JavaScript модули, то средний рост по больнице составляет 3 МБ. Если мы будем префетчить только то, что видим на странице, получаем примерно половину 1,2 МБ. Ситуация получше, но все равно не очень.

Что же делать?

Давайте добавим Machine Learning

Сделать это можно с помощью библиотеки Guess.js. Она создана разработчиками Google и интегрирована с Google Analytics.

Анализируя паттерны поведения пользователей и, подключаясь к вашему приложению и системе сборки, она может интеллектуально делать prefetch только 7% на странице.

При этом эта библиотека будет точна на 90%. Загрузив всего 7%, она угадает желания 90% пользователей. В результате вы выигрываете и от prefetching/preloading, и от того, что не загружаете все подряд. Guess.js это идеальный баланс.

Сейчас Guess.js работает из коробки с Angular, Nuxt js, Next.js и Gatsby. Подключение очень легкое.

Поговорим о Click-ах

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

Как предугадать, на что кликнет пользователь? Есть очевидный ответ.

У нас есть событие, которое называется mousedown. Оно срабатывает в среднем на 100-200 мс раньше, чем событие Click.

Применяется очень просто:

Просто поменяв click на mousedown, мы можем выиграть 100-200 мс.

Мы можем заранее предсказать, что пользователь кликнет на ссылку.

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

Опять магия ML? Нет. Существует паттерн: когда мы хотим на что-то кликнуть, мы замедляем движение (чтобы было легче попасть мышкой на нужный элемент) .

Есть библиотека, которая анализирует скорость, и благодаря этому может предсказать, куда я кликну.

Библиотека называется futurelink. Ее можно использовать абсолютно с любым фреймворком:

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

Что пользователь хочет получить при переходе на страницу? В большинстве сценариев: HTML, CSS и немного картинок.

Все это можно реализовать за счет серверного рендеринга SSR.

В Angular для этого достаточно добавить одну команду:

ng add @nguniversal/express-engine

В большинстве случаев это работает замечательно, и у вас появится Server-Side Rendering.

Но что, если вы не на Angular? Или у вас большой проект, и вы понимаете, что внедрение серверного рендеринга потребует довольно большого количества усилий?

Здесь можно воспользоваться статическим prerender: отрендерить страницы заранее, превратить их в HTML. Для этого есть классный плагин для webpack, который называется PrerenderSPAPlugin:

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

Но вы можете сделать все еще проще: зайти в свое SPA приложение и написать:

document.documentElement.outerHTML,

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

Заключение

Несмотря на то что Perceived Performance очень субъективная метрика, ее можно и нужно измерять. Об этом говорилось в докладе Виктора Русаковича на FrontendConf 2019.

Он рассказывал о том, что в Скелетоне есть анимация в плавном фоне, и слева направо она воспринимается на 68% быстрее, чем справа налево. Есть разные исследования, которые показывают, что неправильно примененные техники Perceived Performance могут визуально сделать сайт хуже. Поэтому обязательно тестируйте.

Сделать это можно при помощи сервиса под названием Яндекс.Толока.

Он позволяет выбирать, какой из двух объектов лучше. Можно сделать два видео с разной анимацией и предложить пользователям оценить, какой из них быстрее. Это стоит недорого, скейлится очень хорошо. В сервисе зарегистрировано огромное количество людей из разных регионов, то есть можно делать разнообразные выборки по разным параметрам. Благодаря этому есть возможность провести серьезное UX-исследование за небольшую сумму.

Даже если в конце концов вашему начальнику покажется, что быстрее не стало, проведите исследование и попробуйте улучшить ситуацию с помощью Perceived Performance.

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

Конференция, посвященная всем аспектам разработки клиентской части веб проектов, FrontendConf 2021 пройдет 29 и 30 апреля. Билеты можно приобрести здесь. Вы можете успеть купить их до повышения цены!

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

Черновик

Подробнее..

Категории

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

  • Имя: Макс
    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