Last active
October 20, 2020 08:36
-
-
Save mbaersch/276f6a821e0770ce596141cece98a750 to your computer and use it in GitHub Desktop.
Identifizieren von schlecht performenden Keywords und Produkten in AdWords
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/**************************************************************/ | |
/****** "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; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.