DAS Werkzeug für den Tag Manager entdecken.Zum GTMSPY

Race Condition
Tracken Sie Ihre AdWords-Änderungen mit Labels und Slack
Google Ads

1. Oktober 2018

Tracken Sie Ihre AdWords-Änderungen mit Labels und Slack

Auch ohne Experimente nach Änderungen auf dem Laufenden bleiben - so geht's

Trotz der Tatsache, dass Google die neue AdWords-Benutzeroberfläche mit der damals mit Spannung erwarteten Notizen-Funktion veröffentlicht hat, ist es immer noch nicht einfach, all die kleinen Änderungen und potentiellen Optimierungen im Auge zu behalten, die Sie in der Vielzahl von Konten vornehmen, die Sie wahrscheinlich verwalten.

Wenn Sie natürlich mal vorhaben, einen neuen Kampagnenaufbau mit viel Traffic zu testen, richten Sie dafür einfach ein Experiment ein und los geht's.

Aber was ist mit all den kleinen Dingen, die Sie auf dem sonstigen Weg machen? Ich meine, besonders bei den gut etablierten Konten, die einfach "nur laufen". Ich spreche davon, hier ein weiteres Keyword hinzuzufügen, dort einen Gerätemodifikator zu setzen und von Zeit zu Zeit neue Anzeigen einzufügen.

Die neue Notizfunktion ist nicht geeignet, Ihnen bei der Überwachung solcher Änderungen zu helfen. Und manuelles Notieren in Excel Dateien oder sonstigem ist auch nicht die Lösung. Warten Sie. Aber warum?

Weil es je nachdem, welche Entität Sie berühren, d.h. Keywords mit hohem oder niedrigem Volumen, einen großen Unterschied macht, wann es sinnvoll ist, einen Blick darauf zu werfen, wie sich die Entität nach Ihren Änderungen verhalten hat. Sie ändern das Keyword X, und X erzeugt 100 Klicks pro Tag →. Sie kontrollieren das Keyword besser noch am selben Tag. Sie ändern das Keyword Y, und Y erzeugt 30 Klicks pro Monat → Entspannen Sie sich eine Weile.

Wer also wirklich in Konten springen und all diese kleinen Änderungen auf täglicher Basis kontrollieren will, muss jedes Mal zuerst prüfen, ob nach den Änderungen für jede einzelne Änderung eine ausreichende Anzahl von Klicks erfolgt ist.

Zu Ihrer Rettung dieser operativen Challenge stelle ich Ihnen die Leistungsfähigkeit von AdWords-Skripts, der Google Firebase Datenbank und von Slack vor, Ihrem bevorzugten Kommunikationstool, das sogar Google in Kombination mit AdWords vorschlägt.

Sie erhalten das Skript und eine kurze Einrichtungsanleitung in einer Sekunde, lassen Sie mich nur kurz erklären, wie es funktioniert.

Sie installieren das Skript und lassen es stündlich in Ihrem AdWords-Konto laufen. Sie werden ein Label in Ihrem Konto haben, mit dem Sie alle Entitäten kennzeichnen (standardmäßig werden Keywords, Adgroups und Anzeigen unterstützt), die Sie überwachen möchten. Lassen Sie uns dieses Label vorerst CONTROL nennen.

Sie ändern also ein Keyword in irgendeiner Weise und setzen dann die Bezeichnung CONTROL auf dieses Keyword. Wenn das Skript das nächste Mal ausgeführt wird, bemerkt es, dass Sie ein neues Keyword mit CONTROL gekennzeichnet haben, und es schreibt die Uhrzeit und einige andere Details in eine Datenbank. Jetzt wird das Skript bei jeder Ausführung einen Blick in Ihre CONTROL-Entitäten werfen, um zu sehen, ob sie eine Trigger-Bedingung auslösen. Standardmäßig kennt das Skript zwei Arten von Triggern, zeit- und statistikbasierte.

Ein statistikbasierter Trigger ist standardmäßig clicks|30, was bedeutet, dass Sie Leistungsstatistiken in Slack erhalten, sobald die Entität 30 Klicks nach Erhalt des CONTROL-Labels erhalten hat.

Der zeitbasierte Trigger ist standardmäßig Wochen|1-4, d.h. ab 1 Woche, nachdem die Entität das Label erhalten hat, werden Leistungsstatistiken wöchentlich an Ihren Slackchannel gesendet, bis 4 Wochen vergangen sind.

Die Statistiken im Slack werden den Zeitraum, der nach dem Setzen des Labels verstrichen ist, mit dem Zeitraum derselben Länge vor dem Setzen des Labels vergleichen. Für den statistikbasierten Trigger kann dies sehr dynamisch sein, wobei ein Tag das kleinste Intervall ist. Der zeitbasierte Standard-Trigger vergleicht immer volle Wochen (7 Tage).

Der folgende Screenshot zeigt eine lockere Ausgabe für ein Keyword, das neu erstellt wurde und das CONTROL-Label erhalten hat (deshalb zeigen alle Vergleichsstatistiken 0).

Wenn Sie die Überwachung einer Entität beenden wollen, entfernen Sie einfach das Label.

Setup Steps

Loggen Sie sich bei Google Firebase ein (es ist kostenlos), erstellen Sie ein Projekt und eine Echtzeit-Datenbank innerhalb dieses Projekts. Bearbeiten Sie die Datenbankregeln, um nur authentifizierten Zugriff zu erlauben:

{
 “rules”: {
 “.read”: “auth != null”,
 “.write”: “auth != null”
 }
}

Notieren Sie die Datenbank-URL und das Datenbankgeheimnis:

Erstellen Sie eine Slack-App für Ihren Slack-Arbeitsbereich und aktivieren Sie eingehende Webhooks. Beachten Sie die Hook-URL, die Sie unter Apps Management → Custom Integrations → Incoming WebHooks finden.

Kopieren Sie das Skript am Ende dieses Beitrags und fügen Sie es zu Ihrem AdWords-Konto hinzu, geben Sie dazu die erforderlichen Firebase- und Slack-Konfigurationsdaten ein.

Konfigurieren Sie den statistikbasierten Trigger und den zeitbasierten Trigger nach Ihren Wünschen.

Aktivieren Sie die stündliche Ausführung für das Skript.


Sie können jetzt Entitäten in Ihrem Konto kennzeichnen und automatisch Statistiken in Slack erhalten. Für die beste Erfahrung sollten Sie die Slack-Ausgabe ein wenig stylen :) Alle benötigten Informationen werden von Slack bereitgestellt.

Code

/**
 * AdWords Performance Monitoring in Slack via Labels
 * @author: Dustin Recko
 *
 */

// Config Section //>

var DB_URL = 'https://...'; // The Firebase Database URL
var DB_AUTH = 'xxx'; // The Firebase Database Secret

var SLACK_HOOK = 'https://...'; // The Slack Hook URL
var SLACK_CHANNEL = '#adwords'; // The Slack Channel
var SLACK_EMOJI = ':smile:'; // The Slack Emoji

var TRIGGER = {
  clicks: 30, //Stats as a trigger with a specified threshold
  weeks: [1,4] //Time as a trigger with a start and end threshold
};

var LABEL_NAME = "CONTROL"; // The name of the label used in AdWords to activate monitoring for Keywords, AdGroups, and Ads

var NOW = new Date();

// End of Config <//

function main() {

  init();

  var ACC = AdWordsApp.currentAccount().getName().split(" ")[0];

  var myDb = new firebase(DB_URL,DB_AUTH);
  var myDbJson = myDb.getJson(LABEL_NAME+'/'+ACC) || nest({},[LABEL_NAME,ACC]);

  var mySlack = new slack(SLACK_HOOK,SLACK_CHANNEL,SLACK_EMOJI);

  var myLabel = AdWordsApp.labels().withCondition("Name = '"+LABEL_NAME+"'").get().next();

  var process = {
    "keywords": myLabel.keywords().get(),
    "adGroups": myLabel.adGroups().get(),
    "ads": myLabel.ads().get()
  };
  
  /// Main process

  for(var handler in process) {
    
    /// Check items with the label

    while(process[handler].hasNext()) {

      var h = process[handler].next();

      if(!myDbJson[handler] || !myDbJson[handler][h.getId()]) {

        var obj = {
          "name"    : (h.getText instanceof Function) ? h.getText() : ((h.getDescription1 instanceof Function) ? h.getDescription1() : h.getName()),
          "campaign": h.getCampaign().getName(),
          "qsStart"  : (h.getQualityScore instanceof Function) ? h.getQualityScore() : 0,
          "started" : NOW.getTime(),
          "trigger": initTrigger(TRIGGER)
        };
        myDb.patch(obj,LABEL_NAME+'/'+ACC+'/'+handler+'/'+h.getId());
        myDbJson = nest(myDbJson,[handler,h.getId()]);
        
      } else {

        for(var i in TRIGGER) {
          switch(typeof(myDbJson[handler][h.getId()].trigger[i])) {
            case "boolean":        
              if(!myDbJson[handler][h.getId()].trigger[i]) {
                if(statsBasedCheck(handler,h,i,myDbJson[handler][h.getId()])) {
                  var status = {};
                  status[i] = true;
                  myDb.patch(status,LABEL_NAME+'/'+ACC+'/'+handler+'/'+h.getId()+'/trigger');
                }
              }
              break;
            case "number":
              var weeksPassed = Math.round((NOW.getTime() - myDbJson[handler][h.getId()].started)/(1000*60*60*24)) / 7;
              if(weeksPassed%1 === 0 && TRIGGER[i][0] <= weeksPassed && weeksPassed <= TRIGGER[i][1] && weeksPassed > myDbJson[handler][h.getId()].trigger[i]) {
                timeBasedCheck(handler,h,i,myDbJson[handler][h.getId()]);
                var status = {};
                status[i] = weeksPassed;
                myDb.patch(status,LABEL_NAME+'/'+ACC+'/'+handler+'/'+h.getId()+'/trigger');
              }
              break;
          }
        }
      }
      /// Flag processed items
      myDbJson[handler][h.getId()].flag = true;
    }
      /// Cleanup no longer labelled items, i.e., non-flagged
      for(var i in myDbJson[handler]) {
      if(myDbJson[handler][i].flag == undefined)
        myDb.purge(LABEL_NAME+'/'+ACC+'/'+handler+'/'+i);
    }
  }


  /// Some functions
  
  function init() {
    Date.prototype.yyyymmdd = function(days) {
      if(days) {
        this.setDate(this.getDate() + days);
      }
      return Utilities.formatDate(this, AdWordsApp.currentAccount().getTimeZone(),'yyyyMMdd');
    }
  }

  function firebase(db,auth) {
    this.db = db;
    this.auth = auth;

    this.patch = function(payload,path) {
      path = path+'/.json?auth=';
      var options = {
        "method"  : "patch",
        "payload" : JSON.stringify( payload )
      };
      UrlFetchApp.fetch(this.db+path+this.auth,options);
    }

    this.purge = function(path) {
      path = path+'/.json?auth=';
      var options = {
        "method": "delete"
      };
      UrlFetchApp.fetch(this.db+path+this.auth,options);
    }

    this.getJson = function(path) {
      path = path+'/.json?auth=';
      return JSON.parse(
        UrlFetchApp
        .fetch(this.db+path+this.auth)
        .getContentText()
      );
    }
  }

  function slack(hook,channel,emoji) {

    this.hook = hook;
    this.channel = channel;
    this.emoji = emoji;

    this.msg = function(payload) {
      payload.channel = this.channel;
      payload.icon_emoji = this.emoji;
      var options = {
        method: "POST",
        contentType: 'application/json',
        payload: JSON.stringify(payload)
      };
      UrlFetchApp.fetch(this.hook,options);
    }
  }

  function initTrigger() {
    var obj = {};
    for(var i in TRIGGER) {
      if(typeof(TRIGGER[i]) == "number")
        obj[i] = false
      else
        obj[i] = 0
    }
    return obj;
  }

  function statsBasedCheck(type,handler,trigger,dbData) {

    var sh = handler.getStatsFor(new Date(dbData.started).yyyymmdd(),NOW.yyyymmdd());

    var stats = {
      avgCpc: sh.getAverageCpc().toFixed(2),
      avgPos: sh.getAveragePosition(),
      clicks: sh.getClicks(),
      conversions: sh.getConversions(),
      cost: sh.getCost(),
      qs: (type == 'keywords') ? handler.getQualityScore() : 0
    };

    if(stats[trigger] >= TRIGGER[trigger])  {

      var daysPassed = Math.round((NOW.getTime() - dbData.started)/(1000*60*60*24));

      var sh = handler.getStatsFor(new Date(dbData.started).yyyymmdd(daysPassed*-1),new Date(dbData.started).yyyymmdd());

      var beforeStats = {
        avgCpc: sh.getAverageCpc().toFixed(2),
        avgPos: sh.getAveragePosition(),
        clicks: sh.getClicks(),
        conversions: sh.getConversions(),
        cost: sh.getCost(),
        qs: dbData.qsStart
      };

      var attachments = [];
      for(var s in stats) {
        attachments.push({
          title: s,
          text: 'is '+stats[s]+ (stats[s]>=beforeStats[s] ? ' (up' : '(down') +' from '+beforeStats[s]+')'
        });
      }

      mySlack.msg({
        text: LABEL_NAME+" > "+ACC+" > "+type+" > "+dbData.name+" in "+dbData.campaign+" passed "+stats[trigger]+" "+trigger+" in "+daysPassed+" days (was "+beforeStats[trigger]+" in the previous period)",
        attachments: attachments
      });

      return true;
    }

    return false;
  }

  function timeBasedCheck(type,handler,trigger,dbData) {

    var sh = handler.getStatsFor(new Date(dbData.started).yyyymmdd(),NOW.yyyymmdd());

    var stats = {
      avgCpc: sh.getAverageCpc().toFixed(2),
      avgPos: sh.getAveragePosition(),
      clicks: sh.getClicks(),
      conversions: sh.getConversions(),
      cost: sh.getCost(),
      qs: (type == 'keywords') ? handler.getQualityScore() : 0
    };

    var sh = handler.getStatsFor(new Date(dbData.started).yyyymmdd((dbData.trigger[trigger]+1)*-7),new Date(dbData.started).yyyymmdd());

    var beforeStats = {
      avgCpc: sh.getAverageCpc().toFixed(2),
      avgPos: sh.getAveragePosition(),
      clicks: sh.getClicks(),
      conversions: sh.getConversions(),
      cost: sh.getCost(),
      qs: dbData.qsStart
    };

    var attachments = [];
    for(var s in stats) {
      attachments.push({
        title: s,
        text: 'is '+stats[s]+ (stats[s]>=beforeStats[s] ? ' (up' : '(down') +' from '+beforeStats[s]+')'
      });
    }

    mySlack.msg({
      text: LABEL_NAME+" > "+ACC+" > "+type+" > "+dbData.name+" in "+dbData.campaign+" passed "+(dbData.trigger[trigger]+1)+" "+trigger,
      attachments: attachments
    });

  }
    
  // by Jason Knight
  function nest(base,arr) {
    for (var obj = base, ptr = obj, i = 0, j = arr.length; i < j; i++)
      ptr = (ptr[arr[i]] = {});
    return obj;
  }

}