Пояснення:

const SPREADSHEET_URL = 'ПОСИЛАННЯ НА ВАШУ ТАБЛИЦЮ' — сюди буде надходити звіт

const KPI_COST_PER_CONVERSION = 1000 — вище цього числа буде вважатись відхиленням та формуватись у звіт. Можна оновлювати періодично за потреби на актуальну CPA

const DATE_RANGE = 'LAST_30_DAYS' — залишати, як є або прописати в такому форматі потрібний діапазон '2024-04-01,2024-04-30'

Як створити телеграм бота, знайти CHAT ID, BOT TOKEN, розповідав ТУТ

Код скрипту:

function main() {
  const SPREADSHEET_URL = 'ПОСИЛАННЯ НА ВАШУ ТАБЛИЦЮ'; // замініть на свою таблицю
  const KPI_COST_PER_CONVERSION = 1000; // вартість конверсії в грн,прописати число
  const DATE_RANGE = 'LAST_30_DAYS'; // або '2024-04-01,2024-04-30'

  const TELEGRAM_BOT_TOKEN = 'ВАШ ТЕЛЕГРАМ ТОКЕН';
  const TELEGRAM_CHAT_ID = 'ВАШ ТЕЛЕГРАМ ЧАТ ID';

  const currencyCode = AdsApp.currentAccount().getCurrencyCode();

	const spreadsheet = SpreadsheetApp.openByUrl(SPREADSHEET_URL);

analyzeKeywords(spreadsheet, KPI_COST_PER_CONVERSION, DATE_RANGE, currencyCode);
analyzeAds(spreadsheet, KPI_COST_PER_CONVERSION, DATE_RANGE, currencyCode);
analyzeDevices(spreadsheet, KPI_COST_PER_CONVERSION, DATE_RANGE, currencyCode);
analyzeTime(spreadsheet, KPI_COST_PER_CONVERSION, DATE_RANGE, currencyCode);

sendTelegramMessage(SPREADSHEET_URL, TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID);

function clearOrCreateSheet(spreadsheet, name, withTimeDevice = false) {
  let sheet = spreadsheet.getSheetByName(name);
  if (!sheet) {
    sheet = spreadsheet.insertSheet(name);
  } else {
    sheet.clear({contentsOnly: true});
  }

  const headers = ['Тип', 'Назва/ID', 'Кампанія', 'Конверсії', 'Ціна/Конверсію', 'Витрати', 'Валюта', 'CTR (%)', 'Конверсія (%)', 'Кліки', 'Середня ціна кліка', 'Покази'];
  if (withTimeDevice) {
    headers.push('День', 'Година');
  }

  sheet.appendRow(headers);
  return sheet;
}

function formatPercent(value) {
  return (value * 100).toFixed(2).replace('.', ',') + '%';
}

function appendFormattedRow(sheet, data, numericColumns) {
  const rowIndex = sheet.getLastRow() + 1;

  const cleanedData = data.map((val, i) => {
    if (numericColumns.includes(i + 1)) {
      const cleanedVal = val.toString().replace(/\\s/g, '').replace(',', '.');
      const num = parseFloat(cleanedVal);
      return isFinite(num) && num >= 0 ? parseFloat(num.toFixed(2).replace('.', ',')) : '0,00';
    }
    return val;
  });

  sheet.appendRow(cleanedData);
}

function analyzeKeywords(spreadsheet, kpi, dateRange, currencyCode) {
  const sheet = clearOrCreateSheet(spreadsheet, 'Ключові слова');
  const report = AdsApp.keywords()
    .withCondition("Status = ENABLED")
    .withCondition("Impressions > 0")
    .withCondition("Clicks > 0")
    .forDateRange(dateRange)
    .get();

  while (report.hasNext()) {
    const kw = report.next();
    const stats = kw.getStatsFor(dateRange);
    const cost = stats.getCost();
    const conversions = Math.ceil(stats.getConversions());  // Округлюємо в більшу сторону
    const clicks = stats.getClicks();
    const impressions = stats.getImpressions();
    const costPerConv = conversions > 0 ? cost / conversions : null;
    const avgCpc = clicks > 0 ? cost / clicks : 0;

    if (conversions === 0 || (costPerConv && costPerConv > kpi)) {
      appendFormattedRow(sheet, [
        'Ключове слово',
        kw.getText(),
        kw.getCampaign().getName(),
        conversions,
        costPerConv ? costPerConv.toFixed(2).replace('.', ',') : 'Немає',
        cost.toFixed(2).replace('.', ','),
        currencyCode,
        formatPercent(stats.getCtr()),
        clicks > 0 ? formatPercent(conversions / clicks) : '0%',
        clicks,
        avgCpc.toFixed(2).replace('.', ','),
        impressions
      ], [4, 5, 6, 10, 12]);
    }
  }
}

function analyzeAds(spreadsheet, kpi, dateRange, currencyCode) {
  const sheet = clearOrCreateSheet(spreadsheet, 'Оголошення');
  const report = AdsApp.ads()
    .withCondition("Status = ENABLED")
    .withCondition("Impressions > 0")
    .withCondition("Clicks > 0")
    .forDateRange(dateRange)
    .get();

  while (report.hasNext()) {
    const ad = report.next();
    const stats = ad.getStatsFor(dateRange);
    const cost = stats.getCost();
    const conversions = Math.ceil(stats.getConversions());  // Округлюємо в більшу сторону
    const clicks = stats.getClicks();
    const impressions = stats.getImpressions();
    const costPerConv = conversions > 0 ? cost / conversions : null;
    const avgCpc = clicks > 0 ? cost / clicks : 0;

    if (conversions === 0 || (costPerConv && costPerConv > kpi)) {
      appendFormattedRow(sheet, [
        'Оголошення',
        ad.getId(),
        ad.getCampaign().getName(),
        conversions,
        costPerConv ? costPerConv.toFixed(2).replace('.', ',') : 'Немає',
        cost.toFixed(2).replace('.', ','),
        currencyCode,
        formatPercent(stats.getCtr()),
        clicks > 0 ? formatPercent(conversions / clicks) : '0%',
        clicks,
        avgCpc.toFixed(2).replace('.', ','),
        impressions
      ], [4, 5, 6, 10, 12]);
    }
  }
}

function analyzeDevices(spreadsheet, kpi, dateRange, currencyCode) {
  const sheet = clearOrCreateSheet(spreadsheet, 'Пристрої');
  const report = AdsApp.report(
    `SELECT Device, CampaignName, Conversions, Cost, Clicks, Impressions
     FROM CAMPAIGN_PERFORMANCE_REPORT
     WHERE Impressions > 0 AND Clicks > 0
     DURING ${dateRange}`
  );

  const rows = report.rows();
  while (rows.hasNext()) {
    const row = rows.next();
    const conv = Math.ceil(parseFloat(row['Conversions']));  // Округлюємо в більшу сторону
    const cost = parseFloat(row['Cost']);
    const clicks = parseFloat(row['Clicks']);
    const impressions = parseFloat(row['Impressions']);
    const costPerConv = conv > 0 ? cost / conv : null;
    const avgCpc = clicks > 0 ? cost / clicks : 0;

    if (conv === 0 || (costPerConv && costPerConv > kpi)) {
      appendFormattedRow(sheet, [
        'Пристрій',
        row['Device'],
        row['CampaignName'],
        conv,
        costPerConv ? costPerConv.toFixed(2).replace('.', ',') : 'Немає',
        cost.toFixed(2).replace('.', ','),
        currencyCode,
        impressions > 0 ? formatPercent(clicks / impressions) : '0%',
        clicks > 0 ? formatPercent(conv / clicks) : '0%',
        clicks,
        avgCpc.toFixed(2).replace('.', ','),
        impressions
      ], [4, 5, 6, 10, 12]);
    }
  }
}

function analyzeTime(spreadsheet, kpi, dateRange, currencyCode) {
  const sheet = clearOrCreateSheet(spreadsheet, 'Час/День', true);
  const report = AdsApp.report(
    `SELECT CampaignName, DayOfWeek, HourOfDay, Conversions, Cost, Clicks, Impressions
     FROM CAMPAIGN_PERFORMANCE_REPORT
     WHERE Impressions > 0 AND Clicks > 0
     DURING ${dateRange}`
  );

  const rows = report.rows();
  while (rows.hasNext()) {
    const row = rows.next();
    const conv = Math.ceil(parseFloat(row['Conversions']));  // Округлюємо в більшу сторону
    const cost = parseFloat(row['Cost']);
    const clicks = parseFloat(row['Clicks']);
    const impressions = parseFloat(row['Impressions']);
    const costPerConv = conv > 0 ? cost / conv : null;
    const avgCpc = clicks > 0 ? cost / clicks : 0;

    if (conv === 0 || (costPerConv && costPerConv > kpi)) {
      appendFormattedRow(sheet, [
        'Година/День',
        '',
        row['CampaignName'],
        conv,
        costPerConv ? costPerConv.toFixed(2).replace('.', ',') : 'Немає',
        cost.toFixed(2).replace('.', ','),
        currencyCode,
        impressions > 0 ? formatPercent(clicks / impressions) : '0%',
        clicks > 0 ? formatPercent(conv / clicks) : '0%',
        clicks,
        avgCpc.toFixed(2).replace('.', ','),
        impressions,
        row['DayOfWeek'],
        row['HourOfDay']
      ], [4, 5, 6, 10, 12]);
    }
  }
}

function sendTelegramMessage(sheetUrl, botToken, chatId) {
  const message = encodeURIComponent(`✅ Ваш звіт моніторингу CPA Google Ads готовий:\\n${sheetUrl}`);
  const url = `https://api.telegram.org/bot${botToken}/sendMessage?chat_id=${chatId}&text=${message}`;
  UrlFetchApp.fetch(url);
}

}