· 19 min di lettura

Il tuo assistente bot su Telegram

Uno script che ogni giorno controlla conversioni, spesa, campagne senza impression, link rotti e stato prodotti, mandando un riepilogo su Telegram. Utile soprattutto nelle "pseudo-ferie".

Il tuo assistente bot su Telegram

Un assistente che ogni giorno ti manda un messaggio per account con conversioni, spesa ed eventuali alert. Utile sempre, ma soprattutto durante le "pseudo-ferie", quando non puoi chiudere del tutto il pc ma sarebbe meglio aprilo il meno possibile.

Ho chiesto all'AI di scrivere uno script che faccia dei controlli semplici sull'account e mi mandi un riepilogo su Telegram.

Per me conta questo: che l'account spenda, che produca conversioni, che non ci siano problemi a link o prodotti.

Lo script gira una volta al giorno e fa questo:

  1. Legge conversioni e costo a livello account e per ogni campagna con impression > 0 nel periodo (oggi escluso), confrontandoli con gli N giorni precedenti.
  2. Calcola CPA e variazioni percentuali in JS, non le lascia decidere all'AI.
  3. Controlla quali campagne abilitate hanno avuto 0 impression oggi.
  4. Verifica broken link negli annunci attivi.
  5. Conta i prodotti per status (ELIGIBLE / NOT_ELIGIBLE) e quelli con impression ieri, confrontando col giorno prima.
  6. Scrive il log campagne sul tab "Log" (sovrascritto a ogni run).
  7. Scrive il log prodotti sul tab "Prodotti" (storico che cresce).
  8. Fa scrivere un messaggio breve a un modello AI economico.
  9. Manda tutto su Telegram.
  10. Se l'AI fallisce, arriva comunque il riepilogo numerico grezzo.

Lo uso da qualche settimana e lo trovo utile davvero.

Esiste anche la versione MCC. Se la vuoi, scrivimi o commenta questo articolo.

Qui il link per la configurazione (molto semplice) del bot su Telegram tramite BotFather.

Il codice dello script

/**
 * Monitor Google Ads -> AI -> Telegram
 * --------------------------------------------------
 * Gira come Google Ads Script (Strumenti > Azioni in blocco > Script).
 * Schedulazione consigliata: Giornaliera, ore 10:00 (fuso dell'account).
 *
 * Cosa fa, una volta al giorno:
 *  1) Legge conversioni e costo a livello account + per ogni campagna
 *     con impression > 0 nel periodo considerato (oggi escluso),
 *     vs i N giorni precedenti.
 *  2) Calcola CPA e variazioni % (in JS, non le lascia all'AI).
 *  3) Verifica quali campagne ABILITATE hanno avuto 0 impression oggi.
 *  4) Verifica la presenza di broken link negli annunci attivi.
 *  5) Conta i prodotti per status (ELIGIBLE / NOT_ELIGIBLE / ecc.)
 *     e quelli con impression ieri; confronta con il giorno precedente.
 *  6) Scrive il log campagne su tab "Log" (sovrascrittura ad ogni run).
 *  7) Scrive il log prodotti su tab "Prodotti" (storico crescente).
 *  8) Fa scrivere un messaggio breve a un modello AI economico.
 *  9) Manda il messaggio sul tuo Telegram.
 *
 * Se l'AI fallisce, ti arriva comunque il riepilogo numerico grezzo.
 *
 * --------------------------------------------------
 * ENG - Monitor Google Ads -> AI -> Telegram
 * --------------------------------------------------
 * Runs as a Google Ads Script (Tools > Bulk actions > Scripts).
 * Recommended schedule: Daily at 10:00 (account timezone).
 *
 * What it does, once a day:
 *  1) Reads conversions and cost at account level + for each campaign
 *     with impressions > 0 in the current period (today excluded),
 *     vs the previous N days.
 *  2) Calculates CPA and % changes in JS (not delegated to AI).
 *  3) Checks which ENABLED campaigns had 0 impressions today.
 *  4) Checks for broken links in active ads.
 *  5) Counts products by status (ELIGIBLE / NOT_ELIGIBLE / etc.)
 *     and those with impressions yesterday; compares with the previous day.
 *  6) Writes the campaign log to the "Log" tab (overwritten each run).
 *  7) Writes the product log to the "Prodotti" tab (append-only history).
 *  8) Has an affordable AI model write a short summary message.
 *  9) Sends the message to your Telegram.
 *
 * If AI fails, you still receive the raw numeric summary.
 */

// ====================== CONFIG ======================
// Template sheet da copiare nel tuo Google Drive:
//  TEMPLATE_SHEET DA COPIARE "https://docs.google.com/spreadsheets/d/1xCh4Qx34jp_zc9Fh0BRHChnsZMzaTj2VwTPbk1lKUvw/copy"
var SHEET_URL         = 'INSERISCI_QUI_URL_DELLO_SHEET';
var SHEET_TAB_LOG     = 'Log';
var SHEET_TAB_VAR     = 'VAR';
var SHEET_TAB_PRODOTTI = 'Prodotti';

// Colonne fisse del tab Prodotti (le colonne status sono dinamiche e seguono).
var PRODUCT_FIXED_COLS = ['Data', 'Totale', 'Con impression', 'Δ Con imp', 'Δ ELIGIBLE', 'Spesa ieri', 'Conversioni ieri'];
// ====================================================


/* ---------------- LETTURA CONFIG DA SHEET ---------------- */
function loadConfig() {
  var ss = SpreadsheetApp.openByUrl(SHEET_URL);

  function get(rangeName) {
    var r = ss.getRangeByName(rangeName);
    if (!r) return null;
    var v = r.getValue();
    if (v === null || v === undefined || v === '') return null;
    return String(v).trim();
  }

  var lookback       = parseInt(get('cfg_lookback_days'), 10);
  var alertPct       = parseFloat(get('cfg_alert_hint_pct'));
  var checkProdottiRaw = get('cfg_check_prodotti');
  // Accetta TRUE, true, 1, SI, sì come valore positivo; assente/vuoto = false
  var checkProdotti  = checkProdottiRaw !== null &&
    ['true', '1', 'si', 'sì', 'yes'].indexOf(checkProdottiRaw.toLowerCase()) !== -1;

  return {
    LOOKBACK_DAYS:      isNaN(lookback) ? null : lookback,
    TELEGRAM_BOT_TOKEN: get('cfg_telegram_bot_token'),
    TELEGRAM_CHAT_ID:   get('cfg_telegram_chat_id'),
    OPENROUTER_API_KEY: get('cfg_openrouter_api_key'),
    OPENROUTER_MODEL:   get('cfg_openrouter_model'),
    SHEET_URL:          SHEET_URL,
    ALERT_HINT_PCT:     isNaN(alertPct) ? null : alertPct,
    CHECK_PRODOTTI:     checkProdotti
  };
}

/* ---------------- VALIDAZIONE CONFIG ---------------- */
var REQUIRED_VARS = [
  ['LOOKBACK_DAYS',      'cfg_lookback_days'],
  ['TELEGRAM_BOT_TOKEN', 'cfg_telegram_bot_token'],
  ['TELEGRAM_CHAT_ID',   'cfg_telegram_chat_id'],
  ['OPENROUTER_API_KEY', 'cfg_openrouter_api_key'],
  ['OPENROUTER_MODEL',   'cfg_openrouter_model'],
  ['ALERT_HINT_PCT',     'cfg_alert_hint_pct']
];

function validateConfig(cfg) {
  var missing = [];
  for (var i = 0; i < REQUIRED_VARS.length; i++) {
    var key = REQUIRED_VARS[i][0];
    var rangeName = REQUIRED_VARS[i][1];
    if (cfg[key] === null || cfg[key] === undefined) missing.push(rangeName);
  }
  return missing;
}


/* ==================== MAIN ==================== */

function main() {
  // ---- Config ----
  var CONFIG;
  try {
    CONFIG = loadConfig();
  } catch (e) {
    Logger.log('ERRORE: impossibile leggere lo sheet di configurazione.\n' +
               'Verifica che SHEET_URL sia corretto e che lo script abbia accesso.\nDettaglio: ' + e);
    return;
  }

  var missing = validateConfig(CONFIG);
  if (missing.length > 0) {
    Logger.log('ERRORE: configurazione incompleta. Variabili mancanti o vuote nel foglio VAR:\n' +
               '  - ' + missing.join('\n  - ') + '\nScript interrotto.');
    return;
  }

  var account     = AdsApp.currentAccount();
  var tz          = account.getTimeZone();
  var currency    = account.getCurrencyCode();
  var accountName = account.getName();

  // ---- Dati campagne ----
  var ranges = computeRanges(CONFIG.LOOKBACK_DAYS, tz);
  var cur    = getPeriodData(ranges.curStart, ranges.curEnd);
  var prev   = getPeriodData(ranges.prevStart, ranges.prevEnd);

  var acc = buildMetrics(cur.totalConv, cur.totalCost, prev.totalConv, prev.totalCost);

  var names = {};
  for (var n in cur.byCampaign) names[n] = true;
  for (var m in prev.byCampaign) names[m] = true;

  var campaigns = [];
  for (var name in names) {
    var c   = cur.byCampaign[name]  || { conv: 0, cost: 0 };
    var p   = prev.byCampaign[name] || { conv: 0, cost: 0 };
    var mtr = buildMetrics(c.conv, c.cost, p.conv, p.cost);
    mtr.name = name;
    campaigns.push(mtr);
  }
  campaigns.sort(function(a, b) { return b.costCur - a.costCur; });

  var dataText = buildDataText(accountName, currency, ranges, acc, campaigns, CONFIG.LOOKBACK_DAYS);

  // ---- Alert raccolta ----
  var alerts = [];
  // checks: null = errore durante l'esecuzione, [] = ok, [...] = problemi trovati
  var checks = { zeroImp: null, broken: null };

  // Check 1: campagne abilitate senza impression oggi
  try {
    checks.zeroImp = checkImpressionsLast24h(tz);
    if (checks.zeroImp.length > 0) {
      alerts.push('⚠️ Campagne abilitate senza impression oggi:\n' +
                  checks.zeroImp.map(function(n) { return '  - ' + n; }).join('\n'));
    }
  } catch (e) { Logger.log('Check impression KO: ' + e); }

  // Check 2: broken link
  try {
    checks.broken = checkBrokenLinks();
    if (checks.broken.length > 0) {
      alerts.push('🔴 Broken link rilevati negli annunci:\n' +
                  checks.broken.map(function(b) { return '  - ' + b; }).join('\n'));
    }
  } catch (e) { Logger.log('Check broken link KO: ' + e); }

  // Check 3: prodotti per status + impression (solo se cfg_check_prodotti = TRUE)
  if (CONFIG.CHECK_PRODOTTI) {
    var productCounts = { __total__: 0 };
    var productImp    = { count: 0, date: '', totalCost: 0, totalConversions: 0 };
    try {
      productCounts = countProductsByStatus();
      productImp    = countProductsWithImpressions(tz);

      Logger.log('=== PRODOTTI ===');
      Logger.log('Totale: ' + productCounts.__total__);
      for (var st in productCounts) {
        if (st !== '__total__') Logger.log('  ' + st + ': ' + productCounts[st]);
      }
      Logger.log('Con impression ieri (' + productImp.date + '): ' + productImp.count);
      Logger.log('Spesa ieri:          ' + productImp.totalCost.toFixed(2));
      Logger.log('Conversioni ieri:    ' + productImp.totalConversions.toFixed(1));

      // Salva su sheet Prodotti e recupera entry precedente per confronto
      var productPrev = logProductsToSheet(tz, productCounts, productImp);

      if (productPrev) {
        Logger.log('Confronto vs ' + productPrev.date + ':');
        var eligCur   = productCounts['ELIGIBLE'] || 0;
        var eligPrev  = productPrev.byStatus['ELIGIBLE'] || 0;
        var eligDelta = eligCur - eligPrev;
        var impDelta  = productImp.count - productPrev.impCount;

        Logger.log('  ELIGIBLE: ' + eligPrev + ' → ' + eligCur + ' (' + signedInt(eligDelta) + ')');
        Logger.log('  Con imp:  ' + productPrev.impCount + ' → ' + productImp.count + ' (' + signedInt(impDelta) + ')');

        if (eligDelta < 0) {
          alerts.push('⚠️ Prodotti eligible in calo: ' +
            eligPrev + ' → ' + eligCur + ' (' + signedInt(eligDelta) + ')');
        }
        if (impDelta < 0) {
          alerts.push('⚠️ Prodotti con impression in calo: ' +
            productPrev.impCount + ' → ' + productImp.count + ' (' + signedInt(impDelta) + ')');
        }
      }
    } catch (e) { Logger.log('Check prodotti KO: ' + e); }
  } else {
    Logger.log('Check prodotti disabilitato (cfg_check_prodotti non attivo).');
  }

  // ---- Log campagne su sheet (sovrascrittura) ----
  var alertsText = alerts.length > 0 ? '\n\nALERT DI SISTEMA:\n' + alerts.join('\n') : '';
  Logger.log('=== LOG COMPLETO CAMPAGNE ===\n' + dataText + alertsText);

  try { logCampaignsToSheet(tz, ranges, acc, campaigns, alerts, checks); } catch (e) { Logger.log('Sheet Log KO: ' + e); }

  // ---- AI ----
  var message;
  try {
    message = callAI(
      buildPrompt(dataText + alertsText, CONFIG.LOOKBACK_DAYS, CONFIG.ALERT_HINT_PCT),
      CONFIG.OPENROUTER_API_KEY, CONFIG.OPENROUTER_MODEL
    );
  } catch (e) {
    Logger.log('AI KO: ' + e);
    message = '⚠️ (AI non disponibile, riepilogo grezzo)\n\n' + dataText + alertsText;
  }

  // ---- Telegram ----
  message = '📊 ' + accountName + '\n\n' + message;
  sendTelegram(message, CONFIG.TELEGRAM_BOT_TOKEN, CONFIG.TELEGRAM_CHAT_ID);
  Logger.log('=== MESSAGGIO TELEGRAM INVIATO ===\n' + message);
}


/* ==================== DATE ==================== */

function computeRanges(lookback, tz) {
  var MS  = 24 * 60 * 60 * 1000;
  var now = new Date();
  function fmt(d) { return Utilities.formatDate(d, tz, 'yyyy-MM-dd'); }
  var curEnd    = new Date(now.getTime() - 1 * MS);
  var curStart  = new Date(now.getTime() - lookback * MS);
  var prevEnd   = new Date(now.getTime() - (lookback + 1) * MS);
  var prevStart = new Date(now.getTime() - (2 * lookback) * MS);
  return {
    curStart:  fmt(curStart),  curEnd:  fmt(curEnd),
    prevStart: fmt(prevStart), prevEnd: fmt(prevEnd)
  };
}

function shiftDate(date, tz, offsetDays) {
  var MS = 24 * 60 * 60 * 1000;
  var d  = new Date(date.getTime() + offsetDays * MS);
  return Utilities.formatDate(d, tz, 'yyyy-MM-dd');
}


/* ==================== DATI CAMPAGNE (GAQL) ==================== */

function getPeriodData(startStr, endStr) {
  var query =
    'SELECT campaign.name, metrics.conversions_by_conversion_date, metrics.cost_micros ' +
    'FROM campaign ' +
    'WHERE metrics.impressions > 0 ' +
    "AND segments.date BETWEEN '" + startStr + "' AND '" + endStr + "'";

  var rows       = AdsApp.report(query).rows();
  var byCampaign = {};
  var totalConv  = 0;
  var totalCost  = 0;

  while (rows.hasNext()) {
    var r    = rows.next();
    var name = r['campaign.name'];
    var conv = parseFloat(r['metrics.conversions_by_conversion_date']) || 0;
    var cost = (parseFloat(r['metrics.cost_micros']) || 0) / 1000000;
    if (!byCampaign[name]) byCampaign[name] = { conv: 0, cost: 0 };
    byCampaign[name].conv += conv;
    byCampaign[name].cost += cost;
    totalConv += conv;
    totalCost += cost;
  }
  return { byCampaign: byCampaign, totalConv: totalConv, totalCost: totalCost };
}


/* ==================== CHECK 1: IMPRESSION ULTIME 24 ORE ==================== */

function checkImpressionsLast24h(tz) {
  var today = Utilities.formatDate(new Date(), tz, 'yyyy-MM-dd');

  var allEnabled = {};
  var allRows    = AdsApp.report(
    'SELECT campaign.name FROM campaign WHERE campaign.status = \'ENABLED\''
  ).rows();
  while (allRows.hasNext()) allEnabled[allRows.next()['campaign.name']] = true;

  var withImp = {};
  var impRows = AdsApp.report(
    'SELECT campaign.name FROM campaign ' +
    'WHERE campaign.status = \'ENABLED\' ' +
    'AND metrics.impressions > 0 ' +
    "AND segments.date = '" + today + "'"
  ).rows();
  while (impRows.hasNext()) withImp[impRows.next()['campaign.name']] = true;

  var zeroImp = [];
  for (var n in allEnabled) { if (!withImp[n]) zeroImp.push(n); }
  return zeroImp;
}


/* ==================== CHECK 2: BROKEN LINK ==================== */

function checkBrokenLinks() {
  var MAX_URLS = 15;
  var query =
    'SELECT ad_group_ad.ad.final_urls FROM ad_group_ad ' +
    'WHERE ad_group_ad.status = \'ENABLED\' ' +
    'AND ad_group.status = \'ENABLED\' ' +
    'AND campaign.status = \'ENABLED\'';

  var rows   = AdsApp.report(query).rows();
  var urlSet = {};
  while (rows.hasNext()) {
    var raw = rows.next()['ad_group_ad.ad.final_urls'];
    if (!raw) continue;
    var cleaned = raw.toString().replace(/[\[\]'"\s]/g, '');
    var parts   = cleaned.split(',');
    for (var i = 0; i < parts.length; i++) {
      var u = parts[i];
      if (u && u.indexOf('http') === 0) urlSet[u] = true;
    }
  }

  var broken  = [];
  var checked = 0;
  for (var url in urlSet) {
    if (checked >= MAX_URLS) break;
    try {
      var resp = UrlFetchApp.fetch(url, { muteHttpExceptions: true, followRedirects: true, method: 'get' });
      if (resp.getResponseCode() >= 400) broken.push(url + ' (HTTP ' + resp.getResponseCode() + ')');
    } catch (e) { broken.push(url + ' (errore: ' + e.message + ')'); }
    checked++;
  }
  return broken;
}


/* ==================== CHECK 3: PRODOTTI ==================== */

function countProductsByStatus() {
  var counts = { __total__: 0 };
  var rows;
  try {
    rows = AdsApp.search(
      'SELECT shopping_product.item_id, shopping_product.status FROM shopping_product'
    );
  } catch (e) {
    Logger.log('ERRORE query shopping_product: ' + e);
    return counts;
  }
  while (rows.hasNext()) {
    var row    = rows.next();
    var status = row.shoppingProduct.status || 'UNKNOWN';
    counts.__total__++;
    counts[status] = (counts[status] || 0) + 1;
  }
  return counts;
}

function countProductsWithImpressions(tz) {
  var yesterday = shiftDate(new Date(), tz, -1);
  var rows = AdsApp.report(
    'SELECT segments.product_item_id, metrics.cost_micros, metrics.conversions_by_conversion_date ' +
    'FROM shopping_performance_view ' +
    "WHERE segments.date = '" + yesterday + "' " +
    'AND metrics.impressions > 0'
  ).rows();
  var seen             = {};
  var totalCost        = 0;
  var totalConversions = 0;
  while (rows.hasNext()) {
    var r  = rows.next();
    var id = r['segments.product_item_id'];
    if (id) seen[id] = true;
    totalCost        += (parseFloat(r['metrics.cost_micros']) || 0) / 1000000;
    totalConversions += parseFloat(r['metrics.conversions_by_conversion_date']) || 0;
  }
  return {
    count:            Object.keys(seen).length,
    date:             yesterday,
    totalCost:        totalCost,
    totalConversions: totalConversions
  };
}


/* ==================== METRICHE & DELTA ==================== */

function buildMetrics(convCur, costCur, convPrev, costPrev) {
  var cpaCur  = convCur  > 0 ? costCur  / convCur  : null;
  var cpaPrev = convPrev > 0 ? costPrev / convPrev : null;
  return {
    convCur:  convCur,  convPrev:  convPrev,  convDelta:  pct(convCur,  convPrev),
    costCur:  costCur,  costPrev:  costPrev,  costDelta:  pct(costCur,  costPrev),
    cpaCur:   cpaCur,   cpaPrev:   cpaPrev,   cpaDelta:   pct(cpaCur,   cpaPrev)
  };
}

function pct(cur, prev) {
  if (prev === null || prev === 0) return null;
  if (cur === null) return null;
  return ((cur - prev) / prev) * 100;
}

function num(x, dec) {
  if (x === null || x === undefined) return 'n/d';
  return x.toFixed(dec === undefined ? 1 : dec);
}

function delta(x) {
  if (x === null) return 'n/d';
  return (x >= 0 ? '+' : '') + x.toFixed(0) + '%';
}

function signedInt(d) {
  return (d >= 0 ? '+' : '') + d;
}


/* ==================== TESTO DATI PER L'AI ==================== */

function buildDataText(accountName, currency, ranges, acc, campaigns, lookback) {
  var L     = lookback;
  var lines = [];
  lines.push('ACCOUNT: ' + accountName + ' (' + currency + ')');
  lines.push('Periodo attuale: ' + ranges.curStart + ' -> ' + ranges.curEnd +
             ' | Confronto: ' + ranges.prevStart + ' -> ' + ranges.prevEnd +
             ' (ultimi ' + L + 'gg vs ' + L + 'gg precedenti, oggi escluso)');
  lines.push('');
  lines.push('TOTALE ACCOUNT:');
  lines.push('  Conversioni: ' + num(acc.convCur) + ' vs ' + num(acc.convPrev) + ' (' + delta(acc.convDelta) + ')');
  lines.push('  CPA: ' + num(acc.cpaCur, 2) + ' vs ' + num(acc.cpaPrev, 2) + ' (' + delta(acc.cpaDelta) + ')');
  lines.push('  Spesa: ' + num(acc.costCur, 2) + ' vs ' + num(acc.costPrev, 2));
  lines.push('');
  lines.push('CAMPAGNE CON IMPRESSION (' + campaigns.length + ' totali):');
  for (var i = 0; i < campaigns.length; i++) {
    var c = campaigns[i];
    lines.push('  - ' + c.name +
      ' | Conv ' + num(c.convCur) + '->' + num(c.convPrev) + ' (' + delta(c.convDelta) + ')' +
      ' | CPA ' + num(c.cpaCur, 2) + '->' + num(c.cpaPrev, 2) + ' (' + delta(c.cpaDelta) + ')' +
      ' | Spesa ' + num(c.costCur, 2));
  }
  return lines.join('\n');
}


/* ==================== PROMPT ==================== */

function buildPrompt(dataText, lookback, alertPct) {
  return (
    'Sei l\'assistente di monitoraggio di un account Google Ads.\n' +
    'Ricevi: dati di performance degli ultimi ' + lookback + ' giorni vs i ' + lookback + ' precedenti ' +
    '(a livello account e campagna) + esito dei controlli automatici di sistema.\n\n' +

    'Scrivi un messaggio Telegram in italiano con esattamente queste 4 sezioni:\n\n' +

    '1) STATO GENERALE\n' +
    '   Semaforo 🟢/🟡/🔴 + massimo 6 parole di sintesi.\n' +
    '   Il semaforo deve tenere conto SIA delle performance (conversioni, CPA) ' +
    'SIA degli ALERT DI SISTEMA se presenti:\n' +
    '   🟢 = tutto nella norma, nessun alert\n' +
    '   🟡 = variazioni moderate o alert minori\n' +
    '   🔴 = calo critico di conversioni, CPA fuori controllo o alert gravi\n\n' +

    '2) ACCOUNT (sempre presente)\n' +
    '   2-3 righe sul totale account: conversioni, CPA e spesa con le relative variazioni %.\n\n' +

    '3) CAMPAGNE CRITICHE\n' +
    '   Elenca SOLO le campagne in cui la variazione di conversioni O di CPA supera ±' + alertPct + '%.\n' +
    '   Per ognuna: nome + metrica critica + variazione. Massimo 3 campagne.\n' +
    '   Se nessuna campagna supera la soglia del ' + alertPct + '%, scrivi "Nessuna campagna critica".\n\n' +

    '4) ALERT DI SISTEMA (solo se presenti nei dati)\n' +
    '   Riporta gli alert con la loro emoji originale (⚠️/🔴). Se non ci sono alert, ometti l\'intera sezione.\n\n' +

    'Regole:\n' +
    '- Indica sempre il numero totale di conversioni per Account.\n' +
    '- Niente preamboli, niente intestazioni di sezione nel testo (non scrivere "1)", "ACCOUNT:", ecc.).\n' +
    '- Usa solo i numeri che ti do, non inventarne.\n' +
    '- Se un dato è "n/d" non commentarlo.\n' +
    '- Max 15 righe totali.\n\n' +

    'DATI:\n' + dataText
  );
}


/* ==================== AI ==================== */

function callAI(prompt, apiKey, model) {
  return callOpenRouter(prompt, apiKey, model);
}

function callOpenRouter(prompt, apiKey, model) {
  var res = UrlFetchApp.fetch('https://openrouter.ai/api/v1/chat/completions', {
    method: 'post',
    contentType: 'application/json',
    headers: {
      'Authorization': 'Bearer ' + apiKey,
      'X-Title': 'Monitor Google Ads'
    },
    payload: JSON.stringify({
      model:      model,
      max_tokens: 600,
      temperature: 0.3,
      messages: [{ role: 'user', content: prompt }]
    }),
    muteHttpExceptions: true
  });
  var data = JSON.parse(res.getContentText());
  if (data.choices && data.choices[0]) return data.choices[0].message.content.trim();
  throw new Error('Risposta OpenRouter inattesa: ' + res.getContentText());
}


/* ==================== TELEGRAM ==================== */

function sendTelegram(text, botToken, chatId) {
  var url = 'https://api.telegram.org/bot' + botToken + '/sendMessage';
  if (text.length > 4000) text = text.substring(0, 3990) + '...';
  var res = UrlFetchApp.fetch(url, {
    method: 'post',
    payload: { chat_id: chatId, text: text, disable_web_page_preview: 'true' },
    muteHttpExceptions: true
  });
  Logger.log('Telegram HTTP ' + res.getResponseCode());
}


/* ==================== SHEET: TAB LOG (campagne, sovrascrittura) ==================== */

function logCampaignsToSheet(tz, ranges, acc, campaigns, alerts, checks) {
  var ss    = SpreadsheetApp.openByUrl(SHEET_URL);
  var sheet = ss.getSheetByName(SHEET_TAB_LOG);
  if (!sheet) {
    sheet = ss.insertSheet(SHEET_TAB_LOG);
  } else {
    sheet.clearContents();
  }

  var runDate    = Utilities.formatDate(new Date(), tz, 'yyyy-MM-dd HH:mm');
  var periodCur  = ranges.curStart  + ' / ' + ranges.curEnd;
  var periodPrev = ranges.prevStart + ' / ' + ranges.prevEnd;
  var COLS = 10;
  var data = [];

  data.push(pad(['Data run',        runDate],    COLS));
  data.push(pad(['Periodo Attuale', periodCur],  COLS));
  data.push(pad(['Periodo prec',    periodPrev], COLS));
  data.push([
    '', 'Conv attuali', 'Conv prec', 'Δ conv %',
    'CPA attuale', 'CPA prec', 'Δ CPA %',
    'Spesa attuale', 'Spesa prec', 'Δ Spesa %'
  ]);
  data.push(makeSheetRow('TOTALE ACCOUNT', acc));
  for (var i = 0; i < campaigns.length; i++) {
    data.push(makeSheetRow(campaigns[i].name, campaigns[i]));
  }
  // Sezione CONTROLLI ESEGUITI — sempre presente, mostra l'esito di ogni check
  data.push(pad([], COLS));
  data.push(pad(['CONTROLLI ESEGUITI'], COLS));

  if (checks && checks.zeroImp === null) {
    data.push(pad(['  Impression oggi:  ❓ errore durante il controllo'], COLS));
  } else if (checks && checks.zeroImp.length === 0) {
    data.push(pad(['  Impression oggi:  ✅ tutte le campagne abilitate hanno avuto impression'], COLS));
  } else if (checks) {
    data.push(pad(['  Impression oggi:  ⚠️ campagne senza impression → ' +
      checks.zeroImp.join(', ')], COLS));
  }

  if (checks && checks.broken === null) {
    data.push(pad(['  Broken link:      ❓ errore durante il controllo'], COLS));
  } else if (checks && checks.broken.length === 0) {
    data.push(pad(['  Broken link:      ✅ nessun link rotto rilevato'], COLS));
  } else if (checks) {
    data.push(pad(['  Broken link:      🔴 ' + checks.broken.join(' | ')], COLS));
  }

  // Sezione ALERT DI SISTEMA — tutti gli alert (impression, broken link, prodotti)
  if (alerts && alerts.length > 0) {
    data.push(pad([], COLS));
    data.push(pad(['ALERT DI SISTEMA'], COLS));
    for (var j = 0; j < alerts.length; j++) {
      data.push(pad([alerts[j]], COLS));
    }
  }

  sheet.getRange(1, 1, data.length, COLS).setValues(data);
}

function makeSheetRow(label, m) {
  return [
    label,
    roundOrEmpty(m.convCur),    roundOrEmpty(m.convPrev),    fmtPct(m.convDelta),
    roundOrEmpty(m.cpaCur, 2),  roundOrEmpty(m.cpaPrev, 2),  fmtPct(m.cpaDelta),
    roundOrEmpty(m.costCur, 2), roundOrEmpty(m.costPrev, 2), fmtPct(m.costDelta)
  ];
}

function pad(arr, len) {
  while (arr.length < len) arr.push('');
  return arr;
}

function roundOrEmpty(x, dec) {
  if (x === null || x === undefined) return '';
  return parseFloat(x.toFixed(dec === undefined ? 1 : dec));
}

function fmtPct(x) {
  if (x === null || x === undefined) return '';
  return parseFloat((x / 100).toFixed(4));
}


/* ==================== SHEET: TAB PRODOTTI (storico crescente) ==================== */

// Apre/crea il tab Prodotti, salva la riga odierna e restituisce l'entry
// del giorno precedente (null al primo run).
function logProductsToSheet(tz, counts, imp) {
  var ss    = SpreadsheetApp.openByUrl(SHEET_URL);
  var sheet = getOrCreateProductSheet(ss, counts);
  var today = Utilities.formatDate(new Date(), tz, 'yyyy-MM-dd');
  var prev  = getProductPreviousEntry(sheet, today);
  saveProductsToday(sheet, today, counts, imp, prev);
  return prev;
}

function getOrCreateProductSheet(ss, counts) {
  var sheet = ss.getSheetByName(SHEET_TAB_PRODOTTI);
  if (!sheet) {
    sheet = ss.insertSheet(SHEET_TAB_PRODOTTI);
    var headers = PRODUCT_FIXED_COLS.slice();
    for (var s in counts) {
      if (s !== '__total__') headers.push(s);
    }
    sheet.appendRow(headers);
    sheet.getRange(1, 1, 1, headers.length).setFontWeight('bold');
  }
  return sheet;
}

function getProductHeaderMap(sheet) {
  var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
  var map = {};
  for (var i = 0; i < headers.length; i++) map[String(headers[i])] = i;
  return map;
}

function getProductPreviousEntry(sheet, today) {
  var lastRow = sheet.getLastRow();
  if (lastRow < 2) return null;

  var headerMap = getProductHeaderMap(sheet);
  var data      = sheet.getRange(2, 1, lastRow - 1, sheet.getLastColumn()).getValues();

  for (var i = data.length - 1; i >= 0; i--) {
    var rowDate = String(data[i][0]).trim();
    if (rowDate && rowDate !== today) {
      var byStatus = {};
      for (var col in headerMap) {
        if (PRODUCT_FIXED_COLS.indexOf(col) === -1) {
          byStatus[col] = Number(data[i][headerMap[col]]) || 0;
        }
      }
      return {
        date:     rowDate,
        total:    Number(data[i][headerMap['Totale']]) || 0,
        impCount: Number(data[i][headerMap['Con impression']]) || 0,
        byStatus: byStatus
      };
    }
  }
  return null;
}

function saveProductsToday(sheet, today, counts, imp, prev) {
  var headerMap = getProductHeaderMap(sheet);

  // Aggiunge colonne per status nuovi
  for (var s in counts) {
    if (s !== '__total__' && !(s in headerMap) && PRODUCT_FIXED_COLS.indexOf(s) === -1) {
      var newCol = sheet.getLastColumn() + 1;
      sheet.getRange(1, newCol).setValue(s).setFontWeight('bold');
      headerMap[s] = newCol - 1;
    }
  }

  var deltaImp  = prev !== null ? imp.count - prev.impCount : '';
  var deltaElig = prev !== null ? (counts['ELIGIBLE'] || 0) - (prev.byStatus['ELIGIBLE'] || 0) : '';

  var numCols = sheet.getLastColumn();
  var row     = new Array(numCols).fill('');
  row[headerMap['Data']]              = today;
  row[headerMap['Totale']]            = counts.__total__;
  row[headerMap['Con impression']]    = imp.count;
  row[headerMap['Δ Con imp']]         = deltaImp;
  row[headerMap['Δ ELIGIBLE']]        = deltaElig;
  row[headerMap['Spesa ieri']]        = parseFloat(imp.totalCost.toFixed(2));
  row[headerMap['Conversioni ieri']]  = parseFloat(imp.totalConversions.toFixed(1));
  for (var st in counts) {
    if (st !== '__total__' && st in headerMap && PRODUCT_FIXED_COLS.indexOf(st) === -1) {
      row[headerMap[st]] = counts[st];
    }
  }

  var lastRow = sheet.getLastRow();
  if (lastRow >= 2) {
    var lastDate = String(sheet.getRange(lastRow, 1).getValue()).trim();
    if (lastDate === today) {
      sheet.getRange(lastRow, 1, 1, numCols).setValues([row]);
      return;
    }
  }
  sheet.appendRow(row);
}

Trovi questo e gli altri script che uso ogni giorno sugli account.

Buone Ads.

← Torna al blog