UTM-помилки рідко видно в момент запуску кампанії. Реклама вже крутиться, лист уже пішов у базу, партнер уже поставив посилання, а проблема проявляється пізніше: трафік розбився на facebook і Facebook, email-розсилка прийшла як referral, частина кліків потрапила в (not set), а landing page виявився недоступним.

UTM validation dashboard у Google Sheets перед запуском рекламної кампанії
UTM validation dashboard у Google Sheets перед запуском рекламної кампанії

Це не проблема аналітики. Це проблема pre-launch контролю. Якщо команда регулярно запускає PPC, email, affiliate або промо-кампанії, UTM треба перевіряти до старту, а не розбирати після того, як бюджет уже витрачений. Найпростіший робочий шар для такого контролю - Google Sheets з правилами, довідниками й Apps Script.

Google Analytics описує UTM-параметри як спосіб передати campaign data через destination URL. Для стабільної атрибуції важливі щонайменше utmsource, utmmedium і utm_campaign, а значення параметрів чутливі до регістру. Це означає, що Meta і meta можуть роз'їхатися в різні значення у звітах. Тому UTM governance починається не з красивого builder, а з правил, які не дають випустити неправильне посилання.

Які помилки треба ловити до запуску

Для першої версії не потрібна складна система approval. Достатньо перевіряти помилки, які реально псують дані або запуск.

Типові проблеми:

  • порожній utmsource, utmmedium або utm_campaign;
  • різний регістр: Google, google, GOOGLE;
  • зайві пробіли в назвах кампаній;
  • medium не з довідника: paid, socialpaid, ppc замість затвердженого cpc або paidsocial;
  • однаковий final URL для різних рядків без відмінності в utm_content;
  • landing page повертає 404, 500 або редіректить на неочікувану сторінку;
  • у назві кампанії є кирилиця, пробіли або символи, які команда не хоче бачити в аналітиці;
  • у посиланні вже є старі UTM, і нові параметри накладаються поверх них.

Окремо треба контролювати дублікати. Два рядки можуть мати різні назви в таблиці, але збирати той самий final URL. Для аналітики це один і той самий тегований клік, а для команди - майбутній спір про те, яка креативна версія спрацювала.

Структура таблиці

Базова вкладка може називатися Campaign_Links. Один рядок - одне посилання, яке піде в рекламу, email, банер або партнерський матеріал.

Таблиця UTM validation з source, medium, campaign, final URL і статусом перевірки
Таблиця UTM validation з source, medium, campaign, final URL і статусом перевірки

Практичний набір колонок:

ПолеДля чого потрібне
base_urlчистий landing page без UTM
sourceплатформа або джерело
mediumканал трафіку
campaignназва кампанії
contentкреатив, placement або варіант листа
termkeyword або audience, якщо потрібно
final_urlзібране посилання
http_statusвідповідь landing page
validation_statusready, warning або blocked
notesщо треба виправити

Другу вкладку краще зробити довідником. Наприклад, Naming_Rules:

RuleAllowed values
sourcegoogle, meta, linkedin, newsletter, partner
mediumcpc, paid_social, email, affiliate, display
campaign_patternlowercase, hyphen або underscore, без пробілів

Так таблиця стає не просто builder, а контрольним шаром. Людина вводить значення, а система одразу показує, що посилання можна запускати, треба перевірити або не можна випускати.

Warning і blocked - це різні речі

Головна помилка UTM-таблиць - всі проблеми фарбують одним кольором. Через це команда або ігнорує попередження, або блокує запуск через дрібниці.

blocked:

  • немає utmsource, utmmedium або utm_campaign;
  • base_url не відкривається;
  • landing page повертає 4xx або 5xx;
  • final URL дублює інший активний рядок;
  • medium не входить у затверджений довідник;
  • у final URL є два набори UTM-параметрів.

warning:

  • landing page редіректить, але final target правильний;
  • utm_content порожній для кампанії з кількома креативами;
  • campaign name формально валідний, але не збігається з naming-патерном команди;
  • посилання дуже довге;
  • у term або content використані динамічні макроси рекламної системи, які треба перевірити вручну.

Різниця проста: blocked зупиняє запуск, warning змушує людину подивитися на рядок. Не треба зупиняти email-кампанію через відсутній utmcontent, якщо там одне посилання. Але не можна запускати платну кампанію без utmcampaign, бо потім дані доведеться відновлювати вручну.

Архітектура pre-launch QA

Базова схема така:

Campaign brief -> Google Sheets UTM table -> Apps Script validation -> final URL -> launch approval

Схема pre-launch QA для рекламних посилань через Google Sheets, naming rules і Apps Script validation
Схема pre-launch QA для рекламних посилань через Google Sheets, naming rules і Apps Script validation

Маркетолог або PPC-спеціаліст заповнює рядки. Apps Script нормалізує значення, збирає final URL, перевіряє статус landing page через UrlFetchApp, шукає дублікати й записує статус. Менеджер дивиться тільки на рядки, де є blocked або warning.

Для посилань корисно тримати поруч офіційні правила Google Analytics про URL builders і UTM parameters, довідку про manual tagging, а для технічної перевірки - документацію Apps Script по UrlFetchApp і SpreadsheetApp.

Базовий Apps Script

Це не production-система з approval, а мінімальний каркас: взяти рядки з таблиці, нормалізувати поля, зібрати final URL, перевірити landing page і записати статус.

function validateCampaignLinks() {
  const ss = SpreadsheetApp.getActive();
  const sheet = ss.getSheetByName('Campaign_Links');
  const values = sheet.getDataRange().getValues();
  const headers = values.shift();
  const rows = values.map(row => toObject_(headers, row));
  const seen = new Set();

  const output = rows.map(row => {
    const result = validateRow_(row, seen);
    return [
      result.finalUrl,
      result.httpStatus,
      result.status,
      result.notes.join('; ')
    ];
  });

  if (output.length) {
    sheet.getRange(2, 7, output.length, 4).setValues(output);
  }
}

function validateRow_(row, seen) {
  const notes = [];
  let status = 'ready';

  const baseUrl = String(row.base_url || '').trim();
  const source = normalize_(row.source);
  const medium = normalize_(row.medium);
  const campaign = normalize_(row.campaign);
  const content = normalize_(row.content);
  const term = normalize_(row.term);

  if (!baseUrl) notes.push('missing base_url');
  if (!source) notes.push('missing source');
  if (!medium) notes.push('missing medium');
  if (!campaign) notes.push('missing campaign');

  const allowedMedium = ['cpc', 'paid_social', 'email', 'affiliate', 'display'];
  if (medium && !allowedMedium.includes(medium)) {
    notes.push('medium is not allowed');
  }

  if (campaign && !/^[a-z0-9_-]+$/.test(campaign)) {
    notes.push('campaign naming warning');
  }

  const finalUrl = baseUrl ? buildUrl_(baseUrl, {
    utm_source: source,
    utm_medium: medium,
    utm_campaign: campaign,
    utm_content: content,
    utm_term: term
  }) : '';

  if (finalUrl && seen.has(finalUrl)) {
    notes.push('duplicate final_url');
  }
  if (finalUrl) seen.add(finalUrl);

  const httpStatus = baseUrl ? checkLanding_(baseUrl) : '';

  if (httpStatus >= 400 || notes.some(note => note.startsWith('missing') || note === 'medium is not allowed' || note === 'duplicate final_url')) {
    status = 'blocked';
  } else if (httpStatus >= 300 || notes.length) {
    status = 'warning';
  }

  return { finalUrl, httpStatus, status, notes };
}

function buildUrl_(baseUrl, params) {
  const parts = [];
  Object.keys(params).forEach(key => {
    const value = params[key];
    if (value) {
      parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
    }
  });

  const separator = baseUrl.indexOf('?') === -1 ? '?' : '&';
  return baseUrl + separator + parts.join('&');
}

function checkLanding_(url) {
  try {
    const response = UrlFetchApp.fetch(url, {
      followRedirects: false,
      muteHttpExceptions: true
    });
    return response.getResponseCode();
  } catch (error) {
    return 0;
  }
}

function normalize_(value) {
  return String(value || '')
    .trim()
    .toLowerCase()
    .replace(/\s+/g, '_');
}

function toObject_(headers, row) {
  return headers.reduce((acc, header, index) => {
    acc[String(header).trim()] = row[index];
    return acc;
  }, {});
}

У цьому прикладі baseurl перевіряється без UTM, а finalurl збирається окремо. Так легше відрізнити проблему сторінки від проблеми тегування. Якщо landing page уже містить query string, скрипт додає параметри через &, а не ламає URL другим знаком ?.

Довідники важливіші за формули

UTM governance працює тільки тоді, коли команда домовилася про назви. Без довідника таблиця перетворюється на ще одне місце, де кожен пише як звик.

Практичні правила:

  • source має описувати платформу або реальне джерело, а не внутрішній відділ;
  • medium має відповідати каналу, який команда хоче бачити у звітах;
  • campaign краще тримати коротким і стабільним;
  • content потрібен для різних креативів, кнопок, банерів або листів;
  • term не треба заповнювати всюди, якщо keyword-level tracking не використовується.

Для Google Ads з auto-tagging окремо вирішуйте, де потрібні UTM. Якщо команда змішує auto-tagging і manual tagging, правила мають бути явними. Google Analytics зазначає, що при manual tagging значення параметрів потрапляють у traffic-source dimensions, а відсутні параметри можуть давати (not set). Саме тому частково заповнені UTM небезпечніші, ніж здаються.

Як запускати процес без зайвої бюрократії

Добрий процес не змушує маркетолога чекати технічну команду на кожне посилання. Він відсікає очевидні проблеми й залишає людині тільки рішення.

Робочий сценарій:

  1. Кампанія створюється у вкладці Campaign_Links.
  2. Скрипт запускається кнопкою або тригером.
  3. Всі рядки отримують ready, warning або blocked.
  4. blocked виправляються до запуску.
  5. warning переглядаються відповідальним за кампанію.
  6. У рекламу, лист або партнерську інструкцію копіюються тільки ready URL.

Для невеликої команди цього достатньо. Для більшого marketing ops можна додати поле owner, дату approval, launch_date, історію змін і Telegram/email-сповіщення тільки по blocked.

Коли Sheets уже замало

Google Sheets добре закриває pre-launch QA, але не повинна ставати єдиним campaign management system. Якщо у вас сотні кампаній на тиждень, кілька ринків і багато рекламних кабінетів, краще підключати API рекламних платформ, server-side templates або окремий internal tool.

Sheets достатньо, коли:

  • команда запускає десятки, а не тисячі посилань;
  • головний біль - дисципліна naming, а не масштаб;
  • треба швидко прибрати ручні помилки;
  • campaign links готуються людьми перед запуском.

Окремий інструмент потрібен, коли:

  • посилання генеруються масово;
  • є кілька approval layers;
  • UTM залежать від продуктового каталогу або CRM;
  • потрібна інтеграція з рекламними кабінетами й automatic publishing.

Починати все одно варто з правил у таблиці. Якщо команда не може домовитися про source, medium і campaign naming у Sheets, складніший інструмент тільки швидше розмножить хаос.

Контроль дешевший за відновлення даних

UTM-перевірка не робить маркетинг складнішим. Вона прибирає ситуацію, коли помилку знаходять після запуску, а аналітик потім вручну склеює джерела, пояснює (not set) і відновлює логіку кампаній по пам'яті.

Сильна UTM-таблиця має три властивості: збирає final URL однаково для всієї команди, блокує помилки, які псують дані, і залишає контрольні попередження там, де потрібне людське рішення. Це дешевше, ніж запускати кампанію двічі: спочатку з помилками, потім з поясненнями.