Як додавати UTM та витрачати на це мінімум часу

Це мені потрібно?

UTM –  це параметри котрі додаєте до посилання, що б система аналітики визначала звідки прийшов користувач

https://назва сайту/якась сторінка/

?utm_source=google&utm_medium= cpc&utm_campaign=21835179774

В даному випадку:
користувач з гугл utm_source=google

канал реклами cpc: utm_medium=cpc

Кампанія: utm_campaign=21835179774 (деякі спеціалісти передають id кампанії – як на мене, це ще ті мазахісти але цей параметр можна відправити динамічно)

Чому мазахісти?
так як шукати за id, потім звіряти з даними у google ads; (або у looker studio прописувати правила щоб підставляло назву кампанії) – це ще той квест.

Тому ми тут, і тому ця стаття з’явилася.

Від себе додам, що фактично це потрібно для CRM-систем та у разі підключення BigQuery.

Для CRM – тому що інколи автоматично зібраний трекінг не передається коректно.

Для BigQuery – тому що буває, що проєкт розвивається настільки, що потрібен окремий аналітик, даних стає багато, і добре, щоб вони правильно збиралися та аналізувалися.

Якщо ні першого, ні другого немає і не планується, то більшість бізнесів можуть навіть не знати, що в Google є якийсь додатковий сенс від прописаних UTM-міток за наявності автотегінгу.

ps: якщо вам кажуть що в них якийсь не такий сайт і потрібно тільки ютм мітки – то це знак що потрібно зафіксувати:
Чи взагалі gclid передається у параметрах?!

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

Але так як є тренд  – не ставиш UTM то ти не професіонал

то покажу як налаштувати швидко та безболісно

  1. Заходимо на рівень акаунту та вставляємо tracking template:

Лайтова версія трекінгу

{lpurl}?utm_source=google&utm_medium=cpc&utm_campaign={_campaign}

Заморочений варіант:

{lpurl}?utm_source=google&utm_medium=cpc&utm_campaign={_campaign}&utm_content=cid|{campaignid}|gid|{adgroupid}|kwid|{targetid}

2. Додаємо скрипт:
“Скрипт для налаштування користувацьких параметрів для кампаній”

Потрібно додати скрипт та авторизуватися

Копіювати те що нижче:
// This script sets the custom parameter campaign (_campaign) for all campaigns in the account. This parameter contains the campaign name transformed into a URL-compatible format and can be used in the tracking template or final URL suffix.

// Before applying: Ensure that this parameter is not used for another purpose to avoid overwriting important information. Also, check that there isn’t a campaign parameter with an empty value, as this can cause errors.

// [Schedule] The script should be scheduled to run regularly, e.g., every hour, to add parameters to new campaigns.

// It is also good to manually run it after adding new campaigns.

// [Warning] As of October 27, 2024: The script does not work for all campaign types, including Demand Gen and Video. These need to be updated manually using Google Ads Editor. 

// Remember: Changing a campaign name will leave the old names in historical data in Analytics. Therefore, avoid changing campaign names.

// © ADEQUATE www.adequate.digital 2024

// More info: https://adequate.digital/en/utms-for-google-ads-campaigns/

function main() {
  processAccount();
}

function processAccount() {
  // Process standard campaigns (Search and Display)
  var campaignIterator = AdsApp.campaigns().get();
  while (campaignIterator.hasNext()) {
    var campaign = campaignIterator.next();
    processCampaign(campaign);
  }

  // Process Shopping campaigns
  var shoppingCampaignIterator = AdsApp.shoppingCampaigns().get();
  while (shoppingCampaignIterator.hasNext()) {
    var campaign = shoppingCampaignIterator.next();
    processCampaign(campaign);
  }

  // Process Performance Max campaigns
  var pmaxCampaignIterator = AdsApp.performanceMaxCampaigns().get();
  while (pmaxCampaignIterator.hasNext()) {
    var campaign = pmaxCampaignIterator.next();
    processCampaign(campaign);
  }

  // Optional: Attempt to process Demand Gen campaigns with error handling
  try {
    var demandGenCampaignIterator = AdsApp.demandGenCampaigns().get();
    while (demandGenCampaignIterator.hasNext()) {
      var campaign = demandGenCampaignIterator.next();
      processCampaign(campaign);
    }
  } catch (e) {
    Logger.log('Demand Gen campaigns may not be supported in this account: ' + e.message);
  }
}

function processCampaign(campaign) {
  var campaignName = campaign.getName();

  // Percent-encode the campaign name
  var modifiedCampaignName = encodeURIComponent(campaignName);

  // Truncate to 250 characters
  if (modifiedCampaignName.length > 250) {
    Logger.log('Warning: The encoded campaign name for "' + campaignName + '" exceeds 250 characters and will be truncated.');
    modifiedCampaignName = modifiedCampaignName.substring(0, 250);
  }

  // Get existing custom parameters at the campaign level
  var customParameters = campaign.urls().getCustomParameters() || {};

  // Update or add the 'campaign' parameter
  customParameters['campaign'] = modifiedCampaignName;

  // Set the updated custom parameters back to the campaign
  campaign.urls().setCustomParameters(customParameters);

  Logger.log('Updated campaign "' + campaignName + '" with campaign=' + modifiedCampaignName);
}

Що робить скрипт?

Назви котрі ви дали для кампаній – запусує у custom parameter на рівні кампаній (переводячи самостійно пробіли у символи що читаються аналітикою).

Приклади як це відпрацьовує:

В цій статті додав опис тільки “як виставляти utm для Custom parameters” і на це є причини:

  1. це для мене працює найкраще і без помилок
  2. можливо швидко редагувати та переглядати при вигрузці через едітор
  3. можливо додати Final url suffix в додаток до UTM параметрів що встановлені для акаунту, кампаній. Це корисно при роботі з афілеткою.

Чи підійде це вам? Скоріш за всього так, у 90%  випадках.

10% коли не підійде – це через кастомну розробку сайту, де є перенаправлення з умовами котрі збивають параметри.

Для таких випадків можете пошукати рішення в оригінальній статті з скриптами: https://adequate.digital/en/utms-for-google-ads-campaigns/ Там автор надав декілька способів як додавати. Можна як суфікс, параметр, трекінг темплейт.

Корисні поради

  1. Не варто цей скрипт ставити на автоматичний запуск. Тим більше якщо є звичка змінювати назву кампанії
  2. Не змінювати назву кампанії в процессі оптимізації. Ви змінюєте назву – в аналітику полетять данні з новою назвою кампанії, де буде не зручно аналізувати “колишню кампанію та ту ж саму кампанію але зі зміненою назвою”
  3. Якщо є звичка писати в назві кампанії 450%tROAS чи cpc 1,3uah і т.д; то варто перестати це робити, чи робити на рівні груп оголошень. Так як конкуренти можуть з легкістю зрозуміти які стратегії використовуєте.

Корисні статті у справці

– тутонькі можна знайти інфу щодо динамічних параметрів котрі можна додати до tracking template. Наприклад, якщо запускаєте шоппінг то може доречніше для вас  підставити {product_id} у якесь зі значень.

 

Додаю змінену версію скрипту, де передаю не тільки назву кампанії, але й назву групи оголшення. Це дозволяє мати трохи ширше уявлення – що саме відпрацьовує. Та й у випадку підміни заголовку через utm – добре підходить.

Шаблон що встановлюю на рівні акаунту:

{lpurl}?utm_source=google&utm_medium=cpc&utm_campaign={_campaign}&utm_content={_adgroup}&utm_term={keyword}&utm_id={campaignid}&utm_adgroupid={adgroupid}&utm_target={targetid}

Та й сам код:

// /**
 * Google Ads Script
 *
 * Account-level Tracking template:
 * {lpurl}?utm_source=google&utm_medium=cpc&utm_campaign={_campaign}&utm_content={_adgroup}&utm_term={keyword}&utm_id={campaignid}&utm_adgroupid={adgroupid}&utm_target={targetid}
 *
 * Custom parameters:
 * campaign -> {_campaign}
 * adgroup  -> {_adgroup}
 *
 * Logic:
 *
 * Campaign level:
 * - campaign = campaign name
 * - adgroup  = campaign name fallback
 *
 * Ad group level:
 * - campaign = parent campaign name
 * - adgroup  = ad group name
 *
 * Active only:
 * - enabled campaigns
 * - enabled ad groups inside enabled campaigns
 */

var CONFIG = {
  PARAM_CAMPAIGN: 'campaign',
  PARAM_ADGROUP: 'adgroup',

  LOWERCASE: true,
  MAX_LEN: 180,

  ONLY_ENABLED: true,
  VERBOSE_LOGS: true
};

var slugCache = {};

var TRANSLIT = {
  // Ukrainian / Cyrillic
  'а': 'a',
  'б': 'b',
  'в': 'v',
  'г': 'h',
  'ґ': 'g',
  'д': 'd',
  'е': 'e',
  'є': 'ie',
  'ж': 'zh',
  'з': 'z',
  'и': 'y',
  'і': 'i',
  'ї': 'i',
  'й': 'i',
  'к': 'k',
  'л': 'l',
  'м': 'm',
  'н': 'n',
  'о': 'o',
  'п': 'p',
  'р': 'r',
  'с': 's',
  'т': 't',
  'у': 'u',
  'ф': 'f',
  'х': 'kh',
  'ц': 'ts',
  'ч': 'ch',
  'ш': 'sh',
  'щ': 'shch',
  'ь': '',
  'ю': 'iu',
  'я': 'ia',

  // Russian-specific
  'ё': 'e',
  'ы': 'y',
  'э': 'e',
  'ъ': ''
};

function main() {
  var stats = {
    campaigns: 0,
    shoppingCampaigns: 0,
    pmaxCampaigns: 0,
    demandGenCampaigns: 0,
    adGroups: 0,
    shoppingAdGroups: 0,
    errors: 0
  };

  processCampaigns(stats);
  processAdGroups(stats);

  Logger.log('Finished. Stats: ' + JSON.stringify(stats));
}

/**
 * Process enabled campaigns:
 * Search / Display / Standard
 * Shopping
 * Performance Max
 * Demand Gen if supported
 */
function processCampaigns(stats) {
  processStandardCampaigns(stats);
  processShoppingCampaigns(stats);
  processPerformanceMaxCampaigns(stats);
  processDemandGenCampaigns(stats);
}

function processStandardCampaigns(stats) {
  try {
    var selector = AdsApp.campaigns();

    if (CONFIG.ONLY_ENABLED) {
      selector = selector.withCondition("Status = ENABLED");
    }

    var iterator = selector.get();

    while (iterator.hasNext()) {
      var campaign = iterator.next();
      setCampaignParams(campaign);
      stats.campaigns++;
    }
  } catch (e) {
    stats.errors++;
    Logger.log('Standard campaigns error: ' + e.message);
  }
}

function processShoppingCampaigns(stats) {
  try {
    var selector = AdsApp.shoppingCampaigns();

    if (CONFIG.ONLY_ENABLED) {
      selector = selector.withCondition("Status = ENABLED");
    }

    var iterator = selector.get();

    while (iterator.hasNext()) {
      var campaign = iterator.next();
      setCampaignParams(campaign);
      stats.shoppingCampaigns++;
    }
  } catch (e) {
    stats.errors++;
    Logger.log('Shopping campaigns error: ' + e.message);
  }
}

function processPerformanceMaxCampaigns(stats) {
  try {
    if (typeof AdsApp.performanceMaxCampaigns !== 'function') {
      Logger.log('Performance Max campaigns are not supported in this Scripts environment.');
      return;
    }

    var selector = AdsApp.performanceMaxCampaigns();

    if (CONFIG.ONLY_ENABLED) {
      selector = selector.withCondition("Status = ENABLED");
    }

    var iterator = selector.get();

    while (iterator.hasNext()) {
      var campaign = iterator.next();
      setCampaignParams(campaign);
      stats.pmaxCampaigns++;
    }
  } catch (e) {
    stats.errors++;
    Logger.log('Performance Max campaigns error: ' + e.message);
  }
}

function processDemandGenCampaigns(stats) {
  try {
    if (typeof AdsApp.demandGenCampaigns !== 'function') {
      Logger.log('Demand Gen campaigns are not supported in this Scripts environment.');
      return;
    }

    var selector = AdsApp.demandGenCampaigns();

    if (CONFIG.ONLY_ENABLED) {
      selector = selector.withCondition("Status = ENABLED");
    }

    var iterator = selector.get();

    while (iterator.hasNext()) {
      var campaign = iterator.next();
      setCampaignParams(campaign);
      stats.demandGenCampaigns++;
    }
  } catch (e) {
    stats.errors++;
    Logger.log('Demand Gen campaigns error: ' + e.message);
  }
}

/**
 * Process enabled ad groups:
 * Search / Display / Standard ad groups
 * Shopping ad groups if supported
 */
function processAdGroups(stats) {
  processStandardAdGroups(stats);
  processShoppingAdGroups(stats);
}

function processStandardAdGroups(stats) {
  try {
    var selector = AdsApp.adGroups();

    if (CONFIG.ONLY_ENABLED) {
      selector = selector
        .withCondition("Status = ENABLED")
        .withCondition("CampaignStatus = ENABLED");
    }

    var iterator = selector.get();

    while (iterator.hasNext()) {
      var adGroup = iterator.next();
      setAdGroupParams(adGroup);
      stats.adGroups++;
    }
  } catch (e) {
    stats.errors++;
    Logger.log('Standard ad groups error: ' + e.message);
  }
}

function processShoppingAdGroups(stats) {
  try {
    if (typeof AdsApp.shoppingAdGroups !== 'function') {
      Logger.log('Shopping ad groups are not supported in this Scripts environment.');
      return;
    }

    var selector = AdsApp.shoppingAdGroups();

    if (CONFIG.ONLY_ENABLED) {
      selector = selector
        .withCondition("Status = ENABLED")
        .withCondition("CampaignStatus = ENABLED");
    }

    var iterator = selector.get();

    while (iterator.hasNext()) {
      var adGroup = iterator.next();
      setAdGroupParams(adGroup);
      stats.shoppingAdGroups++;
    }
  } catch (e) {
    stats.errors++;
    Logger.log('Shopping ad groups error: ' + e.message);
  }
}

/**
 * Campaign level:
 * campaign = campaign name
 * adgroup  = campaign name fallback
 *
 * This fallback is useful for PMax / Demand Gen,
 * where there are no classic ad groups.
 */
function setCampaignParams(campaign) {
  var campaignName = safeName(campaign.getName());
  var campaignSlug = toSlug(campaignName);

  var params = {};
  params[CONFIG.PARAM_CAMPAIGN] = campaignSlug;
  params[CONFIG.PARAM_ADGROUP] = campaignSlug;

  writeParams(campaign, params, 'campaign "' + campaignName + '"');
}

/**
 * Ad group level:
 * campaign = parent campaign name
 * adgroup  = ad group name
 *
 * This ensures that if the campaign is renamed,
 * the ad group-level campaign parameter is also updated.
 */
function setAdGroupParams(adGroup) {
  var adGroupName = safeName(adGroup.getName());
  var campaignName = safeName(adGroup.getCampaign().getName());

  var campaignSlug = toSlug(campaignName);
  var adGroupSlug = toSlug(adGroupName);

  var params = {};
  params[CONFIG.PARAM_CAMPAIGN] = campaignSlug;
  params[CONFIG.PARAM_ADGROUP] = adGroupSlug;

  writeParams(adGroup, params, 'ad group "' + adGroupName + '"');
}

/**
 * Writes custom parameters without deleting existing unrelated parameters.
 * Updates only keys defined in params: campaign / adgroup.
 */
function writeParams(entity, newParams, label) {
  try {
    var existingParams = entity.urls().getCustomParameters() || {};
    var changed = false;

    for (var key in newParams) {
      if (newParams.hasOwnProperty(key)) {
        var value = newParams[key];

        if (!value) {
          value = 'na';
        }

        if (existingParams[key] !== value) {
          existingParams[key] = value;
          changed = true;
        }
      }
    }

    if (!changed) {
      if (CONFIG.VERBOSE_LOGS) {
        Logger.log('No changes: ' + label);
      }
      return;
    }

    entity.urls().setCustomParameters(existingParams);

    if (CONFIG.VERBOSE_LOGS) {
      Logger.log('Updated ' + label + ' -> ' + JSON.stringify(newParams));
    }
  } catch (e) {
    Logger.log('Could not update ' + label + ': ' + e.message);
  }
}

/**
 * Converts campaign/ad group name to latin slug.
 *
 * Example:
 * Search_Стоматологія_Львів sopranodental
 * ->
 * search_stomatolohiia_lviv_sopranodental
 */
function toSlug(name) {
  name = safeName(name);

  var cacheKey = CONFIG.LOWERCASE ? name.toLowerCase() : name;

  if (slugCache[cacheKey] !== undefined) {
    return slugCache[cacheKey];
  }

  var source = CONFIG.LOWERCASE ? name.toLowerCase() : name;
  var output = '';

  for (var i = 0; i < source.length; i++) {
    var ch = source.charAt(i);
    var low = ch.toLowerCase();

    if (TRANSLIT.hasOwnProperty(low)) {
      output += TRANSLIT[low];
    } else if (/[A-Za-z0-9]/.test(ch)) {
      output += ch;
    } else {
      output += '_';
    }
  }

  // Collapse multiple underscores
  output = output.replace(/_+/g, '_');

  // Trim underscores
  output = output.replace(/^_+|_+$/g, '');

  // Fallback if name became empty
  if (!output) {
    output = 'na';
  }

  // Truncate long values
  if (output.length > CONFIG.MAX_LEN) {
    output = output.substring(0, CONFIG.MAX_LEN);
    output = output.replace(/_+$/g, '');

    if (!output) {
      output = 'na';
    }

    Logger.log('Truncated slug for: ' + name + ' -> ' + output);
  }

  slugCache[cacheKey] = output;

  return output;
}

/**
 * Defensive name handling.
 */
function safeName(value) {
  if (value === null || value === undefined) {
    return 'na';
  }

  value = String(value);

  if (!value.replace(/s+/g, '')) {
    return 'na';
  }

  return value;
}

Leave a Reply

Your email address will not be published. Required fields are marked *