// ============================================================
// KONTO-SCHNELLAUDIT-ALS-SHEET · prompted audits · READ-ONLY
// 10 Health-Checks -> Ampel-Sheet (gruen/gelb/rot + Kurzbefund).
// Aendert NICHTS am Konto. URL wird geloggt.
// ============================================================
// ----------------------------- KONFIG -----------------------------
var KONFIG = {
ZEITRAUM_TAGE: 30, // Zeitraum fuer metrische Checks
// Schwellen fuer Ampeln (Anteile in %):
ST_KOSTEN_OHNE_CONV_GELB: 25, ST_KOSTEN_OHNE_CONV_ROT: 50, // Suchbegriffs-Kostenanteil ohne Conv.
IS_VERLUST_GELB: 10, IS_VERLUST_ROT: 20 // Impression-Share-Verlust in %
};
// ------------------------------------------------------------------
function main() {
var z = zeitraum(KONFIG.ZEITRAUM_TAGE);
var waehrung = AdsApp.currentAccount().getCurrencyCode();
// Kampagnen-Metriken einmal laden (mehrere Checks nutzen sie)
var metriken = null;
try { metriken = ladeKampagnenMetriken(z); } catch (e) { metriken = null; }
var checks = [];
// 1) Conversion-Tracking liefert Daten?
checks.push(pruefe('Conversion-Tracking aktiv', function () {
if (!metriken) throw 'Kampagnendaten fehlen';
var conv = 0;
for (var i = 0; i < metriken.length; i++) conv += metriken[i].conv;
if (conv > 0) return ampel('gruen', conv.toFixed(1) + ' Conversions im Zeitraum - Tracking liefert Daten.');
return ampel('rot', '0 Conversions im Konto - Tracking fehlt oder ist defekt. Zuerst fixen!');
}));
// 2) Aktive vs. pausierte Kampagnen
checks.push(pruefe('Aktive vs. pausierte Kampagnen', function () {
var c = zaehleKampagnenStatus();
var txt = c.aktiv + ' aktiv, ' + c.pausiert + ' pausiert.';
if (c.aktiv === 0) return ampel('rot', 'Keine aktive Kampagne - es laeuft nichts.');
if (c.pausiert > c.aktiv) return ampel('gelb', txt + ' Auffaellig viele pausierte Kampagnen - aufraeumen?');
return ampel('gruen', txt);
}));
// 3) Kampagnen mit Ausgaben, aber 0 Conversions
checks.push(pruefe('Kampagnen mit Ausgaben ohne Conversion', function () {
if (!metriken) throw 'Kampagnendaten fehlen';
var betroffen = 0, verbrannt = 0, gesamtKosten = 0;
for (var i = 0; i < metriken.length; i++) {
var m = metriken[i];
gesamtKosten += m.kosten;
if (m.kosten > 0 && m.conv === 0) { betroffen++; verbrannt += m.kosten; }
}
if (betroffen === 0) return ampel('gruen', 'Alle Kampagnen mit Ausgaben liefern auch Conversions.');
var anteil = gesamtKosten > 0 ? (verbrannt / gesamtKosten * 100) : 0;
var txt = betroffen + ' Kampagne(n) ohne Conversion, ' + verbrannt.toFixed(2) + ' ' + waehrung +
' (' + anteil.toFixed(0) + '% der Ausgaben).';
return ampel(anteil >= KONFIG.ST_KOSTEN_OHNE_CONV_ROT ? 'rot' : 'gelb', txt);
}));
// 4) Suchbegriffs-Kostenanteil ohne Conversion
checks.push(pruefe('Suchbegriffe: Kostenanteil ohne Conversion', function () {
var a = suchbegriffAnteilOhneConv(z);
if (a.gesamt === 0) return ampel('gelb', 'Keine Suchbegriffs-Kosten im Zeitraum.');
var pct = a.ohneConv / a.gesamt * 100;
var txt = pct.toFixed(0) + '% der Suchbegriffs-Kosten ohne Conversion (' +
a.ohneConv.toFixed(2) + ' / ' + a.gesamt.toFixed(2) + ' ' + waehrung + ').';
if (pct >= KONFIG.ST_KOSTEN_OHNE_CONV_ROT) return ampel('rot', txt + ' Ausschluss-Listen pruefen.');
if (pct >= KONFIG.ST_KOSTEN_OHNE_CONV_GELB) return ampel('gelb', txt);
return ampel('gruen', txt);
}));
// 5) Negative Keywords im Einsatz
checks.push(pruefe('Auszuschliessende (Negative) Keywords', function () {
var n = zaehleNegativeKeywords();
if (n === 0) return ampel('rot', 'Keine auszuschliessenden Keywords gefunden - hohes Streuverlust-Risiko.');
if (n < 10) return ampel('gelb', 'Nur ' + n + ' Ausschluss-Kriterien - fuer die meisten Konten zu wenig.');
return ampel('gruen', n + ' Ausschluss-Kriterien im Einsatz.');
}));
// 6) Sitelinks / Assets vorhanden
checks.push(pruefe('Anzeigenerweiterungen (Sitelinks)', function () {
var c = zaehleKampagnenStatus();
var mitSitelink = zaehleKampagnenMitSitelinks();
if (c.aktiv === 0) return ampel('gelb', 'Keine aktive Kampagne zum Pruefen.');
var pct = mitSitelink / c.aktiv * 100;
var txt = mitSitelink + ' von ' + c.aktiv + ' aktiven Kampagnen mit Sitelinks (' + pct.toFixed(0) + '%).';
if (mitSitelink === 0) return ampel('rot', 'Keine Sitelinks - wertvolle Anzeigenflaeche verschenkt.');
if (pct < 100) return ampel('gelb', txt);
return ampel('gruen', txt);
}));
// 7) Gebotsstrategie-Uebersicht (informativ)
checks.push(pruefe('Gebotsstrategien-Mix', function () {
if (!metriken) throw 'Kampagnendaten fehlen';
var mix = {};
for (var i = 0; i < metriken.length; i++) {
var s = metriken[i].strategie || 'UNBEKANNT';
mix[s] = (mix[s] || 0) + 1;
}
var teile = [];
for (var k in mix) teile.push(k + ': ' + mix[k]);
if (teile.length === 0) return ampel('gelb', 'Keine aktiven Kampagnen im Zeitraum.');
return ampel('gruen', teile.join(' · '));
}));
// 8) Impression-Share-Verlust durch Budget
checks.push(pruefe('Impression-Share-Verlust (Budget)', function () {
return isVerlustCheck(metriken, 'budgetLostIS', 'Budget zu knapp');
}));
// 9) Impression-Share-Verlust durch Rang/Qualitaet
checks.push(pruefe('Impression-Share-Verlust (Rang/Qualitaet)', function () {
return isVerlustCheck(metriken, 'rankLostIS', 'Gebote/Qualitaet zu niedrig');
}));
// 10) Anzeigengruppen ohne aktive Anzeige
checks.push(pruefe('Anzeigengruppen ohne aktive Anzeige', function () {
var a = anzeigengruppenOhneAnzeige();
if (a.gesamt === 0) return ampel('gelb', 'Keine aktiven Anzeigengruppen gefunden.');
if (a.ohne === 0) return ampel('gruen', 'Alle ' + a.gesamt + ' aktiven Anzeigengruppen haben eine aktive Anzeige.');
return ampel('rot', a.ohne + ' von ' + a.gesamt + ' aktiven Anzeigengruppen ohne aktive Anzeige - schalten nicht aus.');
}));
schreibeSheet(checks, z, waehrung);
}
// ------------------------- Helfer: Ampel-Handling -------------------------
function ampel(farbe, befund) { return { ampel: farbe, befund: befund }; }
function pruefe(name, fn) {
try {
var r = fn();
return { name: name, ampel: r.ampel, befund: r.befund };
} catch (e) {
return { name: name, ampel: 'gelb', befund: 'Nicht pruefbar: ' + e };
}
}
// ------------------------- Datenlader -------------------------
function ladeKampagnenMetriken(z) {
var q =
"SELECT campaign.name, metrics.clicks, metrics.cost_micros, metrics.conversions, " +
"metrics.search_budget_lost_impression_share, metrics.search_rank_lost_impression_share, " +
"campaign.bidding_strategy_type, campaign.advertising_channel_type " +
"FROM campaign WHERE campaign.status = 'ENABLED' " +
"AND segments.date BETWEEN '" + z.von + "' AND '" + z.bis + "'";
var rows = [];
var it = AdsApp.search(q);
while (it.hasNext()) {
var r = it.next();
rows.push({
name: r.campaign.name,
klicks: Number(r.metrics.clicks) || 0,
kosten: (Number(r.metrics.costMicros) || 0) / 1e6,
conv: Number(r.metrics.conversions) || 0,
// IS-Kennzahlen sind Anteile 0..1 und koennen fehlen (undefined) -> so lassen
budgetLostIS: r.metrics.searchBudgetLostImpressionShare,
rankLostIS: r.metrics.searchRankLostImpressionShare,
strategie: r.campaign.biddingStrategyType,
kanal: r.campaign.advertisingChannelType
});
}
return rows;
}
function zaehleKampagnenStatus() {
var aktiv = 0, pausiert = 0;
var it = AdsApp.search("SELECT campaign.status FROM campaign WHERE campaign.status IN ('ENABLED','PAUSED')");
while (it.hasNext()) {
var s = it.next().campaign.status;
if (s === 'ENABLED') aktiv++; else if (s === 'PAUSED') pausiert++;
}
return { aktiv: aktiv, pausiert: pausiert };
}
function suchbegriffAnteilOhneConv(z) {
var q =
"SELECT metrics.cost_micros, metrics.conversions FROM search_term_view " +
"WHERE segments.date BETWEEN '" + z.von + "' AND '" + z.bis + "' AND metrics.clicks > 0";
var gesamt = 0, ohneConv = 0;
var it = AdsApp.search(q);
while (it.hasNext()) {
var r = it.next();
var k = (Number(r.metrics.costMicros) || 0) / 1e6;
var c = Number(r.metrics.conversions) || 0;
gesamt += k;
if (c === 0) ohneConv += k;
}
return { gesamt: gesamt, ohneConv: ohneConv };
}
function zaehleNegativeKeywords() {
var q = "SELECT campaign.id FROM campaign_criterion " +
"WHERE campaign_criterion.type = 'KEYWORD' AND campaign_criterion.negative = true";
var n = 0;
var it = AdsApp.search(q);
while (it.hasNext()) { it.next(); n++; }
return n;
}
function zaehleKampagnenMitSitelinks() {
var q = "SELECT campaign.id FROM campaign_asset WHERE campaign_asset.field_type = 'SITELINK'";
var set = {};
var it = AdsApp.search(q);
while (it.hasNext()) { set[it.next().campaign.id] = true; }
return Object.keys(set).length;
}
function anzeigengruppenOhneAnzeige() {
// Alle aktiven Anzeigengruppen (in aktiven Kampagnen)
var alle = {};
var it1 = AdsApp.search(
"SELECT ad_group.id FROM ad_group " +
"WHERE ad_group.status = 'ENABLED' AND campaign.status = 'ENABLED'");
while (it1.hasNext()) alle[it1.next().adGroup.id] = true;
// Anzeigengruppen, die mind. eine aktive Anzeige haben
var mitAnzeige = {};
var it2 = AdsApp.search(
"SELECT ad_group.id FROM ad_group_ad " +
"WHERE ad_group_ad.status = 'ENABLED' AND ad_group.status = 'ENABLED' AND campaign.status = 'ENABLED'");
while (it2.hasNext()) mitAnzeige[it2.next().adGroup.id] = true;
var gesamt = Object.keys(alle).length, ohne = 0;
for (var id in alle) if (!mitAnzeige[id]) ohne++;
return { gesamt: gesamt, ohne: ohne };
}
// Impression-Share-Verlust auswerten (Durchschnitt ueber Kampagnen mit Wert)
function isVerlustCheck(metriken, feld, ursache) {
if (!metriken) throw 'Kampagnendaten fehlen';
var summe = 0, anzahl = 0;
for (var i = 0; i < metriken.length; i++) {
var v = metriken[i][feld];
if (typeof v === 'number' && !isNaN(v)) { summe += v; anzahl++; } // fehlende Werte ueberspringen
}
if (anzahl === 0) return ampel('gelb', 'Kein Impression-Share-Wert verfuegbar (z. B. keine Suchkampagnen).');
var pct = (summe / anzahl) * 100; // Anteil -> Prozent
var txt = 'Durchschnitt ' + pct.toFixed(0) + '% verlorene Impressionen (' + ursache + '), ueber ' + anzahl + ' Kampagne(n).';
if (pct >= KONFIG.IS_VERLUST_ROT) return ampel('rot', txt);
if (pct >= KONFIG.IS_VERLUST_GELB) return ampel('gelb', txt);
return ampel('gruen', txt);
}
// ------------------------- Sheet schreiben -------------------------
function schreibeSheet(checks, z, waehrung) {
var ss = SpreadsheetApp.create('Konto-Schnellaudit ' + z.von + ' bis ' + z.bis);
var sh = ss.getActiveSheet();
sh.setName('Ampel-Audit');
var kopf = ['Ampel', 'Check', 'Kurzbefund'];
var daten = [kopf];
var rot = 0, gelb = 0, gruen = 0;
for (var i = 0; i < checks.length; i++) {
var c = checks[i];
daten.push([symbol(c.ampel), c.name, c.befund]);
if (c.ampel === 'rot') rot++; else if (c.ampel === 'gelb') gelb++; else gruen++;
}
sh.getRange(1, 1, daten.length, 3).setValues(daten);
sh.getRange(1, 1, 1, 3).setFontWeight('bold');
sh.setFrozenRows(1);
// Zeilen einfaerben nach Ampel
for (var j = 0; j < checks.length; j++) {
sh.getRange(j + 2, 1, 1, 3).setBackground(farbe(checks[j].ampel));
}
sh.setColumnWidth(1, 60);
sh.setColumnWidth(2, 280);
sh.setColumnWidth(3, 560);
var foot = daten.length + 2;
sh.getRange(foot, 1).setValue(
'Konto: ' + AdsApp.currentAccount().getCustomerId() + ' · Zeitraum: ' + z.von + ' bis ' + z.bis +
' · Waehrung: ' + waehrung + ' · Ergebnis: gruen ' + gruen + ' / gelb ' + gelb + ' / rot ' + rot);
sh.getRange(foot + 1, 1).setValue(
'Rote Ampeln? Kostenlose Ersteinschaetzung: hallo@promptedaudits.de - kein Abo.');
Logger.log('=== KONTO-SCHNELLAUDIT fertig ===');
Logger.log('Ampeln: gruen ' + gruen + ' · gelb ' + gelb + ' · rot ' + rot);
Logger.log('SHEET-URL: ' + ss.getUrl());
}
function farbe(a) { return a === 'gruen' ? '#d9ead3' : (a === 'gelb' ? '#fff2cc' : '#f4cccc'); }
function symbol(a) { return a === 'gruen' ? 'GRUEN' : (a === 'gelb' ? 'GELB' : 'ROT'); }
function zeitraum(tage) {
var tz = AdsApp.currentAccount().getTimeZone();
var ende = new Date();
var start = new Date();
start.setDate(ende.getDate() - (tage - 1));
return {
von: Utilities.formatDate(start, tz, 'yyyy-MM-dd'),
bis: Utilities.formatDate(ende, tz, 'yyyy-MM-dd')
};
}