Skip to content

Instantly share code, notes, and snippets.

@mbaersch
Last active October 20, 2020 08:36
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save mbaersch/276f6a821e0770ce596141cece98a750 to your computer and use it in GitHub Desktop.
Save mbaersch/276f6a821e0770ce596141cece98a750 to your computer and use it in GitHub Desktop.
Identifizieren von schlecht performenden Keywords und Produkten in AdWords
/**************************************************************/
/****** "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 = "mailadresse@hier.eintragen";
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 == "mailadresse@hier.eintragen")) {
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;
}
@mbaersch
Copy link
Author

mbaersch commented Dec 2, 2017

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.

@mbaersch
Copy link
Author

mbaersch commented Jan 9, 2020

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment