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

JS-трекер

Готовый скрипт отправляет с вашего сайта визиты страниц и достижения целей. Писать отправку в браузере не нужно — остаётся поднять на своём сервере приёмник: он добавит ключ коннектора и перешлёт событие в Октопус.

Что умеет трекер в первой версии

Визиты страниц и цели, которые вы вызываете функцией oct_goal(). Корзину по-прежнему передаёт ваш сервер через /tracking/basket-sync. Автоматической привязки целей к кнопкам и формам пока нет.


Как это устроено

браузер (tracker.js)  →  ваш приёмник  →  api.octopuscdp.ru

Трекер не обращается к Октопусу напрямую. Причин две:

  • ключ коннектора нельзя отдавать в браузер — с ним любой посетитель сможет записывать данные в ваш проект. Ключ хранится только в приёмнике;
  • браузеру нельзя доверять, кто посетитель. Идентификатор клиента, отправленный из браузера, легко подменить. Приёмник определяет клиента сам — по авторизации на вашем сайте, по своей cookie или по метке utm_oct из письма.

Подключение

Добавьте на все страницы сайта:

<script src="https://api.octopuscdp.ru/tracker/v1.js" data-endpoint="/octopus-collect.php" async></script>
АтрибутОбязательноОписание
srcдаАдрес трекера. Версия v1 в адресе не меняется: исправления выкатываются в неё же, а несовместимые изменения выйдут только в v2
data-endpointдаАдрес вашего приёмника на этом же сайте. Без него трекер ничего не отправляет и пишет предупреждение в консоль браузера
asyncнетСкрипт загружается, не задерживая страницу. Рекомендуется

Требования и особенности:

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

Цели

Вызывайте oct_goal() там, где посетитель совершает нужное действие:

oct_goal('callback_form');                          // по коду цели
oct_goal(12); // по номеру цели
oct_goal('callback_form', { form: 'callback' }); // с дополнительными данными
  • идентификатор из одних цифр считается номером цели, любой другой — кодом. Номер цель получает при создании, код задаётся в её настройках — оба видны в кабинете;
  • дополнительные данные передаются объектом и сохраняются вместе с достижением цели. Поля карточки клиента через них не обновляются: ключ update, как в счётчике 1С-Битрикс, трекер сохранит как обычные данные. Поля клиента передавайте со своего сервера через /users/sync;
  • цель, отправленная при переходе по ссылке или при отправке формы, доходит, даже если браузер уже уходит со страницы.

Если сайт может вызвать oct_goal() раньше, чем загрузится трекер, добавьте в <head> одну строку. Вызовы до загрузки встанут в очередь и уйдут, как только трекер загрузится:

<script>window.oct_goal=window.oct_goal||function(){(window.oct_goal.q=window.oct_goal.q||[]).push(arguments)};</script>

Что трекер отправляет приёмнику

POST на адрес из data-endpoint, тело — JSON, заголовки Content-Type: application/json и X-Octopus-Tracker: 1.

Визит:

{"action": "pageview", "page_url": "https://example.ru/catalog/?utm_source=yandex", "referrer": "https://yandex.ru/"}

Цель:

{"action": "goal", "goal": "callback_form", "data": {"form": "callback"}, "page_url": "https://example.ru/contacts/"}
ПолеОписание
actionpageview — визит, goal — цель
page_urlПолный адрес страницы, на которой находится посетитель
referrerТолько у визита: откуда посетитель пришёл на страницу, может быть пустым
goalТолько у цели: номер или код цели, всегда строкой
dataТолько у цели, необязательно: дополнительные данные объектом

Трекер отправляет только то, чего ваш сервер знать не может, — где находится посетитель. Остальное приёмник берёт сам.


Что делает приёмник

  1. Проверяет, что запрос пришёл от трекера с вашей страницы: есть заголовок X-Octopus-Tracker: 1, а Origin, если он передан, — ваш домен. Браузер не позволит чужому сайту выставить такой заголовок без разрешения приёмника, поэтому подделать событие со стороннего сайта не выйдет.
  2. Проверяет, что page_url — адрес вашего сайта.
  3. Определяет сессию и клиента сам: авторизация на вашем сайте → своя cookie с подписью → метка utm_oct в адресе страницы. Из браузера идентификатор клиента не принимается никогда.
  4. Берёт User-Agent и IP-адрес из своего запроса, а DOMAIN, PAGE_PATH и QUERY_PARAMS вычисляет из page_url.
  5. Отсеивает ботов.
  6. Добавляет ключ коннектора и пересылает событие в /tracking/pageview или /goals/{id}/hit.

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


Готовый приёмник на PHP

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

Как подключить:

  1. Сохраните код в файл octopus-collect.php в корне сайта — тогда адрес приёмника /octopus-collect.php, как в примере подключения выше.
  2. Впишите в OCTOPUS_KEY ключ своего коннектора.
  3. Если ваш коннектор не основной в проекте, впишите в OCTOPUS_UTM_KEY ключ основного: метка utm_oct в письмах подписана им.
  4. Замените тело функции currentUserId() на проверку авторизации вашего сайта. Она должна возвращать тот же ID клиента, что вы передаёте в /users/sync.
<?php
/**
* Приёмник JS-трекера Октопус CDP.
*
* Принимает события от https://api.octopuscdp.ru/tracker/v1.js, сам определяет
* сессию и клиента и пересылает событие в Октопус с ключом коннектора.
* Ключ хранится только здесь, на сервере, — в браузер он не попадает.
*/

// Ключ вашего коннектора
const OCTOPUS_KEY = 'oct-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
// Ключ ОСНОВНОГО коннектора проекта — им подписана метка utm_oct в письмах.
// Если ваш коннектор основной, это тот же ключ
const OCTOPUS_UTM_KEY = OCTOPUS_KEY;
const OCTOPUS_API = 'https://api.octopuscdp.ru';

/**
* Идентификатор клиента, если он вошёл в аккаунт на вашем сайте.
* Тот же ID, что вы передаёте в /users/sync. Замените на свою проверку авторизации.
*/
function currentUserId(): ?string
{
return isset($_SESSION['user_id']) ? (string)$_SESSION['user_id'] : null;
}

// ---------------------------------------------------------------------------

function reply(int $status): void
{
http_response_code($status);
exit;
}

function base64UrlEncode(string $v): string
{
return rtrim(strtr(base64_encode($v), '+/', '-_'), '=');
}

function base64UrlDecode(string $v): ?string
{
$d = base64_decode(strtr($v, '-_', '+/'), true);
return $d === false ? null : $d;
}

/** Разбор метки utm_oct: возвращает идентификатор клиента или null. */
function parseUtmOct(string $token): ?string
{
$parts = explode('.', $token);
if (count($parts) !== 3) {
return null;
}
[$rawId, $exp, $sig] = $parts;
if (!ctype_digit($exp) || time() > (int)$exp) {
return null;
}
if (!hash_equals(hash_hmac('sha256', "$rawId.$exp", OCTOPUS_UTM_KEY), $sig)) {
return null;
}
if (strpos($rawId, '~') === 0) {
$id = base64UrlDecode(substr($rawId, 1));
return ($id === null || $id === '') ? null : $id;
}
return ctype_digit($rawId) ? $rawId : null;
}

/** Опознанный посетитель хранится в cookie с подписью — подделать её нельзя. */
function rememberUser(string $id): void
{
$value = base64UrlEncode($id) . '.' . hash_hmac('sha256', 'oct_uid|' . $id, OCTOPUS_KEY);
setcookie('oct_uid', $value, [
'expires' => time() + 365 * 86400,
'path' => '/',
'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
'httponly' => true,
'samesite' => 'Lax',
]);
}

function rememberedUser(): ?string
{
$parts = explode('.', $_COOKIE['oct_uid'] ?? '');
if (count($parts) !== 2) {
return null;
}
$id = base64UrlDecode($parts[0]);
if ($id === null || $id === '') {
return null;
}
return hash_equals(hash_hmac('sha256', 'oct_uid|' . $id, OCTOPUS_KEY), $parts[1]) ? $id : null;
}

function isBot(string $ua): bool
{
return $ua === '' || (bool)preg_match('/bot|crawl|spider|slurp|headless|preview|monitor/i', $ua);
}

// --- 1. Запрос пришёл от трекера с нашей же страницы ------------------------

if ($_SERVER['REQUEST_METHOD'] !== 'POST' || ($_SERVER['HTTP_X_OCTOPUS_TRACKER'] ?? '') !== '1') {
reply(403);
}

$host = strtolower(explode(':', $_SERVER['HTTP_HOST'] ?? '')[0]);

$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if ($origin !== '' && strtolower((string)parse_url($origin, PHP_URL_HOST)) !== $host) {
reply(403);
}
if (isset($_SERVER['HTTP_SEC_FETCH_SITE']) && $_SERVER['HTTP_SEC_FETCH_SITE'] !== 'same-origin') {
reply(403);
}

$raw = file_get_contents('php://input', false, null, 0, 16385);
if (strlen($raw) > 16384) {
reply(413);
}
$event = json_decode($raw, true);
if (!is_array($event)) {
reply(400);
}

// --- 2. Страница — наша ------------------------------------------------------

$pageUrl = (string)($event['page_url'] ?? '');
$page = parse_url($pageUrl);
if (strlen($pageUrl) > 2048 || !in_array($page['scheme'] ?? '', ['http', 'https'], true)
|| strtolower($page['host'] ?? '') !== $host) {
reply(400);
}

$userAgent = substr((string)($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 1024);
if (isBot($userAgent)) {
reply(204);
}

// --- 3. Сессия и клиент — определяем сами, браузеру не доверяем -------------

session_start();

parse_str($page['query'] ?? '', $query);

$userId = currentUserId();
if ($userId === null && !empty($query['utm_oct']) && is_string($query['utm_oct'])) {
$userId = parseUtmOct($query['utm_oct']);
if ($userId !== null) {
rememberUser($userId);
}
}
if ($userId === null) {
$userId = rememberedUser();
}

$sessId = session_id();

// --- 4. Событие для Октопуса -------------------------------------------------

$action = $event['action'] ?? '';

if ($action === 'pageview') {
// Повтор той же страницы в сессии не считаем — как в модуле 1С-Битрикс
if (($_SESSION['oct_last_page'] ?? '') === $pageUrl) {
reply(204);
}
$_SESSION['oct_last_page'] = $pageUrl;

$path = '/tracking/pageview';
$body = [
'SESSID' => $sessId,
'PAGE_URL' => $pageUrl,
'DOMAIN' => $page['host'],
'PAGE_PATH' => $page['path'] ?? '/',
'PROTOCOL' => $page['scheme'],
'QUERY_PARAMS' => $query,
'REFERRER' => substr((string)($event['referrer'] ?? ''), 0, 2048),
'USER_AGENT' => $userAgent,
'IP_ADDRESS' => $_SERVER['REMOTE_ADDR'] ?? '',
'DATETIME' => date('Y-m-d H:i:s'),
];
} elseif ($action === 'goal') {
$goal = (string)($event['goal'] ?? '');
if ($goal === '' || strlen($goal) > 255) {
reply(400);
}
$data = is_array($event['data'] ?? null) ? $event['data'] : [];
$data['current_url'] = $pageUrl;

$path = ctype_digit($goal) ? "/goals/$goal/hit" : '/goals/0/hit';
$body = ['sessid' => $sessId, 'additional_data' => $data];
if (!ctype_digit($goal)) {
$body['goal_code'] = $goal;
}
} else {
reply(400);
}

if ($userId !== null) {
$body[$action === 'pageview' ? 'USER_ID' : 'user_id'] = $userId;
}

// --- 5. Отвечаем браузеру сразу, пересылаем после ---------------------------

session_write_close();
http_response_code(204);
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
}

$ch = curl_init(OCTOPUS_API . $path);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($body, JSON_UNESCAPED_UNICODE),
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . OCTOPUS_KEY, 'Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
]);
$response = curl_exec($ch);
$result = json_decode((string)$response, true);
if ($response === false || empty($result['success'])) {
// В рабочей версии — положить событие в свою очередь и повторить позже
error_log('Октопус CDP: событие не принято — ' . ($response === false ? curl_error($ch) : $response));
}
curl_close($ch);

Требования и особенности:

  • сессия берётся из стандартной сессии PHP. Если у сайта свой механизм сессий, передавайте в SESSID его идентификатор — главное, чтобы он был одинаковым для всех событий одного посетителя;
  • опознанного по метке посетителя приёмник запоминает в cookie oct_uid с подписью ключом коннектора: подменить в ней идентификатор нельзя, подпись перестанет сходиться;
  • при сбое отправки пример только пишет ошибку в журнал. В рабочей версии стоит складывать событие в свою очередь и повторять — см. «Пакетная отправка».

Проверка подключения

  1. Откройте сайт и инструменты разработчика браузера, вкладка Network.
  2. Найдите запрос к адресу приёмника. Ответ 204 — событие принято.
  3. Перейдите на сайт по ссылке из тестовой рассылки: визит должен появиться в карточке клиента.
Ответ приёмникаЧто означает
204Событие принято. Боты тоже получают 204, но в Октопус не пересылаются
403Нет заголовка трекера или запрос пришёл с другого сайта
400page_url не с вашего домена, тело не JSON или неизвестное действие
413Тело запроса больше 16 КБ

Если приёмник отвечает 204, а в Октопусе событий нет, ищите в журнале ошибок PHP строки, начинающиеся с «Октопус CDP: событие не принято».