Перейти к основному содержимому

Эксперименты при серверном рендеринге (SSR)

Краткое содержание

Когда нужен серверный вызов Серверный вызов необходим, если страница собирается на сервере, чтобы вариант попал в первый HTML-ответ. Он не заменяет клиентский сниппет: сервер отвечает за рендер, а сниппет — за отнесение событий к эксперименту. Контракт универсален и подходит для любого языка, способного отправить HTTP-запрос и установить cookie.

Значения контракта Эндпоинт для запроса — https://api.a.mts.ru/api/splta/experiments (метод GET, авторизация не требуется). Таймаут запроса составляет 250 мс. Кэш ответа хранится не более 60 секунд по ключу flowId + clientId. Cookie ma_exp_cid устанавливается на общий домен сайта с путем / и сроком жизни 1 год. Значение cookie ограничено 20 символами, формат clientId — строка из символов [0-9a-z].

Запрос Запрос отправляется на указанный эндпоинт с параметром flowId (обязательный UUID) и опциональным clientId из cookie ma_exp_cid. На первом визите clientId передавать не нужно. Авторизация и дополнительные заголовки не требуются.

Ответ Штатный код ответа — 200. В теле ответа всегда приходит clientId, объект features с флагами варианта (значения всегда строки) и массив variants (для серверного кода не нужен). Значения флагов нужно экранировать при подстановке в HTML. Рендер страницы осуществляется по features. Код 400 означает ошибку настройки (flowId не передан или неверен); в этом случае, как и при таймауте, рендерится базовый вариант без установки cookie.

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

Cookie ma_exp_cid Cookie хранит идентификатор пользователя, чтобы вариант не менялся от визита к визиту. Атрибуты: имя ma_exp_cid, Domain — общий домен сайта (без зоны), Path/, Max-Age — 31536000. В cookie записывается ровно значение clientId без изменений. Cookie ставится при каждом ответе 200, включая пустой. При таймауте или ошибке cookie не ставится. На localhost атрибут Domain не задается. Не следует использовать SameSite=Strict или HttpOnly.

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

Таймаут, деградация и кэш Таймаут запроса должен составлять 250 мс. При таймауте рендерится базовый вариант, cookie не ставится, код ответа страницы — 200. Повторные запросы внутри обработки страницы не допускаются. Рекомендуется использовать постоянные соединения (keep-alive). Кэш ответа по flowId + clientId должен жить не дольше 60 секунд. Кэшируются только успешные ответы 200. Ответы без clientId не кэшируются. Страницы с вариантом не должны отдаваться из общего кэша (CDN/proxy) без учета cookie ma_exp_cid в ключе.

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

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

Пример обработчика на Node.js В тексте вы найдете руководство по созданию минимального сервера на Node.js 20+, демонстрирующее полный цикл: чтение clientId из cookie, запрос к API, рендеринг варианта или базовой страницы, установку cookie и обработку ошибок. Пример не включает реализацию кэша и пула соединений, которые необходимо добавить согласно разделу о таймаутах.

Чеклист интеграции В статье приведена пошаговая инструкция по проверке корректности интеграции, включающая следующие пункты:

  • сверка flowId с интерфейсом МТС Аналитики;
  • отсутствие передачи clientId на первом визите;
  • настройка таймаута 250 мс;
  • установка cookie при каждом ответе 200;
  • соответствие атрибутов cookie таблице;
  • сохранение значения clientId без изменений;
  • корректная обработка таймаутов и ошибок (базовый вариант, без cookie и сниппета);
  • наличие сниппета на страницах с вариантом и при пустом ответе;
  • включение keep-alive;
  • настройка кэша на 60 секунд с правильным ключом;
  • исключение страниц с вариантом из общего кэша без учета cookie;
  • проведение тестового эксперимента со 100% аллокацией.

Куда сообщать о проблемах По вопросам интеграции и расхождениям с описанием обращайтесь на почту analytics.support@mts.ru.

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

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

Когда нужен серверный вызов

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

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

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

Контракт не зависит от стека: подойдёт любой язык и фреймворк, который умеет отправить HTTP-запрос и поставить cookie. В конце раздела есть пример обработчика на Node.js.

Значения контракта

ПараметрЗначение
Эндпоинтhttps://api.a.mts.ru/api/splta/experiments
МетодGET
Авторизацияне требуется
Таймаут запроса250 мс
Срок жизни кэша ответане более 60 секунд, ключ flowId + clientId
Имя cookiema_exp_cid
Domainобщий домен сайта, например example.ru
Path/
Max-Age31536000
Предел длины значения cookie20 символов
Формат clientIdстрока из символов [0-9a-z], сейчас 12 символов, длина не гарантирована

Запрос

GET https://api.a.mts.ru/api/splta/experiments

ПараметрТипОбязательностьОписание
flowIdUUIDобязательныйИдентификатор потока. Тот же, что вы подставляете в сниппет
clientIdстроканеобязательныйЗначение cookie ma_exp_cid, если она есть. На первом визите не передавайте
curl -s 'https://api.a.mts.ru/api/splta/experiments?flowId=<FLOW_ID>&clientId=<CLIENT_ID>'

Авторизация не нужна, заголовки не требуются.

Ответ

Штатный код ответа — 200.

ПолеТипОписание
clientIdстрокаИдентификатор пользователя для экспериментов. Приходит всегда
featuresобъектФлаги варианта в формате «ключ — значение». Значения всегда строки
variantsмассив чиселИдентификаторы вариантов. Серверному коду не нужны
{
"clientId": "dbffogcgwkxe",
"features": {
"background_color": "#FFFFFF",
"new_checkout": "true"
},
"variants": [851803422999497]
}

Значения флагов приходят строками, в том числе "true" и "1". Приведение типов данных происходит на стороне проекта. Экранируйте значения при подстановке в HTML — они приходят из настроек эксперимента, а не из вашего кода.

Рендерите страницу по features. Поле variants серверному коду не нужно: события к эксперименту относит клиентский сниппет.

Код 400 приходит в двух случаях:

  1. параметр flowId не передан;
  2. его значение не в формате UUID.

Это ошибка настройки интеграции — исправьте параметр запроса. До исправления обрабатывайте 400 так же, как таймаут: базовый вариант, cookie не ставить. Неизвестный, но синтаксически верный flowId возвращает 200 с пустым ответом, см. Пустой ответ. Любой другой код ответа обрабатывайте так же, как таймаут: базовый вариант, cookie не ставить.

Пустой ответ

Ответ, в котором features и variants пустые, — штатный, а не признак ошибки:

{ "clientId": "dkcr432jjqqk", "features": {}, "variants": [] }

Он приходит в трёх случаях:

  1. Пользователь не участвует в экспериментах.
  2. У потока нет активных экспериментов — эксперимент на паузе или завершён.
  3. flowId неизвестен или указан с опечаткой.

По ответу эти случаи не различаются, и различать их не нужно: реакция во всех трёх одна — отрендерите базовый вариант и всё равно поставьте cookie со значением clientId из ответа.

Важно

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

Проверять, что flowId указан верно, нужно на этапе настройки, а не во время работы сайта: сверьте идентификатор с потоком в интерфейсе МТС Аналитики и прогоните тестовый эксперимент со 100 % аллокацией — непустой ответ подтвердит, что идентификатор верный.

Cookie хранит идентификатор пользователя для экспериментов, поэтому вариант не меняется от визита к визиту. При серверной интеграции её ставит ваш сервер. Эта же cookie входит в общий перечень cookie МТС Аналитики, см. Куки, устанавливаемые МТС Аналитикой — значения совпадают.

АтрибутЗначение
Имяma_exp_cid
Domainобщий домен сайта, например example.ru
Path/
Max-Age31536000

В Domain укажите домен, на который зарегистрирован сайт: для shop.example.ru это example.ru. Не указывайте доменную зону (ru, spb.ru, com.ru) — браузер отклонит такую cookie, и пользователь будет получать новый clientId на каждый визит.

Set-Cookie: ma_exp_cid=<clientId>; Path=/; Domain=example.ru; Max-Age=31536000

Правила:

  • сохраняйте в cookie ровно то значение, которое пришло в поле clientId: без префиксов, без обёрток, без URL-кодирования;
  • сейчас clientId не длиннее 20 символов, и на это рассчитан клиентский сниппет: он читает из cookie только первые 20 символов. Если значение окажется длиннее, сервер и браузер будут работать с разными идентификаторами и пользователь попадёт в разные группы — поставьте значение как есть и сообщите нам, см. Ограничения clientId;
  • ставьте cookie при каждом ответе 200, в том числе при пустом ответе;
  • при таймауте или ошибке cookie не ставьте. Уже существующую cookie не удаляйте и не перезаписывайте;
  • на localhost и при обращении по IP-адресу атрибут Domain не задавайте;
  • задавайте только атрибуты из таблицы. Не задавайте SameSite=Strict: при переходе с внешнего ресурса cookie не отправится и пользователь получит новый clientId; не задавайте HttpOnly: клиентский сниппет читает cookie из браузера и не увидит её;
  • клиентский сниппет записывает эту же cookie с этим же значением — это нормально. Важно, чтобы совпадали имя, домен и путь, иначе в браузере окажутся две разные cookie.

Ограничения clientId

Соблюдайте правила работы с clientId:

  • clientId генерирует только сервис экспериментов. Не подставляйте собственный идентификатор: проверки формата на входе нет, поэтому вместо ошибки вы получите собственную схему разбиения пользователей на варианты;
  • считайте значение непрозрачным: не разбирайте его, не обрезайте, не меняйте регистр;
  • не рассчитывайте на конкретную длину — она может измениться. Единственное ограничение, на которое можно опираться, — предел в 20 символов;
  • если значение clientId длиннее 20 символов, поставьте его без изменений и сообщите нам на analytics.support@mts.ru: это признак изменения на нашей стороне. Не обрезайте значение сами;
  • не используйте clientId как признак авторизации или как идентификатор пользователя в своих системах.

Таймаут, деградация и кэш

Настройте запрос и обработку сбоев так:

  • установите таймаут запроса 250 мс;
  • при таймауте отрендерите базовый вариант и не ставьте cookie;
  • отдавайте страницу с кодом 200: сбой получения конфигурации не должен превращаться в ошибку страницы;
  • не повторяйте запрос внутри обработки страницы: бюджет 250 мс не вмещает вторую попытку;
  • держите к api.a.mts.ru постоянные соединения — keep-alive. Иначе на каждый запрос уходит установка TLS-соединения, а она съедает заметную часть бюджета;
  • кэшируйте ответ по паре flowId + clientId не дольше 60 секунд. Ответ для одного и того же clientId не меняется, пока не изменены настройки эксперимента, а изменения настроек применяются в пределах минуты. Более длинный кэш добавит расхождение между тем, что видит пользователь, и настройками эксперимента;
  • кэшируйте только ответы 200: ошибки и таймауты не кэшируйте, иначе сбой продлится на весь срок жизни кэша;
  • ответы на запросы без clientId не кэшируйте: у такого запроса нет ключа кэширования, и разные посетители получили бы один и тот же идентификатор;
  • страницу, отрендеренную по флагам, не отдавайте из общего кэша без учёта cookie ma_exp_cid в ключе кэширования: иначе все посетители получат один вариант и один идентификатор. Общий кэш — это CDN и обратный прокси перед вашим сервером.

Клиентский сниппет на SSR-странице

  1. На странице, отрендеренной по флагам, оставьте клиентский сниппет. Без него сервис не отнесёт события к эксперименту.
  2. На странице, отрендеренной базовым вариантом после таймаута или любого ответа, кроме 200, сниппет не подключайте. Иначе сервис засчитает визит варианту, который пользователь не видел, и наблюдаемый эффект эксперимента окажется ниже реального.
  3. На странице, отрендеренной базовым вариантом из-за пустого ответа, сниппет оставьте: cookie вы поставили, и сервис экспериментов уже учёл пользователя.

Пример обработчика на Node.js

Минимальный сервер на Node.js 20 и новее. Подставьте свой идентификатор потока в FLOW_ID и общий домен сайта в COOKIE_DOMAIN. На localhost и при обращении по IP-адресу атрибут Domain не задавайте — см. Cookie ma_exp_cid.

import { createServer } from 'node:http';

const FLOW_ID = '<FLOW_ID>';
const API_URL = 'https://api.a.mts.ru/api/splta/experiments';
const TIMEOUT_MS = 250;
const COOKIE_NAME = 'ma_exp_cid';
const COOKIE_DOMAIN = 'example.ru';
const COOKIE_MAX_AGE = 31536000;

function readClientId(request) {
const cookies = request.headers.cookie ?? '';
const prefix = `${COOKIE_NAME}=`;
const found = cookies.split(/;\s*/).find((cookie) => cookie.startsWith(prefix));
return found ? found.slice(prefix.length) : undefined;
}

async function loadExperiments(clientId) {
const url = new URL(API_URL);
url.searchParams.set('flowId', FLOW_ID);
if (clientId) {
url.searchParams.set('clientId', clientId);
}

const response = await fetch(url, { signal: AbortSignal.timeout(TIMEOUT_MS) });
if (!response.ok) {
throw new Error(`Сервис экспериментов ответил ${response.status}`);
}
return response.json();
}

function render(features) {
const color = features.background_color ?? '#FFFFFF';
return `<!doctype html><html><body style="background:${color}">Вариант</body></html>`;
}

function renderBaseline() {
return '<!doctype html><html><body>Базовый вариант</body></html>';
}

createServer(async (request, response) => {
try {
const experiments = await loadExperiments(readClientId(request));
if (!experiments.clientId) {
throw new Error('Ответ без clientId');
}

// Тело собираем до отправки заголовков: иначе сбой рендера оставит
// ответ с уже отправленными заголовками и без тела
const body = render(experiments.features);

response.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8',
'Set-Cookie':
`${COOKIE_NAME}=${experiments.clientId}; Path=/; ` +
`Domain=${COOKIE_DOMAIN}; Max-Age=${COOKIE_MAX_AGE}`,
});
response.end(body);
} catch {
// Таймаут, ошибка сервиса или неполный ответ: базовый вариант, cookie не ставим
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
response.end(renderBaseline());
}
}).listen(3000);

При пустом ответе функция render вернёт базовый вид: отдельная ветка для этого случая не нужна. Cookie при этом ставится по общему правилу — см. Пустой ответ.

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

Чеклист интеграции

  • flowId сверен с идентификатором потока в интерфейсе МТС Аналитики
  • на первом визите clientId не передаётся
  • таймаут запроса 250 мс
  • cookie ставится при каждом ответе 200, включая пустой
  • атрибуты cookie совпадают с таблицей: имя, Domain, Path, Max-Age
  • значение cookie — ровно то, что пришло в clientId: без обрезки, префиксов и URL-кодирования
  • при таймауте и любом ответе, кроме 200, рендерится базовый вариант, cookie не ставится, сниппет не подключается
  • на страницах с вариантом и на базовых страницах из-за пустого ответа клиентский сниппет присутствует
  • включён keep-alive к api.a.mts.ru
  • кэш ответа живёт не дольше 60 секунд, ключ flowId + clientId
  • страницы с вариантом не раздаются из общего кэша без учёта cookie ma_exp_cid
  • проведён тестовый эксперимент со 100 % аллокацией: ответ непустой, вариант виден в первом HTML

Куда сообщать о проблемах

По вопросам интеграции и при расхождениях с этим описанием обращайтесь на почту поддержки analytics.support@mts.ru.