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".
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:
- Legge conversioni e costo a livello account e per ogni campagna con impression > 0 nel periodo (oggi escluso), confrontandoli con gli N giorni precedenti.
- Calcola CPA e variazioni percentuali in JS, non le lascia decidere all'AI.
- Controlla quali campagne abilitate hanno avuto 0 impression oggi.
- Verifica broken link negli annunci attivi.
- Conta i prodotti per status (ELIGIBLE / NOT_ELIGIBLE) e quelli con impression ieri, confrontando col giorno prima.
- Scrive il log campagne sul tab "Log" (sovrascritto a ogni run).
- Scrive il log prodotti sul tab "Prodotti" (storico che cresce).
- Fa scrivere un messaggio breve a un modello AI economico.
- Manda tutto su Telegram.
- 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.
Uno script ti avvisa quando qualcosa non va, ma non ti dice perché. Se vuoi un esperto consulente Google Ads che vada a fondo, posso fare un audit account Google Ads completo e, per chi vende online, un’analisi Merchant Center del catalogo.
Buone Ads.