Перейти к содержанию

action_script — PHP-скрипт на потоке

Когда декларативного конструктора (фильтры, веса, схемы) не хватает, поток со схемой action script отдаёт решение о маршрутизации вашему PHP-коду, который исполняется на каждом клике.

Как включить

PHP-рантайм включён по умолчанию при установке — если вы не ставили с --no-php, всё уже работает.

  1. В редакторе потока выберите схему action script (PHP).
  2. Напишите скрипт.
  3. Кнопка ▶ Test прогоняет его на синтетическом клике прямо в редакторе: покажет решение, изменённые sub'ы и cost, вывод echo и ошибки с номером строки. Страну, устройство и sub'ы для тестового клика задают рядом с кнопкой. Отлаживаться на живом трафике не нужно.

Если PHP выключен, скрипт сохранится, но исполняться не будет: клик пойдёт обычной маршрутизацией, а в трейсе на фазе action_script будет ошибка script: engine disabled (PHP_FPM_ADDR not set). Кнопка ▶ Test в этом случае отвечает «PHP-FPM не настроен». Включить PHP позже: install.sh update --with-php.

Правила исполнения

  • Скрипт пишется без <?php (с тегом тоже можно — он отбрасывается).
  • Бюджет на клике — 100 мс. Обычный скрипт укладывается в 1–5 мс. Сетевых вызовов (curl, file_get_contents наружу) не делайте: не успеют. У Test-прогона бюджет больше — 2 секунды, чтобы холодная компиляция не читалась как таймаут.
  • Память — 128 МБ на исполнение: потолок задаёт пул php-fpm (php_admin_value[memory_limit] = 128M в zz-minik.conf), и ini_set() из скрипта его не меняет.
  • Одновременно исполняется не больше 8 скриптов. Девятый клик ждёт свободный слот не дольше 10 мс и, если тот не освободился, уходит обычной маршрутизацией.
  • Fail-open: ошибка, таймаут, переполнение или недоступный PHP-FPM никогда не роняют клик — он маршрутизируется как обычный поток. Текст ошибки виден в Traffic log, фаза action_script.
  • Синтаксис проверяется при сохранении потока и перед каждым Test-прогоном. Сохранение отклоняет только явную синтаксическую ошибку; если PHP-FPM недоступен, поток сохранится без проверки.

$click — контекст клика

Массив, только чтение: он приходит в скрипт копией, и записи прямо в $click до трекера не доходят — для этого есть set_sub() и set_cost().

Ключи Что в них
click_id, visitor_id идентификаторы клика и визитора
subs все sub-параметры: $click['subs']['sub_id_5']. Там же лежат и остальные ключи query-строки под своими именами
ip, user_agent, language сетевые атрибуты
country, region, city, isp гео (страна — ISO-код)
device, os, os_version, browser, browser_version, connection_type устройство
referrer, keyword, search_engine источник перехода
campaign_id, campaign_name, flow_id, flow_name, source_id сущности трекера
cost, currency стоимость клика до override
is_bot, is_unique, is_proxy, is_tor, is_ipv6, is_datacenter, is_vpn флаги (bool)
now время клика в UTC, RFC3339 (у date() внутри скрипта — часовой пояс сервера)

Функции

Функция Действие
set_url(string $url, $type = '302') редирект. В URL работают {макросы} трекера — раскроются после скрипта
send(int $status = 204) ответить голым статусом без редиректа (404/403/204). Значение вне 200–599 заменяется на 204
set_sub(string $name, $value) перезаписать sub до записи клика в статистику
set_cost($cost) перезаписать стоимость клика (приводится к float)
sub(string $name, string $default = '') шорткат чтения $click['subs'][$name]
echo / print вывод попадает в лог Test-прогона и в трейс клика

$type у set_url() — любой из типов редиректа потока: 301 302 303 307 meta double_meta js iframe form direct no_referrer curl (remote — синоним curl). Что делает каждый — в таблице типов редиректа. Неизвестное значение игнорируется: останется тип, заданный в настройках потока.

set_sub() принимает любое имя, но в колонки клика пишутся только sub_id_1sub_id_30. Произвольное имя доживёт до макросов в URL назначения — в отчёты оно не попадёт.

Если ни set_url, ни send не вызваны — скрипт «решения не принял»: клик маршрутизируется лендингами и офферами потока как при обычной схеме, а сделанные set_sub / set_cost всё равно применяются.

send() и set_url() не останавливают скрипт

Обе функции только записывают решение — код после них продолжает выполняться, и следующий вызов перезапишет предыдущий. Чтобы выйти сразу, ставьте return;. Именно return, а не exit/die: те обрывают ответ PHP, трекер не сможет его разобрать и клик уйдёт обычной маршрутизацией.

Примеры

Подпись URL (HMAC), которую не собрать макросами:

$sig = hash_hmac('sha256', $click['click_id'] . '|' . sub('sub_id_1'), 'MY_SECRET');
set_url('https://network.example.com/offer?cid=' . $click['click_id'] . '&sig=' . $sig);

Детерминированный сплит — визитор всегда попадает на свой вариант:

$bucket = crc32($click['visitor_id']) % 100;
if ($bucket < 50) {
    set_url('https://offer-a.example.com/?cid=' . $click['click_id']);
} else {
    set_url('https://offer-b.example.com/?cid=' . $click['click_id'], 'js');
}

Роутинг по времени плюс чистка мусора:

if ($click['is_bot'] || $click['is_datacenter']) {
    send(404);                       // боты и хостинги — мимо
    return;                          // без return скрипт пойдёт дальше
}
$hour = (int)date('H');              // часовой пояс сервера
set_url($hour >= 22 || $hour < 6
    ? 'https://night-offer.example.com/?cid=' . $click['click_id']
    : 'https://day-offer.example.com/?cid=' . $click['click_id']);

Нормализация sub'ов без смены маршрута:

set_sub('sub_id_10', strtolower(substr(sub('sub_id_2', 'na'), 0, 16)));
if (sub('sub_id_3') === '') {
    set_sub('sub_id_3', 'organic');
}

Перенос скриптов с других трекеров

Язык тот же (PHP 8.3) — hash(), hash_hmac(), строковые и массивные функции переносятся как есть. Отличается только API трекера.

Классический трекер Minik
$rawClick->getCountry() и хелперы потока $click['country'], $click['device'], …
getSubId(N) / токены {subN} sub('sub_id_N')
смена действия потока (URL) set_url($url, $type)
«показать 404» / блок-действия send(404)
правка токенов клика set_sub($name, $value)
правка стоимости set_cost($cost)
file_get_contents / curl наружу не используйте: бюджет 100 мс, fail-open
KV-хранилище не предусмотрено — держите состояние в sub'ах или снаружи

Ограничения

  • Скрипты исполняются отдельно от трекинг-движка, в контейнере php-fpm: зависший скрипт не останавливает приём трафика.
  • Пропускная способность ограничена числом PHP-воркеров: один воркер ≈ 1000 мс / среднее время скрипта кликов в секунду. Инсталлятор ставит pm.max_children = 16 в deploy/php/zz-minik.conf — при заметном трафике на скриптовые потоки его можно поднять. Потолок в 8 параллельных исполнений на стороне трекера при этом не изменится: он зашит в коде и настройке не поддаётся.
  • Скрипт исполняется на браузерном клике (/r/{alias}, он же /click/{alias}). Click API и MClient action-потоки не исполняют — там клик маршрутизируется лендингами и офферами потока.

Изоляция

Скрипт — это PHP на вашем сервере, а не песочница для чужого кода: схему action script доверяйте только тем, кому доверяете сервер. Правка требует права campaigns:edit и фиксируется в audit-логе.

Что ограничено штатной конфигурацией:

  • open_basedir на каждый запрос сужается до каталога этого потока — соседние потоки и лендинги других воркспейсов скрипту не видны;
  • запуск процессов отключён: exec, passthru, shell_exec, system, proc_open, popen, pcntl_exec, pcntl_fork;
  • контейнер php-fpm живёт в отдельной docker-сети и не достаёт до Postgres, Redis, ClickHouse и Centrifugo;
  • request_terminate_timeout = 5s в пуле — страховка поверх бюджета клика.

Не редактируйте zz-minik.conf наугад

Эти ограничения — граница RCE для пользовательского PHP (скрипты и .php-лендинги). Ослабив их, вы отдаёте сервер автору скрипта.