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.
Buone Ads.