vvs35.ru боты, парсеры, серверы
УслугиГотовые решенияБлог принимаем заказы

Почему UTM-метки не доходят до письма с заявкой

Реклама откручивается, заявки идут, а откуда именно пришёл человек — непонятно. В письме только имя и телефон. Ставится задача «прокинуть UTM-метки в заявку», разработчик добавляет скрытые поля — и метки всё равно не приходят.

Разберу, почему так происходит и что действительно нужно сделать. Речь про самописные формы на чистом HTML и JS, без плагинов вроде WPForms или Contact Form 7 — у них свои механизмы, там всё проще.

Сначала проверьте, дойдут ли новые поля до письма вообще

Это главная причина, из-за которой задача проваливается, и проверять её нужно до того, как писать хоть строчку JavaScript.

Скрытое поле в форме — это просто ещё одна пара «ключ-значение» в POST-запросе. Попадёт ли она в письмо, зависит от того, как письмо собирает серверный обработчик.

Если он перебирает всё, что пришло, — новые поля появятся в письме сами:

$body = '';
foreach ($_POST as $key => $value) {
    $body .= htmlspecialchars($key) . ': ' . htmlspecialchars($value) . "\n";
}

А если поля перечислены руками — а так написано большинство обработчиков, — то никакие скрытые инпуты в письмо не попадут, сколько их ни добавляй:

$body = "Имя: {$_POST['name']}\n"
      . "Телефон: {$_POST['phone']}\n"
      . "Сообщение: {$_POST['message']}\n";

Отсюда два сценария:

  1. Обработчик можно править — добавляем скрытые поля и дописываем их вывод в письмо. Дальше всё по инструкции ниже.
  2. Обработчик трогать нельзя — так бывает, когда сайт делали давно, исходников нет или заказчик боится сломать приём заявок. Тогда единственный способ ничего не сломать: дописывать метки в конец уже существующего поля, которое точно уходит в письмо, — обычно это «сообщение» или «комментарий».

Второй вариант выглядит грубо, но он работает всегда и не требует доступа к бэкенду. Метки просто оказываются внизу текста заявки:

Перезвоните после 18:00

--- источник: yandex / cpc / avgust_remont / yclid=7712... / cid=17419...

Вторая ловушка: форма отправляется не так, как вы думаете

Допустим, с письмом разобрались. Дальше вы вешаете обработчик на submit, создаёте скрытые поля — а в POST их всё равно нет.

Причина в том, что форму отправляет не браузер, а JavaScript. И если код собирает данные вручную, скрытые поля он просто не видит:

// метки сюда не попадут никогда
form.addEventListener('submit', async (e) => {
  e.preventDefault();
  await fetch('/send.php', {
    method: 'POST',
    body: JSON.stringify({
      name: form.name.value,
      phone: form.phone.value
    })
  });
});

В браузере поля есть, в DevTools они видны, а в запрос уходят три перечисленных ключа — и всё.

Как отличить один случай от другого. Откройте вкладку Network, отправьте тестовую заявку и посмотрите payload запроса. Если там ровно те поля, что вы ожидали, и ничего лишнего — данные собираются вручную. Если весь набор полей формы — используется FormData(form), и скрытые поля сработают.

Когда сборка ручная, скрытые поля бесполезны, и правильное место — обёртка над fetch, которая дописывает метки в тело запроса:

const origFetch = window.fetch;
window.fetch = function (url, opts = {}) {
  if (opts.method && opts.method.toUpperCase() === 'POST' && isFormEndpoint(url)) {
    opts.body = appendMarks(opts.body);
  }
  return origFetch.call(this, url, opts);
};

appendMarks разбирает тело — JSON, FormData или строку application/x-www-form-urlencoded — и добавляет туда метки. Для старых сайтов на jQuery то же самое делается через $.ajaxPrefilter.

Куда складывать метки, чтобы они не терялись

Классическое решение — записать UTM в cookie на 365 дней. С этим есть проблема, о которой обычно узнают постфактум.

В Safari cookie, поставленная из JavaScript, живёт 7 суток. Это Intelligent Tracking Prevention, и обойти его нельзя: срок в 365 дней, который вы указали, браузер молча сократит. На мобильном трафике из iOS это заметная часть аудитории.

Поэтому дублируем в localStorage — на него ограничение не распространяется, — и читаем оттуда, если cookie уже нет:

const KEYS = ['utm_source','utm_medium','utm_campaign','utm_content','utm_term','yclid'];
const TTL_DAYS = 365;

function save(name, value) {
  const d = new Date();
  d.setTime(d.getTime() + TTL_DAYS * 864e5);
  document.cookie = `${name}=${encodeURIComponent(value)};expires=${d.toUTCString()};path=/;SameSite=Lax`;
  try { localStorage.setItem(name, value); } catch (e) {}
}

function read(name) {
  const m = document.cookie.match('(?:^|; )' + name + '=([^;]*)');
  if (m) return decodeURIComponent(m[1]);
  try { return localStorage.getItem(name) || ''; } catch (e) { return ''; }
}

Не затирайте метки пустыми значениями

Частая ошибка: скрипт при каждой загрузке страницы перезаписывает все ключи тем, что нашёл в адресе. Человек пришёл по объявлению, через неделю зашёл напрямую и оставил заявку — источник потерян, заявка засчитана как «прямой заход».

Записываем только тогда, когда в адресе действительно есть метки:

const params = new URLSearchParams(location.search);
const hasNew = KEYS.some(k => params.get(k));
if (hasNew) KEYS.forEach(k => { if (params.get(k)) save(k, params.get(k)); });

Дальше вопрос модели атрибуции: оставлять первый источник или перезаписывать последним. Универсального ответа нет, но по умолчанию я беру последний непрямой заход — так считает большинство рекламных систем, и цифры в отчётах сходятся.

Client ID Метрики появляется не сразу

_ym_uid ставит счётчик Метрики, а не ваш скрипт. Если пользователь отправил форму в первые секунды после загрузки, счётчик может ещё не отработать, и в заявку уйдёт пустое значение.

Поэтому читаем с коротким повтором и не блокируем отправку, если значения так и не дождались:

function waitYm(timeout = 3000, step = 300) {
  return new Promise(resolve => {
    const t0 = Date.now();
    (function tick() {
      const v = read('_ym_uid');
      if (v || Date.now() - t0 > timeout) return resolve(v || '');
      setTimeout(tick, step);
    })();
  });
}

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

Формы, появившиеся после загрузки страницы

Модалки, попапы и формы, подгружаемые аяксом, к моменту работы скрипта в DOM не существуют. Перебор document.querySelectorAll('form') на DOMContentLoaded их не увидит.

Решение — не перебирать формы, а слушать событие на уровне документа в фазе перехвата:

document.addEventListener('submit', onSubmit, true);

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

Как проверить, что всё работает

Проверять надо не глазами по коду, а реальными заявками:

  1. Зайти на сайт с адресом вида ?utm_source=test&utm_medium=cpc&utm_campaign=check1&yclid=123.
  2. Отправить заявку — метки должны прийти в письме.
  3. Закрыть вкладку, зайти на сайт заново напрямую, отправить вторую заявку — метки те же, значит хранение работает.
  4. Открыть в Safari и повторить — проверяем localStorage.
  5. Отправить форму из модального окна — проверяем перехват на уровне документа.

Пять писем, пять минут, и никаких сюрпризов через месяц, когда по отчётам окажется, что 80 % заявок «прямые».

Коротко: частые вопросы

Почему UTM-метки не доходят до письма, если скрытые поля в форме уже добавлены?

Чаще всего письмо в PHP собирается по жёсткому списку полей: имя, телефон, сообщение. Новое скрытое поле в этот список не входит и в письмо не попадает, хотя браузер его отправил. Проверяется за минуту: временно собрать тело письма циклом по всему $_POST и отправить заявку самому себе.

Метки видны в адресе страницы, но в заявке их нет. Почему?

Форма уходит не обычным submit, а через fetch с телом, собранным руками из перечисленных полей. Скрытые поля в такое тело не попадают никогда, сколько их ни добавляй в разметку. Смотреть надо не на форму, а на код отправки.

Почему метка теряется через неделю, хотя кука поставлена на год?

Safari сокращает срок куки, поставленной из JavaScript, до семи суток. Для сделок с долгим циклом метку надо дублировать — в localStorage или в куку, выставленную сервером.

Как проверить, что метки доходят, не дожидаясь рекламы?

Открыть страницу с формой, дописав в адрес ?utm_source=proverka&utm_medium=test, и отправить заявку самому себе. Если метки нет в письме — дело не в рекламе и не в подрядчике по трафику.

Разбираться самому некогда? Сделаю под ключ: один js-файл в футер, работает со всеми формами, с проверкой тестовыми заявками и показом результата в письмах.

Написать в Telegram @vvs35_bot

Читать дальше