-
-
Save mbaersch/276f6a821e0770ce596141cece98a750 to your computer and use it in GitHub Desktop.
/**************************************************************/ | |
/****** "Gurkenfinder"-Script für Google AdWords *******/ | |
/**************************************************************/ | |
/* v1.4 2020 Markus Baersch (@mbaersch) | |
Reduzierte Non-MCC-Fassung | |
gandke marketing & software - www.gandke.de */ | |
/*********** Start Setup **********************/ | |
var emailAddress = "[email protected]"; | |
var emailName = "" ; | |
var chkLabel = "PC:DoCheck" ; //Nur Kampagnen mit diesem Label werden berücksichtigt | |
var chkPauseLabel = "PC:Paused" ; //Optionales Label für pausierte Keywords | |
//Standard- / Schwellwerte & Einstellungen | |
var chkType = 'REL'; //REL = Umsatz/Kosten, CNV = Anzahl Conversions; CPA = Kosten/Conversion | |
var trshClicks = 200; //Schwellwert für erzielte Klicks zur Untersuchung | |
var chkDateRange = 'ALL_TIME'; //Mögl. Werte: TODAY, YESTERDAY, LAST_7_DAYS, THIS_WEEK_SUN_TODAY, | |
//LAST_WEEK, LAST_14_DAYS, LAST_30_DAYS, LAST_BUSINESS_WEEK, LAST_WEEK_SUN_SAT, | |
//THIS_MONTH, LAST_MONTH, ALL_TIME | |
//REL | |
var trshCnvCostRelReport = 2; //Melden ab CVal/Cost unter 2 | |
var trshCnvCostRelPause = 0.5; //Pausieren, wenn CVal/Cost unter 0,5 | |
//CNV | |
var trshCnvReport = 2; //Melden bei weniger als 2 Conversions | |
var trshCnvPause = 0.5; //Pausieren unter 0,5 Conversions | |
//CPA | |
var trshCpaReport = 30; //Melden bei CPA über 30,-- | |
var trshCpaPause = 50; //Pausieren bei CPA > 40,-- | |
var setupDays = [1,4] ; //Wochentage; Sonntag = 0. Hier: nur Montags und Donnertags ausführen | |
//Mieser Workaround, weil immer noch kein Zugriff auf Smart Shopping Kampagnen via API besteht, wohl aber | |
//die Berichte genutzt werden können: Hier die Namen der Smart Shopping Kampagnen als Array eintragen, | |
//welche zusätzlich zu normalen Shopping Kampagnen untersucht werden sollen. Fundstellen werden dann ausgewiesen, | |
//aber es besteht keine Information darüber, ob das Produkt bereits deaktiviert wurde oder nicht. | |
//Daher speziell in Verbindung mit "ALl_TIME" als Datumsbereich nervig, weil Einträge nie aus dem Bericht | |
//verschwinden würden, wenn auch das Produkt deaktiviert sein mag. | |
var setupSmartCampaignNames = ['Smart Shopping Beispiel 1', 'Smart Shopping Beispiel 2']; | |
var debug = false ; | |
/*********** Ende Setup ***********************/ | |
var Wochentage = new Array("Sonntag", "Montag", "Dienstag", "Mittwoch", | |
"Donnerstag", "Freitag", "Samstag"); | |
function main() { | |
var chkShResults = new Array(); | |
var chkKwResults = new Array(); | |
//Start nur an ausgewählten Wochentagen | |
var wTag = getAccountCurrentDateTime().getDay(); | |
if (debug || (setupDays.indexOf(wTag) >= 0)) { | |
w2log('Vorgang gestartet.') ; | |
var mandantId = AdWordsApp.currentAccount().getCustomerId(); | |
var mandantName = AdWordsApp.currentAccount().getName(); | |
//Labels anlegen, wenn nicht vorhanden | |
needsLabel(chkLabel) ; | |
if (chkPauseLabel) needsLabel(chkPauseLabel) ; | |
//Ergebnisse für Keyword- und Shopping-Kampagnen ermitteln | |
chkKwResults = checkKeywordPerformance(); | |
chkShResults = checkProductPerformance(); | |
if (debug) { | |
w2log(formatResults(chkShResults, 'S', true)) ; | |
w2log(formatResults(chkKwResults, 'K', true)) ; | |
} | |
w2log('Vorgang abgeschlossen.') ; | |
if ((!emailAddress) || (emailAddress == "[email protected]")) { | |
w2log("Es wurde keine Mailadresse angegeben, Report wird nicht versendet."); | |
return ""; | |
} else { | |
//Mail mit Ergebnissen erstellen | |
if (!debug) { | |
if (emailName) emailName = ' ('+emailName+')' ; | |
var htmlResult = "<br><br><hr><h2>Ergebnisse für \"" + mandantName + "\"</h2>\n" + "\n" + | |
formatResults(chkShResults, 'S', false) + "\n" + | |
formatResults(chkKwResults, 'K', false) ; | |
var mlSubject = 'Gurkenfinder-Report'+ emailName +' für '+ Wochentage[wTag] + ', den '+ | |
Utilities.formatDate(new Date(), AdWordsApp.currentAccount().getTimeZone(), "dd.MM.yyyy") ; | |
var resPreText = '<style type="text/css">body,small,h1,h2,h3,p,ul,li{font-size:11px;font-family:arial,sans-serif;color:#222}'+ | |
'td{padding-right:13px;white-space:nowrap;vertical-align:top}b.r{color:red}h1,h2,h3{margin:20px 0 0 0;'+ | |
'padding:0 0 2px 0;font-size:16px;color:#C44E00}h2 a{color:#C44E00}h1{font-size:18px}h3{font-size:13px;color:#14941D}</style>'+ | |
'<h1>'+mlSubject+'</h1>' ; | |
var resBody = resPreText + htmlResult + '<p>llap!</p>' ; | |
if (resBody.length >= 200*1024) { | |
w2log("Report zu groß für E-Mail.") ; | |
//nicht senden... | |
resBody = resPreText + '<p>Der Report ist leider zu groß für den Versand per E-Mail.</p>' ; | |
w2log(formatResults(chkShResult, 'S', true)) ; | |
w2log(formatResults(chkKwResult, 'K', true)) ; | |
} | |
//Mail senden | |
MailApp.sendEmail({ | |
to: emailAddress, | |
subject: mlSubject, | |
htmlBody: resBody, | |
}); | |
w2log('Info wurde per Mail gesendet.'); | |
} | |
} | |
} else | |
w2log('Heute setze ich aus...') ; | |
} | |
function checkKeywordPerformance() { | |
//Aktive Kampagnen abrufen | |
var campaignIterator = AdWordsApp.campaigns() | |
.withCondition("Status = ENABLED") | |
.withCondition("LabelNames CONTAINS_ALL ['"+chkLabel+"']") | |
.get(); | |
var cmpFound = false; | |
var cResults = new Array(); | |
//Keyword-Reports für ausgewählte Kampagnen abrufen | |
while (campaignIterator.hasNext()) { | |
cmpFound = true; | |
var campaign = campaignIterator.next(); | |
var repStatement = 'SELECT Id, CampaignName, AdGroupName, Criteria, Clicks, '+ | |
'Ctr, AveragePosition, AverageCpc, Cost, Conversions, CostPerConversion, ConversionValue ' + | |
'FROM KEYWORDS_PERFORMANCE_REPORT WHERE CampaignName="' + campaign.getName() + | |
'" AND Status = ENABLED AND Clicks > '+trshClicks ; | |
if (chkDateRange != "ALL_TIME") repStatement += ' DURING '+chkDateRange ; | |
var report = AdWordsApp.report(repStatement); | |
var rows = report.rows(); | |
while (rows.hasNext()) { | |
var row = rows.next(); | |
var cst = row['Cost']; | |
var cstraw = getValue(cst) ; | |
var rev = row['ConversionValue']; | |
var revraw = getValue(rev) ; | |
var rel = formatFloat(revraw / cstraw) ; | |
var cnvs = row['Conversions'] ; | |
var cpa = row['CostPerConversion'] ; | |
var cnvsraw = getValue(cnvs); | |
var cparaw = getValue(cpa) ; | |
if ((cnvsraw === 0) && (chkType === 'CPA')) cpa = 999999; | |
if (((chkType === 'REL') && (rel < trshCnvCostRelReport)) || | |
((chkType === 'CPA') && (cparaw > trshCpaReport)) || | |
((chkType === 'CNV') && (cnvsraw < trshCnvReport))) | |
{ | |
var aktRow = new Array(); | |
aktRow.push(row['CampaignName']); | |
aktRow.push(row['AdGroupName']); | |
if (((chkType === 'REL') && (rel < trshCnvCostRelPause)) || | |
((chkType === 'CPA') && (cpa > trshCpaPause)) || | |
((chkType === 'CNV') && (cnvs < trshCnvPause))) | |
{ | |
var kw = AdWordsApp.keywords().withCondition('Id='+row['Id']).get().next(); | |
if (chkPauseLabel != "") addLbl(kw, chkPauseLabel) | |
kw.pause(); | |
aktRow.push('<b style="color:red">' + row['Criteria'] + ' [pausiert]</b>'); | |
} else | |
aktRow.push(row['Criteria']); | |
aktRow.push(formatInt(row['Clicks'])); | |
aktRow.push(row['Ctr']); | |
aktRow.push(row['AverageCpc']); | |
aktRow.push(cst); | |
aktRow.push(row['AveragePosition']); | |
aktRow.push(cnvs); | |
aktRow.push(cpa); | |
aktRow.push(row['ConversionValue']); | |
aktRow.push(rel); | |
cResults.push(aktRow) ; | |
} | |
} //Reportiterator | |
} //Kampagneniterator | |
if (!cmpFound) cResults = "NIX" ; | |
return cResults; | |
} | |
function checkProductPerformance() { | |
//Aktive Kampagnen abrufen | |
var campaignIterator = AdWordsApp.shoppingCampaigns() | |
.withCondition("Status = ENABLED") | |
.withCondition("LabelNames CONTAINS_ALL ['"+chkLabel+"']") | |
.get(); | |
var cmpFound = false; | |
var cResults = new Array(); | |
var campaignNames = []; | |
campaignNames.push(setupSmartCampaignNames); | |
while (campaignIterator.hasNext()) { | |
campaignNames.push([campaignIterator.next().getName()]); | |
} | |
//Keyword-Reports für ausgewählte Kampagnen abrufen | |
for (i=0; i<campaignNames.length; i++) { | |
cmpFound = true; | |
var campaign = campaignNames[i]; | |
var repStatement = 'SELECT OfferId, Brand, ProductTypeL1, CategoryL1, AdGroupId, CampaignName, AdGroupName, Clicks, Ctr, AverageCpc, Cost, '+ | |
'Conversions, CostPerConversion, ConversionValue ' + | |
'FROM SHOPPING_PERFORMANCE_REPORT WHERE CampaignName="' + campaign + '" AND Clicks > '+trshClicks ; | |
if (chkDateRange != "ALL_TIME") repStatement += ' DURING '+chkDateRange ; | |
var report = AdWordsApp.report(repStatement); | |
var rows = report.rows(); | |
while (rows.hasNext()) { | |
var row = rows.next(); | |
var adg = row['AdGroupId']; | |
var prdId = row['OfferId']; | |
var cst = row['Cost']; | |
var cstraw = getValue(cst) ; | |
var rev = row['ConversionValue']; | |
var revraw = getValue(rev) ; | |
var rel = formatFloat(revraw / cstraw) ; | |
var cnvs = row['Conversions'] ; | |
var cpa = row['CostPerConversion'] ; | |
var cnvsraw = getValue(cnvs); | |
var cparaw = getValue(cpa) ; | |
if ((cnvsraw === 0) && (chkType === 'CPA')) cpa = 999999; | |
if (((chkType === 'REL') && (rel < trshCnvCostRelReport)) || | |
((chkType === 'CPA') && (cparaw > trshCpaReport)) || | |
((chkType === 'CNV') && (cnvsraw < trshCnvReport))) | |
{ | |
//Ist das Produkt noch aktiv oder schon deaktiviert? | |
var itemInactive = false ; | |
var adgrp = AdWordsApp.shoppingAdGroups().withIds([adg]).get(); | |
//Ist es eine Anzeigengruppe einer normalen Shoping Kampagne? Dann status überprüfen. | |
//Bei Smart Shopping steht die Info derzeit nicht zur Verfügung | |
if (adgrp.totalNumEntities() > 0) { | |
var adgrps = adgrp.next().productGroups().get(); | |
while (adgrps.hasNext()) { | |
var grp = adgrps.next(); | |
if (grp.getDimension() == 'ITEM_ID') | |
if (grp.asItemId().getValue() == prdId) | |
if (grp.asItemId().isExcluded()) { | |
itemInactive = true ; | |
w2log('INFO: ' + prdId + ' übergangen, weil bereits deaktiviert'); | |
break; | |
} | |
} | |
} | |
if (!itemInactive) { | |
var aktRow = new Array(); | |
aktRow.push(row['CampaignName']); | |
aktRow.push(row['AdGroupName']); | |
if (((chkType === 'REL') && (rel < trshCnvCostRelPause)) || | |
((chkType === 'CPA') && (cpa > trshCpaPause)) || | |
((chkType === 'CNV') && (cnvs < trshCnvPause))) | |
{ | |
aktRow.push('<b style="color:red">' + row['OfferId'] + ' - manuell pausieren!</b>'); | |
} else | |
aktRow.push(row['OfferId']); | |
aktRow.push(row['Brand']); | |
aktRow.push(row['ProductTypeL1']); | |
aktRow.push(row['CategoryL1']); | |
aktRow.push(formatInt(row['Clicks'])); | |
aktRow.push(row['Ctr']); | |
aktRow.push(row['AverageCpc']); | |
aktRow.push(row['Cost']); | |
aktRow.push(cnvs); | |
aktRow.push(cpa); | |
aktRow.push(row['ConversionValue']); | |
aktRow.push(rel); | |
cResults.push(aktRow) ; | |
} | |
} | |
} //Reportiterator | |
} //Kampagneniterator | |
if (!cmpFound) cResults = "NIX" ; | |
return cResults; | |
} | |
/**************** Helper ******************/ | |
//Ergebnisse für Log oder Mail ausgeben | |
function formatResults(arr, tp, forLog) { | |
var res = "" ; | |
if (arr === 'NIX') return ""; | |
var doHtmlTitle = true ; | |
if (tp === 'S') { | |
var nam = "Aktive Produkte"; | |
var hdr = ['Kampagne', 'Anzeigengruppe', 'Produkt', 'Marke', 'Prod-Typ', 'Kategorie', 'Klicks', 'CTR', 'CPC', 'Kosten', 'Conv.', 'Kost./Conv.', 'Umsatz', 'Umsatz/Kosten'] ; | |
} else { | |
var nam = "Aktive Keywords"; | |
var hdr = ['Kampagne', 'Anzeigengruppe', 'Keyword', 'Klicks', 'CTR', 'CPC', 'Kosten', 'Position', 'Conv.', 'Kost./Conv.', 'Umsatz', 'Umsatz/Kosten'] ; | |
} | |
if (chkType === 'REL') | |
var ttl = nam + " mit Verhältnis von Umsatz/Kosten unter "+trshCnvCostRelReport + ' und mindestens '+trshClicks+' Klicks' ; | |
else if (chkType === 'CPA') | |
var ttl = nam + " mit Kosten/Conversion über "+trshCpaReport + ' und mindestens '+trshClicks+' Klicks' ; | |
else | |
var ttl = nam + " mit weniger als "+trshCnvReport + ' Conversions und mindestens '+trshClicks+' Klicks' ; | |
ttl += " im Zeitraum "+chkDateRange; | |
if (forLog) { | |
res = "\n"+ttl + ":\n" ; | |
if (arr.length == 0) res += "- Keine Einträge -" ; else { | |
res += hdr.join('; ').replace(/<br>/g,"") + '\n'; | |
for (var i=0;i<arr.length;i++) { | |
if(typeof arr[i] === 'string') | |
res += arr[i] + '\n'; | |
else | |
res += arr[i].join('; ') + '\n'; | |
} | |
} | |
} else { | |
if (doHtmlTitle) { | |
res = "\n<h3>" + ttl + "</h3>\n"; | |
if (arr.length == 0) res += "<small>Keine Einträge</small>\n" ; else { | |
res += "<table>\n"; | |
if (hdr.length > 0) res += "<tr><td class=\"first\"><b>"+ hdr.join('</b></td>\t<td><b>') + '</b>\t</td></tr>\n'; | |
for (var i=0;i<arr.length;i++) { | |
if(typeof arr[i] === 'string') | |
res += "<tr><td class=\"first\">" + arr[i] + "</td>\t</tr>\n"; | |
else | |
res += "<tr><td class=\"first\">"+arr[i].join('</td>\t<td>').replace(/\n/gm,"<br>") + '</td>\t</tr>\n'; | |
} | |
res += "</table>\n"; | |
} | |
} | |
} | |
return res ; | |
} | |
//angefordertes Label anlegen, wenn noch nicht vorhanden | |
function needsLabel(lbl) { | |
if (!(AdWordsApp.labels().withCondition("Name = '" + lbl + "'").get().hasNext())) { | |
AdWordsApp.createLabel(lbl, "Markiert Daten für Controlling / Steuerung per Script. " + | |
"Fragen dazu? Kontakt unter www.gandke.de)", "#C44E00"); | |
//Da das Label hier ggf. schon direkt gebraucht wird, aber nicht zwingend schon genutzt werden kann, lieber noch ein wenig warten... | |
Utilities.sleep(1000); | |
} | |
return true ; | |
} | |
function w2log(txt) { | |
//Zu viele Details im Log bringen nichts - daher hier jeden Eintrag kürzen. Um Überlauf zu vermeiden ggf. hier als Ergänzung | |
//die Gesamtlänge aller Einträge beobachten und begrenzen | |
if (txt.length > (4096)) { | |
Logger.log(txt.substr(0, 4000)+"...\n\nACHTUNG:Text für Log gekürzt!!!") ; | |
return false; | |
} else { | |
try {Logger.log(txt) ;} catch(e) { } | |
return true; | |
} | |
} | |
//Ausgleich der ggf. bestehenden Zeitdifferenz zwischen Zeitstempel des | |
//ausf. Systems (PST) und Planungszeit im Konto | |
function getAccountCurrentDateTime() { | |
return new Date(Utilities.formatDate(new Date(), AdWordsApp.currentAccount().getTimeZone(), "MMM dd,yyyy HH:mm:ss")); | |
} | |
//Label ohne Fehlermeldungen in der Vorschau bei fehlendm Label hinzufügen | |
function addLbl(entity, lblname) { | |
if (!(entity.labels().withCondition("Name = '" + lblname + "'").get().hasNext())) | |
try { entity.applyLabel(lblname) ; } catch(e) { } ; | |
} | |
function getValue(v) { | |
return parseFloat(v.toString().replace(/,/g,'')); | |
} | |
function formatInt(number) { | |
number = number || 0; | |
thousand = ","; | |
var negative = number < 0 ? "-" : "", | |
i = parseInt(number = Math.abs(+number || 0).toFixed(0), 10) + "", | |
j = (j = i.length) > 3 ? j % 3 : 0; | |
return negative + (j ? i.substr(0, j) + thousand : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + thousand) ; | |
} | |
function formatFloat(amount) { | |
var delimiter = ","; // replace comma if desired | |
amount = amount.toFixed(2).toString() ; | |
var a = amount.split('.',2) | |
var d = a[1]; | |
var i = parseInt(a[0]); | |
if(isNaN(i)) { return ''; } | |
var minus = ''; | |
if(i < 0) { minus = '-'; } | |
i = Math.abs(i); | |
var n = new String(i); | |
var a = []; | |
while(n.length > 3) | |
{ | |
var nn = n.substr(n.length-3); | |
a.unshift(nn); | |
n = n.substr(0,n.length-3); | |
} | |
if(n.length > 0) { a.unshift(n); } | |
n = a.join(delimiter); | |
if(d.length < 1) { amount = n; } | |
else { amount = n + '.' + d; } | |
amount = minus + amount; | |
return amount; | |
} |
Hinweis zum Update vom Januar 2020
Da nach wie vor kein Zugriff auf Smart Shopping Kampagnen via Ads Scripts besteht, wohl aber in den Performance Reports Daten zu diesen Kampagnen abgerufen werden können, ist als Workaround eine Möglichkeit geschaffen worden, Namen von Smart Shopping Kampagnen selbst zu definieren, wenn diese ebenfalls untersucht werden sollen.
Dazu können Namen von Smart Shopping Kampagnen als Array in der Variable setupSmartCampaignNames
eintragen werden, welche zusätzlich zu normalen Shopping Kampagnen untersucht werden sollen.
Fundstellen werden dann ausgewiesen, aber es besteht keine Information darüber, ob das Produkt bereits deaktiviert wurde oder nicht. Daher speziell in Verbindung mit "ALL_TIME" als Datumsbereich nervig, weil Einträge nie aus dem Bericht verschwinden werden, wenn auch das Produkt deaktiviert sein mag.
Das kann wieder umgestellt werden, wenn denn irgendwann auch Zugriff auf die Smart Kampagnen besteht, aber zum aktuellen Zeitpunkt sind diese nach wie vor "unsichtbar", wenn man generell Kampagnen oder gezielt Shopping Kampagnen oder Shopping Anzeigengruppen abrufen möchte - Sorry.
Google AdWords Script zur Identifizierung schlechter Keywords in Suchkampagnen und Produkten in Shopping-Kampagnen
Das Script sucht Keywords und Produkte, die bei Erreichen eines Schwellwertes an Klicks die definierten Ziele anhand CR, CPL oder ROAS nicht erreicht haben und listet diese auf bzw, pausiert Keywords, die unter einen zweiten Schwellwert fallen.
Voraussetzungen
Conversiontracking muss für die zu untersuchenden Kampagnen stattfinden und für ROAS-Steuerung müssen auch Umsätze messbar sein.
Installation
Nachdem mindestens eine geeignete Kampagne im Konto mit einem entsprechenden Label versehen wurde, kann das Script getestet und anschließend eingesetzt werden, Es können Shopping- oder Suchnetzwerk-Kampagnen berücksichtigt werden.
Der Scriptcode wird im Konto unter “Gemeinsam genutzte Bibliothek -> Bulk-Vorgänge -> Scripts” eingetragen. Dazu auf “+Script” klicken, einen Namen vergeben und den kopierten Scriptcode (siehe unten) über die Zwischenablage einfügen. In der Konfiguration sind für einen ersten Durchlauf keine Einstellungen neben der bei der bzw. den Kampagne(n) verwendeten Label-Bezeichnung erforderlich. Bevor das Script gestartet oder die Vorschau genutzt werden kann, ist aber noch eine Autorisierung erforderlich.
Verwendung
Wird das Script gestartet oder (nach Anlage des erforderlichen Labels) in der Vorschau betrachtet, werden anhand der definierten Schwellwerte und der gewählten Art der Steuerung (siehe Kommentare im Script) Keywords oder Produkte identifiziert, die die Schwellwerte zur Leistung unterschreiten und in einem Report ausgegeben bzw. ggf. direkt pausiert (nur Keywords , keine Produkte). Nach Abschluss wird das Ergebnis entweder (debug=true) im Protokoll oder per Mail (Standardeinstellung) ausgegeben.