Тег widget.js, который вы уже поставили ради живого редактирования, втихую считает каждый просмотр, понимает, откуда пришёл посетитель и какая реклама его привела. Добавьте одну строку JavaScript — или один запрос с сервера, — и вы увидите ещё и тех, кто положил товар в корзину, зарегистрировался и наконец заплатил. Вместе с суммой. Без второго счётчика, без менеджера тегов и без переписывания cookie-баннера.
Что в меню:
- Просмотры страниц — сами, без единой строки кода. Каналы, UTM-метки, идентификаторы кликов, география, устройства.
- Свои события из браузера —
window.crmTrack('add_to_cart', …)для поведения на сайте. - События с сервера —
POST /marketing/eventс секретным ключом: для покупок и всего, что нужно посчитать ровно один раз.
Всё это оказывается в CRM в разделе Маркетинг: визиты — в Посещаемости, ваши события — в Событиях, размеченные ссылки — в Рекламных каналах.
Что работает сразу: просмотры страниц
Если на странице есть widget.js, просмотры уже считаются. Тег тот же, что и в разделе Виджет и разметка:
<script src="https://widget.sitecog.com/widget.js" defer></script>При каждой загрузке страницы виджет отправляет один просмотр через navigator.sendBeacon — крошечный запрос «отправил и забыл»: страницу он не тормозит и доходит, даже если посетитель тут же закрыл вкладку. Сервер отвечает 204 No Content — читать в ответе нечего.
Что собирается
Из браузера:
- адрес страницы, заголовок и реферер;
- UTM-метки:
utm_source,utm_medium,utm_campaign,utm_term,utm_content; - идентификаторы рекламных кликов:
gclid,gbraid,wbraid(Google),yclid(Яндекс),fbclid(Meta),msclkid(Microsoft); - код рекламной ссылки
?dl=из Рекламных каналов; - несколько рекламных cookie, если они на сайте уже есть:
_ga,_gcl_*,_ym_uid,_fbp,_fbc. В режиме согласия они не читаются, пока посетитель не согласился, а если браузер передаёт Global Privacy Control — не читаются вообще никогда; - размер экрана и окна, плотность пикселей, язык браузера и часовой пояс.
Добавляется на нашей стороне:
- местоположение — по IP-адресу;
- тип устройства, браузер и операционная система;
- канал трафика: платный, соцсети, поиск, переходы с сайтов, почта или прямые заходы;
- новый это посетитель или вернувшийся.
Сам IP-адрес хранится усечённым, а из адресов страниц и рефереров перед сохранением вычищаются якорь #… и параметры вроде токенов и паролей. Что именно и сколько живёт — в разделе Что мы храним и сколько.
Ботов мы не выбрасываем, а помечаем и убираем из отчётов. Так что цифры в отчётах — про людей, а сырые данные остаются: вдруг захочется узнать, какая доля трафика на самом деле краулеры (спойлер: больше, чем хотелось бы).
Отчёты — в Маркетинг → Посещаемость: посетители, сессии, каналы, страницы, география и устройства.
Сутки и часы в отчётах
«Вчера» в отчёте — это вчера в часовом поясе вашего сайта, а не по UTC и не в поясе того, кто сейчас смотрит на график. Магазин в Киеве видит вечерний пик в 20:00, а не в 17:00, а владелец в Киеве и менеджер в Нью-Йорке смотрят на одни и те же сутки.
- Пояс задаётся в CRM → Настройки → Часовой пояс. Пока его никто не выбрал, CRM берёт часовой пояс браузера владельца.
- По нему режутся сутки в Посещаемости, Рекламных каналах и Событиях и считаются часы на графиках.
- Смена пояса пересчитывает и прошлые отчёты. Сами данные не меняются — меняется только то, где проходит полночь.
Идентификаторы посетителя и сессии
Вернувшегося посетителя виджет узнаёт без cookie. Он хранит три ключа в localStorage:
| Ключ | Что это | Сколько живёт |
|---|---|---|
crm_vid | Идентификатор посетителя | Пока посетитель не очистит данные сайта |
crm_sid | Идентификатор сессии | Новая сессия начинается после 30 минут бездействия |
crm_sat | Время последней активности — по нему виджет решает, что сессия закончилась | Обновляется, пока посетитель ходит по сайту |
Если хранилище заблокировано (так делают некоторые приватные режимы), идентификаторы живут в памяти, пока открыта вкладка. Запомните crm_vid и crm_sid — они понадобятся, чтобы привязать серверные события к посетителю. В режиме согласия эти ключи появляются только после согласия, а crmConsent.deny() их стирает.
Одностраничные приложения
В SPA (React Router, клиентская навигация Next.js, Vue Router) страница не перезагружается, а просмотры всё равно считаются — сами, без единой строки кода. Виджет следит за history.pushState, history.replaceState и событием popstate и, выждав 300 мс (чтобы цепочка быстрых переходов и редиректов не превратилась в пачку просмотров), отправляет просмотр, если поменялся путь или строка запроса. Реферером такого «виртуального» просмотра становится предыдущая страница вашего сайта — так что пути посетителя по сайту в отчётах выглядят так же, как на обычном многостраничном сайте.
Поведение настраивается атрибутом data-spa на теге скрипта:
| Значение | Что считается просмотром |
|---|---|
| атрибута нет | Загрузка страницы плюс каждая смена пути или строки запроса. Подходит большинству SPA. |
data-spa="hash" | То же самое плюс смена якоря — для приложений с маршрутами вида #/catalog. |
data-spa="off" | Отслеживание SPA выключено: ровно один просмотр на одну загрузку скрипта, как раньше. |
<script src="https://widget.sitecog.com/widget.js" data-spa="hash" defer></script>Приватность и согласие
Cookie виджет не ставит, но аналитика остаётся аналитикой. Если сайту нужно согласие посетителя, включите режим согласия: виджет будет молчать, пока ваш баннер не скажет «можно». Переписывать баннер не придётся — достаточно, чтобы его кнопки дёргали одну функцию.
Режим согласия
Включается одним атрибутом на теге:
<script src="https://widget.sitecog.com/widget.js" data-consent="required" defer></script>Пока посетитель не согласился, виджет:
- не отправляет ни просмотров, ни событий. Они именно выбрасываются, а не копятся в очереди: задним числом после согласия ничего не уйдёт;
- не читает
document.cookie— рекламные_ga,_gcl_*,_ym_uid,_fbp,_fbcостаются нетронутыми; - не создаёт и не хранит
crm_vid,crm_sidиcrm_sat.
Без атрибута всё работает как раньше: счёт начинается сразу, с первой загрузки.
Заявки и чат поддержки работают и до согласия — закрывать посетителю дорогу к вам незачем. Но идентификаторов посетителя и сессии у них в этот момент нет, а значит, нет и атрибуции: такая заявка придёт без канала, UTM-меток и рекламной ссылки. Чат до согласия пользуется одноразовым идентификатором, который живёт только в памяти и никуда не сохраняется (старый разговор при этом по-прежнему восстанавливается по своему crm_chat_token).
window.crmConsent: связываем с баннером
Виджет кладёт в window объект crmConsent:
crmConsent.grant()functionнеобязательноcrmConsent.deny()functionнеобязательноcrm_vid, crm_sid и crm_sat. Работает даже без data-consent — см. ниже.crmConsent.status()() => 'granted' | 'denied' | 'pending'необязательноcrmConsent.requiredbooleanнеобязательноtrue, если на теге стоит data-consent="required".crmConsent.gpcbooleanнеобязательноtrue, если браузер передаёт Global Privacy Control.Выбор запоминается в localStorage под ключом crm_consent, так что при следующих визитах баннер можно не показывать. А когда выбор меняется, на window приходит событие crm:consent — в обработчике просто спросите window.crmConsent.status().
Баннер нередко загружается раньше виджета. На этот случай есть очередь — та же идея, что и у crmq:
window.crmConsent = window.crmConsent || [];
crmConsent.push('grant'); // или 'deny'Загрузившись, виджет выполнит всё, что накопилось, а дальше push срабатывает сразу. Так что кнопкам баннера проще всего всегда пользоваться push и не думать о порядке загрузки.
<script src="https://widget.sitecog.com/widget.js" data-consent="required" defer></script>
<div id="consent-banner" hidden>
<p>Мы считаем посещения, чтобы понимать, какая реклама работает. Можно?</p>
<button type="button" data-choice="grant">Принять</button>
<button type="button" data-choice="deny">Отказаться</button>
</div>
<script>
window.crmConsent = window.crmConsent || [];
const banner = document.getElementById('consent-banner');
banner.addEventListener('click', (e) => {
const choice = e.target.closest('[data-choice]')?.dataset.choice;
if (!choice) return;
crmConsent.push(choice); // 'grant' или 'deny'
banner.hidden = true;
});
// widget.js с defer выполняется до DOMContentLoaded, так что status() уже на месте.
// Выбор сделан в прошлый раз — баннер не показываем. Нет виджета (блокировщик) — тоже.
document.addEventListener('DOMContentLoaded', () => {
if (window.crmConsent.status?.() === 'pending') banner.hidden = false;
});
</script>// ConsentBanner.tsx
'use client';
import { useEffect, useState } from 'react';
type ConsentStatus = 'granted' | 'denied' | 'pending';
type Choice = 'grant' | 'deny';
declare global {
interface Window {
crmConsent?: Choice[] | { status(): ConsentStatus; push(choice: Choice): void };
}
}
// null — widget.js ещё не загрузился (или его съел блокировщик)
function readStatus(): ConsentStatus | null {
const api = window.crmConsent;
return api && !Array.isArray(api) ? api.status() : null;
}
export function ConsentBanner() {
const [status, setStatus] = useState<ConsentStatus | null>(null);
useEffect(() => {
const sync = () => setStatus(readStatus());
sync();
window.addEventListener('crm:consent', sync);
// Виджет мог загрузиться позже эффекта — подождём его несколько секунд
let tries = 0;
const timer = setInterval(() => {
if (readStatus() || ++tries > 25) {
sync();
clearInterval(timer);
}
}, 200);
return () => {
clearInterval(timer);
window.removeEventListener('crm:consent', sync);
};
}, []);
if (status !== 'pending') return null;
const choose = (choice: Choice) => {
window.crmConsent = window.crmConsent || [];
window.crmConsent.push(choice); // crm:consent обновит status, и баннер исчезнет
};
return (
<div className="consent-banner">
<p>Мы считаем посещения, чтобы понимать, какая реклама работает. Можно?</p>
<button type="button" onClick={() => choose('grant')}>Принять</button>
<button type="button" onClick={() => choose('deny')}>Отказаться</button>
</div>
);
}
// app/layout.tsx
// <ConsentBanner />
// <Script src="https://widget.sitecog.com/widget.js" data-consent="required" strategy="afterInteractive" />Global Privacy Control и отказ без баннера
Некоторые браузеры и расширения передают сигнал Global Privacy Control (navigator.globalPrivacyControl) — «не продавайте и не передавайте мои данные». Если он есть, виджет никогда не читает рекламные cookie — в любом режиме, с атрибутом data-consent или без. Проверить сигнал из своего кода можно через window.crmConsent.gpc.
deny() тоже работает на любом сайте, даже без data-consent: отказ соблюдается, а идентификаторы стираются. Так что если баннера у вас нет и не будет, честную ссылку «Не отслеживать меня» в подвале всё равно можно сделать за пять строк:
<a href="#" id="do-not-track">Не отслеживать меня</a>
<script>
document.getElementById('do-not-track').addEventListener('click', (e) => {
e.preventDefault();
window.crmConsent = window.crmConsent || [];
crmConsent.push('deny');
e.currentTarget.textContent = 'Готово: больше не отслеживаем';
});
</script>Что мы храним и сколько
IP-адреса хранятся усечёнными. У IPv4 обнуляется последний октет (203.0.113.57 → 203.0.113.0), IPv6 обрезается до /48. Страну и город мы определяем по полному адресу ещё до усечения, поэтому отчёты по географии работают как прежде.
Адреса чистятся перед сохранением. Из адресов страниц, рефереров, адреса первой страницы визита и адресов событий убираются якорь (#…) и чувствительные параметры запроса:
token *_token access_token id_token refresh_token auth_token auth password pass passwd pwd email e-mail mail key api_key apikey secret client_secret otp session sessionid jwt
*_token — это любое имя, которое заканчивается на _token. А вот code остаётся: промокоды в ссылках — обычное дело. UTM-метки, идентификаторы кликов и dl тоже на месте, иначе атрибуции было бы не из чего взяться.
Просмотры и события живут 13 месяцев (395 дней), потом удаляются. Срок настраивается на стороне сервера — его может поменять тот, кто администрирует вашу установку Diil. Заявки под это правило не попадают и не удаляются.
Посетитель может попросить забыть его раньше. Владелец сайта удаляет его профиль, просмотры и события по идентификатору посетителя прямо из CRM; заявки — только если это выбрано отдельно. Подробнее — в разделе Заявки → Удаление данных человека.
Рекламные ссылки: какая реклама привела посетителя
В Маркетинг → Рекламные каналы вы создаёте размеченную ссылку для каждой площадки — пост в соцсети, рассылка, баннер на чужом сайте. Ссылка ведёт на ваш сайт и несёт UTM-метки плюс короткий код:
https://shop.example/?utm_source=instagram&utm_medium=social&utm_campaign=autumn_sale&dl=k3Zp9QaW1xdl — код ссылки из 10 символов. На сайте ничего делать не нужно: виджет подхватит его с первым просмотром, и все визиты, события и заявки этой сессии засчитаются ссылке. У каждой ссылки в CRM своя статистика — видно, сколько людей она привела.
Свои события
Просмотры показывают, куда люди заходили. События — что они там делали: положили в корзину, зарегистрировались, открыли калькулятор, заплатили. Событие один раз описывается в CRM, а потом отправляется из браузера или с вашего сервера.
Шаг ноль: опишите событие в CRM
Diil принимает только знакомые события. И это фича: опечатка в коде не создаст молча новый тип события и не расколет ваши отчёты надвое.
Откройте Маркетинг → События и нажмите «Создать событие»
Вверху должен быть выбран сайт (домен).Назовите его так же, как в коде
add_to_cart,signup_completed,purchase. Латиница, цифры и подчёркивания, начинается с буквы, до 64 символов. Добавьте человеческое название и описание — вы же через полгода скажете себе спасибо.Опишите параметры
До 20 на событие, у каждого тип: Строка, Число или Да/нет. Всё, что здесь не описано, в базу не попадёт.Про деньги? Отметьте «Событие приносит деньги»
И укажите название валюты (EUR,USD,USDTили даже ваши бонусные баллы). Без этой галочкиvalueсобытия не сохраняется.Скопируйте готовый вызов
CRM покажет точный вызовcrmTrackи серверный запрос именно для этого события. Вставили — работает.
На сайте может быть до 100 активных типов событий. Когда по событию уже пришли данные, его имя меняться не может (оно ведь уже в коде), а удалить событие нельзя — только отправить в архив, чтобы в отчётах не остались строки без названия.
События из браузера: crmTrack
window.crmTrack(
name: string,
params?: Record<string, string | number | boolean>,
options?: { id?: string; value?: number; currency?: string },
): voidПринцип «отправил и забыл»: ничего не возвращает, исключений не бросает и шлёт через sendBeacon, так что событие доходит, даже если клик уводит со страницы. Идентификаторы посетителя и сессии виджет подставит сам — вам остаётся описать, что случилось.
Аргументы
namestringобязательноparamsobjectнеобязательноПо умолчанию: {}{ sku: 'air3-graphite', price: 149 }. Ключи — по тем же правилам, что и имя, до 40 символов. Сохраняются только параметры, описанные в CRM; значения приводятся к объявленному типу — см. Параметры и типы.options.idstringнеобязательноПо умолчанию: нетid дважды — событие сохранится один раз. Для покупок берите номер заказа.options.valueintegerнеобязательноПо умолчанию: нет149900 — это 1499.00. От 0 до 1012. Сохраняется, только если у события включено «Событие приносит деньги».options.currencystringнеобязательноПо умолчанию: валюта событияEUR, USD, UAH. Не передали — возьмётся валюта, указанная у события в CRM.Примеры
Три события, без которых не обходится почти ни один магазин, — на чём бы ни был сделан ваш сайт:
<button id="buy" data-sku="air3-graphite" data-price="149">В корзину</button>
<form id="signup">…</form>
<script>
// ?. — чтобы блокировщик рекламы, съевший widget.js, не сломал кнопку
document.getElementById('buy').addEventListener('click', (e) => {
const { sku, price } = e.currentTarget.dataset;
window.crmTrack?.('add_to_cart', { sku, price: Number(price) });
});
// Вызывайте, когда аккаунт действительно создан, а не на первом клике
function onSignupSuccess() {
window.crmTrack?.('signup_completed', { method: 'email', newsletter: true });
}
// На странице «Спасибо за заказ». Этот код выполняется раньше отложенного widget.js,
// поэтому идёт через очередь (см. ниже); id делает перезагрузку безвредной
window.crmq = window.crmq || [];
crmq.push([
'purchase',
{ order_id: 'A-1024', items: 2 },
{ id: 'A-1024', value: 29800, currency: 'EUR' }, // 298.00 EUR
]);
</script>// diil.d.ts — один раз рассказываем TypeScript про виджет
export {};
type CrmParams = Record<string, string | number | boolean>;
type CrmOptions = { id?: string; value?: number; currency?: string };
declare global {
interface Window {
crmTrack?: (name: string, params?: CrmParams, options?: CrmOptions) => void;
crmq?: unknown[];
}
}
// AddToCartButton.tsx
type Product = { sku: string; price: number };
export function AddToCartButton({ product, onAdd }: { product: Product; onAdd: () => void }) {
const handleClick = () => {
onAdd();
window.crmTrack?.('add_to_cart', { sku: product.sku, price: product.price });
};
return <button onClick={handleClick}>В корзину</button>;
}// app/checkout/success/PurchaseTracker.tsx
'use client';
import { useEffect } from 'react';
type Props = { orderId: string; items: number; totalCents: number; currency: string };
export function PurchaseTracker({ orderId, items, totalCents, currency }: Props) {
useEffect(() => {
// Эффект может сработать раньше, чем загрузится widget.js, — очередь его дождётся
window.crmq = window.crmq || [];
window.crmq.push([
'purchase',
{ order_id: orderId, items },
{ id: orderId, value: totalCents, currency },
]);
}, [orderId, items, totalCents, currency]);
return null;
}
// app/checkout/success/page.tsx (серверный компонент)
// <PurchaseTracker orderId="A-1024" items={2} totalCents={29800} currency="EUR" />Вызов до загрузки скрипта: очередь crmq
widget.js подключается с defer, поэтому какое-то мгновение window.crmTrack ещё не существует. События, отправленные слишком рано — при загрузке страницы, в эффекте, из inline-скрипта в <head>, — потерялись бы. Для этого есть очередь:
window.crmq = window.crmq || [];
crmq.push(['purchase', { order_id: 'A-1024', items: 2 }, { id: 'A-1024', value: 29800, currency: 'EUR' }]);Каждый элемент — массив из тех же трёх аргументов, что у crmTrack: имя, параметры, опции. Загрузившись, widget.js проигрывает всё, что накопилось в очереди. Дальше crmq.push отправляет сразу — так что можно всегда пользоваться очередью и вообще не думать о порядке загрузки.
type CrmParams = Record<string, string | number | boolean>;
type CrmOptions = { id?: string; value?: number; currency?: string };
export function track(name: string, params?: CrmParams, options?: CrmOptions) {
if (typeof window === 'undefined') return; // серверный рендер: делать нечего
window.crmq = window.crmq || [];
window.crmq.push([name, params || {}, options || {}]);
}Параметры и типы
Параметры сверяются с описанием в CRM и приводятся к объявленному типу по таким правилам:
| Тип в CRM | Что принимает | Полезно знать |
|---|---|---|
| Строка | Любое значение | Обрезается до 500 символов. Объекты превращаются в JSON-строку — но плоские значения в отчётах смотрятся куда приятнее. |
| Число | Конечные числа, по модулю до 1012 | Округляется до 4 знаков после запятой. |
| Да/нет | true, false, "true", "false", 1, 0 | Удобно, когда значение берётся из атрибута data-*. |
Ключи параметров: латиница, цифры и подчёркивания, начинаются с буквы, до 40 символов. Не больше 20 параметров на событие.
Деньги и идемпотентность
value и currency
Сумма передаётся целым числом в минимальных единицах: центах, копейках, сатоши — в чём угодно, что у вашей валюты самое мелкое. 29800 при EUR — это 298.00 €. Дробные деньги в базе рано или поздно дают копеечные расхождения, поэтому мы их просто не допускаем.
value— целое число от 0 до 1012.currency— 1–10 латинских букв или цифр (EUR,USD,UAH,USDT). Не передали — возьмётся валюта события.- Оба поля сохраняются, только если у события отмечено Событие приносит деньги; иначе
valueбудетnull.
const total = 298.0; // что показывает корзина
const value = Math.round(total * 100); // 29800 — что ждёт Diilid: посчитать один раз
Страницу «Спасибо за заказ» перезагружают, открывают из истории, пересылают себе на телефон. Передайте id (в браузере) или event_id (с сервера) — и Diil сохранит событие один раз, сколько бы раз оно ни пришло. До 128 символов, уникален в пределах сайта. Номер заказа — идеальный кандидат.
События с сервера: POST /marketing/event
Деньги в браузере не считают. Блокировщик рекламы может срезать запрос, покупатель закроет вкладку раньше, чем загрузится «Спасибо», а вызвать crmTrack('purchase') из консоли может кто угодно. Зато ваш сервер точно знает, когда оплата подтвердилась. Оттуда и отправляйте:
POST https://back.sitecog.com/marketing/event
content-type: application/json
x-event-key: sk_…Секретные ключи
Серверные события подписываются секретным ключом: Маркетинг → События → Секретные ключи → Выпустить ключ. Выглядит он как sk_ + 48 шестнадцатеричных символов. На сайт — до 5 активных ключей (удобно по одному на сервер или интеграцию), любой можно отозвать в CRM. Заявки с сервера отправляются с теми же ключами.
Запрос
x-event-keyheaderstringобязательноsk_….namestringобязательноn.event_idstringнеобязательноduplicate: true, и второй раз событие не сохранится. Синоним: id.visitor_idstringнеобязательноcrm_vid посетителя из браузера. Синоним: vid.session_idstringнеобязательноcrm_sid посетителя. С ним событие унаследует канал, UTM-метки и рекламную ссылку этой сессии. Синоним: sid.paramsobjectнеобязательноp.valueintegerнеобязательноval.currencystringнеобязательноcur.urlstringнеобязательноu.Всё тело запроса должно уложиться в 8 КБ.
// Node 18+ — fetch встроен
export async function sendPurchase(order) {
const res = await fetch('https://back.sitecog.com/marketing/event', {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-event-key': process.env.DIIL_EVENT_KEY,
},
body: JSON.stringify({
name: 'purchase',
event_id: order.id, // "A-1024" — повторять безопасно
visitor_id: order.crmVid, // сохранили при оформлении, может быть null
session_id: order.crmSid,
params: { order_id: order.id, items: order.items.length },
value: order.totalCents, // 29800 = 298.00
currency: 'EUR',
url: 'https://shop.example/checkout',
}),
signal: AbortSignal.timeout(5000),
});
if (!res.ok) {
console.error('Diil не принял событие:', res.status, await res.text());
}
return res.ok;
}<?php
function send_purchase(array $order): bool
{
$payload = [
'name' => 'purchase',
'event_id' => $order['id'], // "A-1024" — повторять безопасно
'visitor_id' => $order['crm_vid'], // сохранили при оформлении, может быть null
'session_id' => $order['crm_sid'],
'params' => ['order_id' => $order['id'], 'items' => count($order['items'])],
'value' => $order['total_cents'], // 29800 = 298.00
'currency' => 'EUR',
'url' => 'https://shop.example/checkout',
];
$ch = curl_init('https://back.sitecog.com/marketing/event');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'x-event-key: ' . getenv('DIIL_EVENT_KEY'),
],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
error_log("Diil не принял событие: $status $body");
return false;
}
return true;
}curl -X POST https://back.sitecog.com/marketing/event \
-H "content-type: application/json" \
-H "x-event-key: $DIIL_EVENT_KEY" \
-d '{
"name": "purchase",
"event_id": "A-1024",
"visitor_id": "<crm_vid>",
"session_id": "<crm_sid>",
"params": { "order_id": "A-1024", "items": 2 },
"value": 29800,
"currency": "EUR",
"url": "https://shop.example/checkout"
}'Ответ
{ "ok": true, "duplicate": false }{ "ok": true, "duplicate": true }okbooleanduplicatebooleanevent_id уже есть. Ничего нового не сохранено — и это нормально, а не ошибка.Ошибки
В отличие от браузера, серверный адрес прямо говорит, что пошло не так:
| Статус | Тело | Что случилось |
|---|---|---|
| 400 | {"message":"invalid_body"} | Тело — не JSON или не тот объект, который ожидается. |
| 400 | {"message":"invalid_event_name"} | Имя нарушает формат: латиница, цифры, подчёркивания, начинается с буквы, до 64 символов. |
| 400 | {"message":"unknown_event","name":"purchse"} | Такого события нет в Маркетинг → События. Опечатка или ещё не описали. В ответе — имя, которое вы прислали. |
| 401 | {"message":"invalid_key"} | Нет x-event-key, он кривой или отозван. |
| 413 | — | Тело больше 8 КБ. |
| 429 | {"message":"rate_limit_exceeded"} | Слишком много запросов за минуту. См. Лимиты. |
Как связать серверное событие с посетителем
Само по себе серверное событие ничего не знает о рекламе: ваш сервер понятия не имеет, что покупатель три дня назад пришёл из поста в Instagram. А браузер знает. Значит, нужно донести идентификаторы виджета из браузера до бэкенда вместе с заказом:
- при оформлении заказа прочитайте
crm_vidиcrm_sidизlocalStorage; - отправьте их на бэкенд вместе с заказом и сохраните рядом с ним;
- когда оплата подтвердится, передайте их как
visitor_idиsession_id.
Тогда событие унаследует канал, UTM-метки и рекламную ссылку первого просмотра сессии — и в Маркетинг → События будет видно, какой канал и какая ссылка принесли деньги, а не только клики.
// checkout.js — покупатель нажал «Оплатить»
function diilIds() {
try {
return {
crm_vid: localStorage.getItem('crm_vid'),
crm_sid: localStorage.getItem('crm_sid'),
};
} catch {
return { crm_vid: null, crm_sid: null }; // хранилище закрыто: заказ всё равно уходит
}
}
const res = await fetch('/api/orders', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ cart, ...diilIds() }),
});// server.js (Express)
app.post('/api/orders', async (req, res) => {
const { cart, crm_vid, crm_sid } = req.body;
// Храним идентификаторы вместе с заказом: оплата часто подтверждается позже, вебхуком
const order = await db.orders.create({ cart, crmVid: crm_vid, crmSid: crm_sid });
res.json({ id: order.id, payUrl: await createPayment(order) });
});
// Платёжный сервис дёргает этот адрес, когда деньги действительно пришли
app.post('/webhooks/payment', async (req, res) => {
const order = await db.orders.markPaid(req.body.orderId);
// 3 · Diil — аналитика не должна ломать оплату
try {
await sendPurchase(order); // функция из раздела «События с сервера» выше
} catch (err) {
console.error('Diil недоступен, повторим позже', err);
}
res.sendStatus(200);
});Лимиты
Лимиты общие для всех маркетинговых адресов — просмотров, событий и заявок вместе — и считаются в фиксированных минутных окнах.
| Что | Лимит |
|---|---|
| Запросов с одного IP | 120 в минуту |
| Запросов на сайт | 6000 в минуту |
| Тело запроса | 8 КБ (больше → 413) |
| Активных типов событий на сайт | 100 |
| Параметров у события | 20 |
| Имя события / ключ параметра | 64 / 40 символов |
| Строковое значение параметра | 500 символов |
id / event_id | 128 символов |
| Активных секретных ключей на сайт | 5 |
Если что-то не так
Событие не появляется
- Оно не описано. Браузерный адрес всегда отвечает
204и молча выбрасывает незнакомые события — никаких подсказок, так задумано. Серверный честнее:400 unknown_eventс присланным именем. Сомневаетесь — отправьте то же событие разок через curl и прочитайте ответ. - Имя отличается.
addToCartв коде иadd_to_cartв CRM — два разных события. Копируйте вызов из карточки события. - Вы проверяете в режиме Live. Внутри фрейма CRM ничего не считается. Откройте обычную вкладку.
- Включён режим согласия, а согласия ещё нет. Пока
window.crmConsent.status()возвращает'pending', просмотры и события выбрасываются — и после согласия задним числом не отправятся. Нажмите «Принять» в своём баннере и повторите. Подробнее — в разделе Режим согласия. - Скрипт не загрузился или
crmTrackвызвали слишком рано. Используйте очередь crmq. - Домена нет среди сайтов в CRM. Сайт определяется по
Originстраницы (www.не учитывается); незнакомый получает400 unknown_domain— ищите его во вкладке Network в DevTools. - Мешает CSP. Разрешите
https://widget.sitecog.comвscript-srcиhttps://back.sitecog.comвconnect-src.
Событие есть, а параметров нет
Откройте событие в Маркетинг → События. Если там есть строка Приходит, но не описано, параметр до нас дошёл, но в описании события его нет — добавьте его (или исправьте опечатку в коде). И проверьте, что значения подходят под типы из раздела Параметры и типы.
У покупки нет суммы
Отметьте у события Событие приносит деньги и передавайте value целым числом в минимальных единицах — 29800, а не 298.00 и уж точно не "298 €".
Цифры не сходятся с платёжной системой
У части посетителей стоят блокировщики рекламы и расширения для приватности, которые режут запросы аналитики, а кто-то закрывает вкладку до страницы «Спасибо». Браузерные события всегда будут слегка недосчитываться. Для поведения на сайте это нормально, для денег — нет: отправляйте покупки с сервера. Такой запрос не заблокировать, не подделать из консоли, а с event_id он никогда не посчитается дважды.
Как называть события: привычки, которые окупаются
- snake_case и смысл действия:
add_to_cart,signup_completed,purchase,calculator_opened. Неclick1и неButtonPressed. - Называйте результат, а не кнопку.
signup_completedпереживёт редизайн,green_button_click— нет. - Одно событие, много параметров.
add_to_cartс{ sku, price }лучше, чемadd_to_cart_air3,add_to_cart_air4… — и до лимита в 100 типов вы так никогда не дойдёте. - Всегда передавайте
idдля того, что может сработать дважды: покупки, подтверждения, разовые регистрации. - Поведение — из браузера, деньги — с сервера. Клики и шаги через
crmTrack; оплаты, возвраты и подписки — с вашего бэкенда. - Заполняйте описание в CRM. «Срабатывает, когда платёжный сервис подтвердил списание» сэкономит вам совещание через полгода.