Атака на АБ-тест: рецепт 'R'+t(101)+'es46'
Несколько месяцев назад сервис рекомендаций REES46 начал предлагать сравнение своей системы рекомендаций с сервисом Retail Rocket через АБ-тесты в формате «пари» с обязательством заплатить 100 000 рублей в случае проигрыша.
Первые тесты пройдены и в нашу редакцию поступило исследование аналитиков Retail Rocket (копия есть в распоряжении издания) в котором авторы столкнулись с довольно странными результатами. Предлагаем вам для ознакомления полную версию исследования проведённого АБ-теста.
АБ-тестирование систем рекомендаций в интернет-магазине «Дочки&Сыночки»
В интернет-магазине «Дочки&Сыночки» в течение нескольких месяцев шел тест трех рекомендательных систем: Retail Rocket, REES46 и внутренней системы компании.
Механика проведения АБ-тестирования: вся аудитория сайта случайным образом делится на три равных части, и каждая часть аудитории видит свою версию сайта. Меняются только блоки персональных рекомендаций — каждому сегменту показываются блоки, управляемые одной из рекомендательных систем:
В рамках теста измеряется конверсия каждого сегмента трафика, сравнивается с другими и по результатам принимается решение о том, какая система работает эффективнее.
Аудитория делится на клиенте с помощью JavaScript-кода, все пользователи получают идентификатор одного из трех сегментов теста, который сохраняется в куке и затем передается в Google (сайт нарушает закон РФ) Analytics при каждом значимом действии на сайте.
Результаты теста на момент написания статьи из Google (сайт нарушает закон РФ) Analytics — конверсия по сегментам
Сегмент А — рекомендательная система Дочки Сыночки
Сегмент В — рекомендательная система REES46
Сегмент С — рекомендательная система Retail Rocket
Изменения конверсии относительно показателей внутренней рекомендательной системы «Дочки&Сыночки»
По этим данным сегмент С (Retail Rocket) проигрывает, сегмент B (REES46) выигрывает. Отдельно обратите внимание 27 мая, в этот день Retail Rocket показывает лучшие показатели — к этой детали мы вернемся позже.
В течение теста инженерная команда Retail Rocket провела множество внутренних тестов, выявила несколько ошибок на сайте, исправила немало проблем с интеграцией и провела набор внутренних тестов различных алгоритмов и их вариаций. Ощутимых изменений все эти действия не принесли.
Визуальная оценка качества рекомендаций
У нас в Retail Rocket есть несколько способов оценки эффективности и качества рекомендаций. Самый первый из них – так называемая “экспертная оценка” (субъективная визуальная оценка “адекватности”).
Посмотрим на примеры рекомендаций, сформированные системами Retail Rocket и REES46:
К наполнителю для кошачьего туалета, наша система рекомендует переноску для животных и разные виды корма для кошек, а система REES46 — детское питание, чай и ректальную трубку для детей.
Таких примеров по довольно посещаемым товарам (по которым быстро копится статистика) очень много (вот один из отчетов по визуальной оценке качества), и несмотря на то, что экспертная оценка не влияет напрямую на цифры, это простой и быстрый способ, который служит определенным показателем качества работы рекомендательных систем.
Косвенная оценка качества рекомендаций
Нам показалось странным, что при такой визуальной составляющей, цифры показывают результат не в нашу пользу, поэтому мы потратили много ресурсов на различные внутренние исследования причин.
В первую очередь мы решили исследовать аудиторию, которая взаимодействует с блоками товарных рекомендаций. При клике товары в рекомендательных блоках REES46, к URL добавляется параметр:
Мы добавили похожий параметр в URL товаров из блоков рекомендаций Retail Rocket:
И построили в GA сегменты кликавших в рекомендательные блоки пользователей:
Первой гипотезой стало то, что наша система хуже угадывает предпочтения пользователей, рекомендует менее релевантные товары.
Если это так, то наши блоки должны получать меньше кликов, чем блоки рекомендаций REES46, что опровергается данными Google (сайт нарушает закон РФ) Analytics — мы получаем в 2,81 раз больше кликов по виджетам:
Вторая гипотеза, которую мы рассматривали: визуально хорошие рекомендации отвлекают людей от покупки и снижают конверсию. Т.е. притягивают их внимание, но отвлекают от покупок и не способствуют росту продаж.
В этом случае кликнувшие в блоки рекомендаций Retail Rocket будут конвертироваться хуже, чем кликнувшие в блоки REES46. Но по данным Google (сайт нарушает закон РФ) Analytics это не так, конверсия кликнувших в блоки Retail Rocket значительно выше (на 37% по данным за 4 дня):
Таким образом, Retail Rocket значительно чаще рекомендует релевантные пользователю товары, пользователи чаще кликают на эти товары и рекомендации положительно влияют на продажи.
Если проблем с теми, кто взаимодействует с рекомендациями нет, и с визуальной стороны рекомендации выглядят релевантными, остается посмотреть на тех, кто не кликает по рекомендациям.
Исследование аудитории интернет-магазина
Начав исследовать этот сегмент аудитории, мы заметили два интересных факта:
- В сегменте REES46 на несколько процентов больше пользователей, чем в других сегментах, хотя настройки АБ-теста предполагают равномерное распределение аудитории между рекомендательными системами.
- В сегменте REES46 аудитория более лояльная, в ней гораздо больше посетителей, которые приходят на сайт повторно.
Чтобы проверить корректность работы разделения трафика интернет-магазина на сегменты, мы самостоятельно протестировали сегментатор с помощью кода, который использовал сайт: параллельно с основным делением, запустили сегментирование той же аудитории — погрешность получилась минимальной:
- Сегмент 1: 63215 пользователей
- Сегмент 2: 63500 пользователей
- Сегмент 3: 63686 пользователей
Это означает, что сегментатор работает правильно и погрешности в несколько процентов быть не может, т.е. распределение трафика в рамках АБ-теста «Дочки&Сыночки» содержит аномалию.
Наши разработчики детально исследовали код сайта на предмет JS ошибок и багов, которые могли бы влиять на сегментацию, и не нашли ничего, что могло бы вызвать аномалию.
Логичным предположением стала мысль, что пользователи каким-то образом могут перемещаться между сегментами. В нашей практике встречались случаи, когда пользователи меняли сегмент внутри теста, например из-за неправильно заданного времени жизни куки (в одном из магазинов кука, в которую сохраняли идентификатор сегмента АБ-теста, жила только две недели, и если пользователь возвращался по истечении этого времени, ему присваивалось случайно значение — т.е. пользователь мог попасть в другой сегмент теста). Чтобы избежать подобных ситуаций, у нас разработан чек-лист, в котором есть пункт о необходимости убедиться, что пользователь не меняет сегмент в ходе теста.
Для отслеживания подобных ситуаций в Google (сайт нарушает закон РФ) Analytics есть инструмент «Последовательности», который позволяет выделить пользователей, которые сначала были в одном сегменте, а затем перешли в другой. Для анализа мы построили несколько таких сегментов в Google (сайт нарушает закон РФ) Analytics:
И в результате получили такие цифры:
По этим данным четко видно, что в сегмент REES46 из остальных перемещается аномально много пользователей. И это точно не баг, иначе пользователи перемещались бы между всеми сегментами равномерно.
Второй вывод: эти пользователи делают много заказов.
*Интернет-магазин подтвердил, что это настоящие заказы (почти все они имеют статус «выкуплено»)
По номерам заказов пользователей, перемещенных в сегмент REES46, мы исследовали наши внутренние логи сессий и выявили следующие паттерны:
- Почти все пользователи, перемещенные в сегмент REES46, имеют добавление товаров в корзину (т.е. это более лояльная/конверсионная аудитория);
- Перемещения пользователей распределяются по часам неравномерно, это указывает на то, что оно инициируется вручную;
- Перемещения пользователей в сегмент REES46 происходит в те дни, когда Retail Rocket начинает побеждать в АБ тесте:
Перемещение пользователей в сегмент REES46 (сверху часы, слева дни)
Перемещение пользователей в сегмент Retail Rocket (сверху часы, слева дни)
В таблице видно, что 25 и 26 мая перемещений почти нет, а 27 мая, когда система Retail Rocket начинает выходить в плюс — перемещения начинаются снова. И вновь перемещаются пользователи, которые добавляют товар в корзину и скоро сконвертируются в покупателей.
Исследование кода, работающего на сайте
Поскольку перемещение лояльных пользователей в семент REES46 выглядело подозрительно, мы начали искать причину смены сегмента пользователей и изучать код. Мы тщательно исследовали, кто и как работает с куками, не мог ли кто-то случайно что-то сделать для появления подобных ошибок, и ничего подозрительного мы не нашли.
Оставалось два варианта: либо кука изменяется сервером магазина Дочки Сыночки и этого не видно на клиенте, либо динамическим кодом, который приходит с сервера по какому-то запросу.
Проверяя динамический код, мы искали в том числе функцию eval — специальную javascript функцию, которая может выполнять любой текст, к примеру присланный с сервера, как JavaScript код, что в недобросовестных руках позволяет скрыть функциональность кода, но при этом дает полный доступ ко всему окружению сайта.
В ходе проверки наткнулись на странный кусок кода в JS библиотеке REES46:
Кусок кода из JS библиотеки REES46
Весь код доступен по ссылке. Особенность этого куска кода — в нем явно пытаются скрыть его функциональность.
По коду можно сделать несколько выводов:
- Этот фрагмент кода написан специально для магазина ДочкиСыночки, поскольку он скрыто использует куку под именем “city”, принадлежащую магазину (магазин хранит в ней идентификатор региона пользователя)
- Код намеренно написан так, чтобы затруднить его чтение и понимание (вместо текста используются числовые идентификаторы букв)
- Функциональность кода специально скрывается от внешних разработчиков — код не отрабатывает при открытой консоли браузера и для посетителей сайта из Москвы (интернет-магазин должен знать, что он интегрирует к себе на сайт, и какая строчка кода за что отвечает, а здесь — намеренное сокрытие)
- Код предназначен для загрузки картинки с сервера REES46, раскодирования из этой картинки текста, и передаче текста на вход в наивно спрятанную функцию eval (window[t(101) + «val»](u))
- Все это указывает на возможность скрыто выполнить любой код со стороны REES46
Мы предполагаем, что как только это информация будет опубликована, REES46 удалит этот код, поэтому мы сохранили его с помощью двух внешних независимых сервисов: https://web.archive.org и https://www.runscope.com
Его отформатированная версия доступна для исследования по ссылке.
Чтобы понять, что именно делает этот фрагмент, мы написали модуль, который эмулирует действия пользователя и логирует все запросы в сторону сервера REES46. 25 и 26 мая ничего не происходило (это также видно из таблицы с данными о почасовому перемещению пользователей в сторону REES46), а 27 мая, когда по данным Google (сайт нарушает закон РФ) Analytics система Retail Rocket вышла в плюс по АБ тесту, около 7 вечера по московскому времени вновь начались перемещения пользователей в сегмент REES46.
Перемещение пользователей в сегмент REES46 (сверху часы, слева дни)
В это же время мы зафиксировали запросы в сторону сервера REES46 на картинку в формате PNG (содержимое картинки можно посмотреть по ссылке). Просто так картинка не доступна (возвращается ошибка 404), но при передаче в заголовке запроса к картинке сессии пользователя REES46, картинка оказывается доступной для скачивания:
Если картинку передать на вход в код, который пытались закодировать/скрыть, для удобства мы вынесли его отдельно, получается вот такой JS, который изменяет значение куки, где хранится сегмент пользователя АБ теста:
document.cookie="rr-VisitorSegment_Rec=3:2; domain=.dochkisinochki.ru; path=/; expires=Mon, 25 Sep 2017 10:15:20 +0000";document.cookie="DS_SM_rrSegmentRecommendedABC=B; domain=.dochkisinochki.ru; path=/
Этот код явно изменяет две куки, принадлежащих магазину, в которых хранится сегмент пользователя, на значение сегмента равного сегменту REES46.
Мы уверены, что REES46 скроет все следы этой атаки, поэтому картинка так же сохранена запросом независимого стороннего сервиса.
Таким образом, код системы REES46 перемещает в свой сегмент пользователей, которые добавили товар в корзину и вот-вот совершат заказ.
По данным, полученным с момента начала логирования перемещений пользователей (1–28 мая), построенным на основе изначально выданного пользователям сегмента (то есть из этих данных исключены все, кто впервые приходил на сайт до 1 мая), Retail Rocket достоверно побеждает в тесте, а REES46 уменьшает продажи магазина:
Точное окно миграции лояльных пользователей интернет-магазина в сегмент REES46 неизвестно, поэтому разница в эффективности значительно больше.
Кроме того, мы видим признаки других атак на тест в коде REES46, например, при первом посещении сайта их система осуществляет куки матчинг с несколькими RTB-сетями.
Код синхронизации:
Сохраненный запрос можно посмотреть по ссылке на web.archive.org
Запросы синхронизации:
Это как минимум позволяет конкурентам интернет-магазина получить доступ к этим пользователям, и как максимум — вести ретаргетинг на трафик из своего сегмента и уводить трафик из других сегментов теста к конкурентом, снижая конверсию.
Интересный факт, что эта атака REES46 поддерживалась активной PR-кампанией в СМИ и социальных сетях:
Вместо заключения
За без малого 5 лет работы, мы впервые сталкиваемся с подобным поведением. С сожалением надо признать, что АБ тесты можно проводить только при абсолютной уверенности порядочности всех его участников.
Мы считаем такие методы конкуренции недобросовестными и недопустимыми, это наносит ущерб всему сообществу и подрывает доверие к сложившимся практикам работы. В данный момент мы ведем активную работу в правовом поле с целью наказать виновных и призываем сообщество поделиться опытом решения подобных ситуаций.
UPD 23:00 2.06.17. Представители сервиса рекомендаций REES46 прокомментировали исследование следующим образом:
"Немного вводной информации с нашей точки зрения.
В начале теста на магазине Дочки-Сыночки мы обнаружили две вещи:
- Пользователи переходят из сегмента в сегмент.
- В магазине используется версия сегментатора, загружаемая с сервера Retail Rocket, а не с магазина (магазинная перезаписывается серверной).
Напрашивался вывод: Retail Rocket может контролировать сегментацию в свою пользу.
Мы решили провести свое расследование. Для этого сделали функционал, отслеживающий переходы пользователей между сегментами и скрыли его код, чтобы предотвратить его преждевременное раскрытие.
В ходе проверки причина оказалась простой: кука сегмента, проставляемая сегменатором Retail Rocket, удаляется по сроку жизни либо плагинами типа adblock, в итоге сегмент пользователя меняется.
В начале мая Retail Rocket прислал магазину письмо о том, что заказы утекают из их сегмента в наш. Мы сообщили магазину свою точку зрения: про устаревание cookie с сегментом и предоставили отчет с выгрузкой за сутки, где количество переходов между сегментами было примерно одинаково и составляло около 400 переходов за сутки. При посещаемости магазина доля таких переходов незначительна. Представитель магазина информацию принял.
Мы в свою очередь добавили функционал, восстанавливающий исходный сегмент посетителя, если он по какой-либо причине изменился. Алгоритм действий: если у пользователя установлена наша cookie и сохранен сегмент в нашей БД и в очередном запросе к API сегмент изменился, мы восстанавливаем исходное значение сегмента.
На сегодняшний день количество переходов выросло до 1500 в день. Причина также проста: тест длится больше 5 месяцев, куки массово устаревают и очищаются. Функционал их восстанавливает.
В своем доказательстве Retail Rocket приводит в пример код пикселя, который устанавливает сегмент B и это их главный аргумент. В запросе фигурирует код сессии пользователя: 708c0150-c562-4906-8f86-d7a64fa0663a
В нашей базе существует история перехода этого пользователя между сегментами:
- {"s1"=>"3:2", "s2"=>"B", "date"=>"07-03-2017 12:42:05"},
- {"s1"=>"3:3", "s2"=>"C", "date"=>"13-04-2017 19:13:59"},
- {"s1"=>"3:2", "s2"=>"B", "date"=>"13-04-2017 19:14:06"},
- {"s1"=>"3:1", "s2"=>"A", "date"=>"24-05-2017 16:37:05"},
- {"s1"=>"3:2", "s2"=>"B", "date"=>"24-05-2017 16:39:20"},
- {"s1"=>"3:3", "s2"=>"C", "date"=>"24-05-2017 17:00:58"},
- {"s1"=>"3:2", "s2"=>"B", "date"=>"24-05-2017 17:15:01"},
- {"s1"=>"3:3", "s2"=>"C", "date"=>"24-05-2017 17:15:13"},
- {"s1"=>"3:2", "s2"=>"B", "date"=>"26-05-2017 15:43:24"},
- {"s1"=>"3:1", "s2"=>"A", "date"=>"26-05-2017 16:04:58"},
- {"s1"=>"3:2", "s2"=>"B", "date"=>"26-05-2017 16:08:00"},
- {"s1"=>"3:1", "s2"=>"A", "date"=>"26-05-2017 16:08:47"},
- {"s1"=>"3:2", "s2"=>"B", "date"=>"26-05-2017 16:40:38"},
Если менять этому пользователю сегмент, он будет восстановлен в сегмент B.
Если не менять пользователю сегмент, пиксель не работает.
То же самое можно сделать с любым другим сегментом. Досточно зайти в режиме инкогнито на сайт, получить куку с сегментом, изменить ее на другое значение, пройтись еще раз по сайту и сегмент восстановится в исходный. Независимо, каким был исходный: A, B или C. Этот функционал будет работать на сайте еще пару дней, чтобы вы могли его проверить. Потом мы его отключим.
Пример для сессии: aebbf2c3-4e20-4a43-8091-9eca853bc577
- {"s1"=>"3:3", "s2"=>"C", "date"=>"16-05-2017 08:33:12"},
- {"s1"=>"3:2", "s2"=>"B", "date"=>"16-05-2017 08:37:43"},
- {"s1"=>"3:3", "s2"=>"C", "date"=>"16-05-2017 10:47:00"},
- {"s1"=>"3:1", "s2"=>"A", "date"=>"16-05-2017 17:21:28"},
- {"s1"=>"3:3", "s2"=>"C", "date"=>"16-05-2017 23:00:40"}
Началось с С и закончилось им же".