Shoppingfeed Monitoring mit AdWords Scripts: Fehler & Potentiale auf einen Blick

norisk ScriptLab MerchantCenter

****

TL;DR: Dieser Artikel beschreibt einen skriptbasierten Ansatz zum Monitoring verteilter Merchant Center Konten auf Basis von Google Spreadsheets. Die Übersicht führt Uploadprozesse, Fehler, Warnungen und Vorschläge aller verwalteten Konten zusammen. Nachdem das Skript bereits auf der SMX und OMLIVE 2017 gefeatured wurde, hier nun der ausführliche Blogpost.

****

Google Merchant Center – Wie behält man bei vielen Konten die Übersicht?

Das Google Merchant Center hat im Laufe des Jahres 2016 ein überfälliges Redesign erfahren: Die Steuerflächen sind größer und sichtbarer, die linke Navigation setzt sich durch den schwarzen Hintergrund farblich besser ab. Insgesamt in jeder Hinsicht eine Verbesserung. Nichtsdestotrotz sehen wir bei norisk in der täglichen SEA Agenturarbeit zwei Kernherausforderungen in mit dem Merchant Center:

  1. Kontenübergreifende Übersicht von Problemen und Potentialen Eurer Shoppingfeeds
  2. Zusammenführung separater MCCs und Einzelkonten in eine Übersicht

 

Für beide Herausforderungen werden wir in diesem Artikel eine Lösung mittels AdWords Scripts bereitstellen, um eine Multitab-Übersicht in einem Google Spreadsheet zu erhalten. Beginnen wir mit der ersten Herausforderung und einer Hinführung zur Lösung.

1. Kontenübergreifende Übersicht von Problemen und Potentialen

Die Einstiegsseite “/aggregatordashboard” bildet den Startpunkt bei Mutliclient-Merchantkonten, obwohl es den Namen Dashboard nicht verdient: Es ist lediglich eine verlinkte Kontenliste und führt keine Informationen zusammen.

Es fehlt eine Dashboardübersicht aller Shoppingfeedprobleme

Folgende operativen Fragen bleiben noch unbeantwortet, welche erst beim Klick in das jeweilige Einzelkonto geklärt werden können:

  • Wie viele Artikel wurden zuletzt importiert?
  • Wie viele Artikel werden aktuell wegen Fehlern abgelehnt?
  • Wie setzen sich die Ablehnungen aus Fehlertypen zusammen?
  • Was sind Beispielartikel für Ablehnungen?
  • Welche Warnungen gibt es aktuell pro Konto?

 

All diese wichtigen Informationen gilt es, in einem Dashboard (welches den Namen verdient) täglich zusammenzuführen, um mehrere Konten zentral mit Fokus auf Abweichungen und Potentiale zu beobachten. Beginnen wir zunächst mit dem einfacheren Fall, dass alle Merchant-Center Konten in einem Überkonto zusammengeführt sind – wie oben abgebildet. Zum umständlicheren Fall verteilter Konten kommen wir im Teil 2. der Zusammenführung separater Einzelkonten.

Multi-Tab Spreadsheet als Merchant Center Übersicht

Auch wenn Merchant Center Überkonten streng genommen nicht „MCCs” genannt werden, leihen wir uns der Einfachheit halber diese Abkürzung von AdWords. Grundlage für das anvisierte Monitoring ist ein Google Spreadsheet als Ausgabeort, um tabellarisch alle gewünschten Informationen zusammenzuführen. Die Daten werden über AdWords Scripts mittels der aktivierten Content API for Shopping abgerufen, welche eine einfache Abstraktion der Merchant-Daten bereitstellt. Das nachfolgende Demo-Spreadsheet steht – zusammen mit dem Skript – zur Kopie bereit und ist in vier Datenblätter aufgeteilt:

1. FEEDS. Dieses Tab enthält alle aggregierten Informationen pro Feed: Gesamtanzahl, valide Produkte, Fehler, letzter Import etc. Shopping API-seitig bedient sich das Skript hier Informationen der Endpoints Accounts und Datafeeds.

 

Ein wertvoller Nebeneffekt der Versionsspeicherung in Google Sheets ist der langfristige Rückblick auf historische Feedmengen pro Tag. Diese Funktion ist über „Datei > Überarbeitungsverlauf anzeigen“ einsehbar. Oft speichern Produktdatentools nur kurze Zeiträume wie 7 Tage, das Google Merchant Center zeigt zumindest 30 Tage. Unten ist erkennbar, dass ein Shop sukzessive weniger Artikel an Shopping übergibt, die Zahlen fallen von 7573 im Januar auf 4631 im Juli. Diese Versionsspeicherung ist immer dann wertvoll, wenn ein Wert aus der weiter zurückliegenden Vergangenheit benötigt wird.

2. Errors. Hier werden alle konkreten Fehlerfälle mit Error Message, Code, Fehleranzahl und jeweils drei Beispielen aufgeführt. Die Informationen werden API-seitig von Datafeedstatuses geholt, im Merchant Center werden Fehler durch das rote Dreieck  angezeigt. Fehler führen zu einer direkten Ablehnung des Artikels im Merchant Center. Beispiel sind für Fehler sind:

  • Fehlende oder falsche GTINs
  • zu kleine Bilder
  • Nicht übereinstimmende Preise
  • Fehlende Attribute, z.B. Farbe

 

 

3. Warnings. In diesem Tab werden alle Warnungen – keine Fehler bzw. Ablehnungen – mit den gleichen Informationen wie in Tab 2 gelistet. Die Daten stammen ebenfalls aus den Datafeedstatuses, im GMC-Interface sieht man ein gelbes Dreieck . Beispiele für Warnungen sind:

  • GTIN mit eingeschränktem Verwendungsbereich oder „Gutschein-GTIN“
  • Fehlende Attribut, z.B. gender
  • Mehrere Werte für ein Variantenattribut

 

4. DataQualityIssues. Diese von der Shopping-API unter AccountStatuses bereitgestellte Liste mischt Fehler und Warnungen mit weiteren wichtigen Informationen. Im Merchant Center sind sie mit einem blauen Infosymbol  gekennzeichnet:

  • Issue severity “critical” & “error” = Fehler bzw. Ablehnung, s.o.
  • Issue severity “suggestion” = Vorschläge zur Optimierung

 

Insbesondere die Suggestion-Einträge sind wertvolle Ausgangspunkte für kleine und unkritische Verbesserungen in Shoppingfeeds. Die anderen beiden Issuetypen sind eher eine Duplikation von Fehlern und Warnungen.

Welche Alternativen gibt es für Shoppingfeed-Monitoring?

Es gibt auch andere Wege, detaillierte Merchant Center-Informationen für ein tägliches Monitoring zu beziehen:

  • AdWords zeigt für Shoppingkampagnen im Tab „Produkte” den Zustand jedes Produkts an
  • Produktfeedplattformen wie Productsup ermöglichen den Import der Shopping API-Daten in ihre Systeme

In beiden Fällen werden die Informationen – zu einem gewissen Grad – tiefer im Frontend der jeweiligen Tools begraben, was in Anbetracht der Wichtigkeit von Shoppingfehlern nicht zielführend ist. Auch fehlt eine direkte Emailfunktionalität zur täglichen Übersicht.

Unser AdWords Skript zum Download

Nun zum eigentlichen Skript, welches die Abfrage und Befüllung ausführt. Zunächst ist wichtig zu erwähnen, dass das Skript bevorzugt als ein MCC-Skript genutzt werden sollte, da es so von uns eingesetzt wird und in Betrieb ist. Theoretisch sollte es mit dem gleichen User auf Einzel-Adwordskonto-Ebene auch das gleiche Ergebnis liefern – der Logik halber ist eine Verknüpfung auf AdWords MCC Ebene zum korrespondierenden Merchant-Center MCC intuitiver. Das Skript erwartet in der Konfiguration zwei Inputwerte:

1. Merchant Center MCC-ID (Überkonto). Diese lässt sich aus der URL der Startseite kopieren, siehe Screenshot

>> WICHTIGER Schritt: Jetzt das Demosheet kopieren. Wir warten solange 😀

2. Spreadsheet-ID. Diese wird aus der URL Eures angelegten, eigenen Spreadsheets kopiert – nach dem “/d/” Verzeichnis und vor “edit”.  Die ID des Demosheets wird wegen Berechtigung einen Fehler auswerfen, sowie jegliche Werte mit Leerzeichen oder “/” am Ende.

Die weiteren Variablen Sheetnames sollte eher NICHT angepasst werden, da diese mit denen im Spreadsheet übereinstimmen müssen. Die Variable SCRIPT_NAME dient nur der Beschreibung für Sheet und Email und müssen nicht zwingend beibehalten werden.

Skriptcode für MCC zum Download

Hier ist der CODE zum Copy&Pasten in Euer AdWords MCC, :

https://github.com/norisk/AdWords-Scripts/blob/master/2017_GMC_MCC_MerchantCenterMonitoring_v1.1.1

/**
*   Merchant Center Dashboard for MCC
*   Version: 1.1.1
*   @author: Christopher Gutknecht
*   norisk GmbH
*   [email protected]
*
*   Demo-Spreadsheet to copy: https://docs.google.com/spreadsheets/d/1SD-1xOeRxk5BRkGc-6WdFy2JQOMrC6ZqZguVoHoH-kQ/edit?usp=sharing
*   Detailed Blog post soon here: https://www.noriskshop.de/category/adwords-scripts/ 
*   updates 1.1.1: skips accounts with no feeds @TODO: generic skip account rule 
*
*
* THIS SOFTWARE IS PROVIDED BY norisk GMBH ''AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL norisk GMBH BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
// Reference https://developers.google.com/shopping-content/v2/reference/v2/


function main() {
  
  var agencyAccountId = '10000000'; // If you have multiple merchant center accounts, enter your TOPLEVEL account ID here
  var SPREADSHEET_ID = 'SPREADSHEET-ID'; // The spreadsheet ID is part of the spreadsheet URL, ie docs.google.com/spreadsheets/d/ SPREADSHEET-ID /edit
  var SHEETNAMES = ['feeds', 'errors', 'warnings','dataQualityIssues']; // Name your spreadsheets tabs accordingly
  var SCRIPT_NAME = 'MerchantCenterDashboard';
  
  // 1. Write to Feed Sheet
  var accountArray = [];
  accountArray = getAccountArray(agencyAccountId); 
  Logger.log(accountArray);
  
  var feedArray = [];
  feedArray = getFeedArray(accountArray);
  
  var feedColumnHeaders = [['Account Name', 'Country', 'Filename', 'Items Total', 'Items Valid', 'Item Errors', 'Status', 'Last Update', 'Feed Schedule']];
  var errorColumnHeaders = [['Account Name', 'Feed Id', 'Error Message', 'Error Code', 'Nr Errors', 'Error Examples']];
  var warningColumnHeaders = [['Account Name', 'Feed Id', 'Warning Message', 'Warning Code', 'Nr Warnings', 'Warning Examples']];
  var dataQualityIssueHeaders = [['Account Name', 'Issue Severity', 'Nr Issues', 'Issue Type', 'LastChecked', 'Data Quality Issue Examples']];
  
  
  var ss = SpreadsheetApp.openById(SPREADSHEET_ID);
  printDataToSheet(SHEETNAMES[0], feedColumnHeaders, feedArray, 4);
  
  // 2.Write to Error Sheet
  var feedErrorArray = getFeedErrorArray(accountArray);
  printDataToSheet(SHEETNAMES[1], errorColumnHeaders, feedErrorArray, 5);
  
  // 3.Write to Warning Sheet
  var feedWarningArray = getFeedWarningArray(accountArray);
  printDataToSheet(SHEETNAMES[2], warningColumnHeaders, feedWarningArray, 5);
 
  // 4.Write to Data Quality Issue Sheet
  var feedDataQualityArray = getFeedDataQualityArray(agencyAccountId, accountArray);
  printDataToSheet(SHEETNAMES[3],dataQualityIssueHeaders, feedDataQualityArray, 3);
  
  ////////////////////////////////////
  ////////////////////////////////////
  //// Function Definitions
  ////////////////////////////////////
  ////////////////////////////////////
  
  /**
  * Returns a list of GMC accounts for a given client center ID.
  * @param {String} a client center/agency account ID.
  * @return {Array} A twodimensional array of account names and IDs.
  */
  function getAccountArray(agencyAccountId) { 
    var accounts = ShoppingContent.Accounts.list(agencyAccountId);
    for(i=0;i < accounts.resources.length; i++) {      
      var accountData = [accounts.resources[i].name, accounts.resources[i].id];
      accountArray.push(accountData);
    }
    return accountArray;
  }
  
  /**
  * Return a list of feeds for given account array.
  * @param {Array} a twodimensional list of account names and IDs.
  * @return {Array} A twodimensioal array of feeds and related information.
  */
  function getFeedArray(accountArray){
    for(i=0;i<accountArray.length;i++) {
      var datafeeds = ShoppingContent.Datafeeds.list(accountArray[i][1]);
      // Checks if there are any datafeeds in account, if not continue with next account.
      if(!datafeeds.resources)
        continue;     
      for(j=0;j<datafeeds.resources.length;j++) {
        Logger.log("datafeeds.resources: " + datafeeds.resources[j]);
        Logger.log("dataFeedsStatuses: " + dataFeedsStatuses);
        
        var singlefeedInfo = [accountArray[i][0], datafeeds.resources[j].targetCountry,datafeeds.resources[j].fileName];
        var dataFeedsStatuses = ShoppingContent.Datafeedstatuses.get(accountArray[i][1], datafeeds.resources[j].id);
        singlefeedInfo.push(dataFeedsStatuses.itemsTotal,dataFeedsStatuses.itemsValid, dataFeedsStatuses.itemsTotal - dataFeedsStatuses.itemsValid, dataFeedsStatuses.processingStatus, dataFeedsStatuses.lastUploadDate.slice(0,10), 
        // check if scheduling exists
        datafeeds.resources[j].fetchSchedule ? datafeeds.resources[j].fetchSchedule.hour + " Uhr" : "not scheduled"
        );
        feedArray.push(singlefeedInfo);
      } 
    }
    return feedArray;
  } 
  
  /**
  * Prints the respective data array to the specified sheet.
  * @param SHEETNNAME {String} the name of the sheet or tab.
  * @param COLUMNHEADERS {Array} a list of columns names.
  * @param dataArray {Array} a client center/agency account ID.
  * @param sortColumn {Int} the column index to sort by.
  * @return void.
  */
  function printDataToSheet(SHEETNAME,COLUMNHEADERS,dataArray, sortColumn) {
    var sheet = ss.getSheetByName(SHEETNAME);
    sheet.getRange('a2:i1000').clearContent();
    sheet.getRange(2,1,1,COLUMNHEADERS[0].length).setValues(COLUMNHEADERS);
    
    var dataRange = sheet.getRange(3, 1, dataArray.length, dataArray[0].length);
    dataRange.setValues(dataArray);
    var sortRange = sheet.getRange("A:I");
    sortRange.sort({column: sortColumn, ascending: false});
    
    Logger.log(SHEETNAME + ' list successfully printed to ' + SHEETNAME + ' sheet.');  
  }  
  
  /**
  * @param {Array} a twodimensional list of account names and IDs.
  * @return {Array} A twodimensional array of feed errors and related information.
  */
  function getFeedErrorArray(accountArray) {
    var feedErrorArray = [];
    
    for(i=0; i < accountArray.length;i++) { // 1. START Iterate through all GMC accounts > return resources array
      var dataFeedsStatuses = ShoppingContent.Datafeedstatuses.list(accountArray[i][1]);
      if(!dataFeedsStatuses.resources)
        continue;
      for(j=0;j < dataFeedsStatuses.resources.length;j++) { // 2. START Resource Iterator: Iterate through all resources aka feeds   
        if(dataFeedsStatuses.resources[j].errors) { // 3. Check if errors exist
          for(k=0;k < dataFeedsStatuses.resources[j].errors.length; k++) { // 3.a) START Error Iterator: Iterate through all errors
            var singleFeedInfo = [];
            singleFeedInfo.push(accountArray[i][0],dataFeedsStatuses.resources[j].datafeedId,
                                dataFeedsStatuses.resources[j].errors[k].message.substring(0,100).replace("Insufficient product identifiers", "Insuff Product Ident").replace(',','\,'),
                                dataFeedsStatuses.resources[j].errors[k].code.replace(',','\,'),
                                dataFeedsStatuses.resources[j].errors[k].count);
            var errorExampleMax = dataFeedsStatuses.resources[j].errors[k].examples.length > 3 ? 3 : dataFeedsStatuses.resources[j].errors[k].examples.length;
            var exampleConcatString ='';
            
            for(m=0; m < errorExampleMax; m++) { // 3.a.1 START Concatenate error examples
              var singleExampleValue = dataFeedsStatuses.resources[j].errors[k].examples[m].value ? dataFeedsStatuses.resources[j].errors[k].examples[m].value : 'noValue';
              var singleExampleSuffix = m < errorExampleMax - 1 ? '\n' : '';
              var singleExampleString = dataFeedsStatuses.resources[j].errors[k].examples[m].itemId + ' : ' + singleExampleValue.substring(0,15).replace(',','\,') + singleExampleSuffix;
              exampleConcatString += singleExampleString;
            } // 3.a.1 END Concatenate error examples
            singleFeedInfo.push(exampleConcatString);
            feedErrorArray.push(singleFeedInfo);
          } // 3.a) END Error Iterator
        } 
        else { // 3.b)) No Error Default Value
          var singleFeedInfo = [];
          singleFeedInfo.push(accountArray[i][0],dataFeedsStatuses.resources[j].datafeedId, 'no errors', '-', 0, 'no examples');
          feedErrorArray.push(singleFeedInfo);
        } // 3. END Error Iter
      } // 2. END Resource Iter 
    } // 1. END Account Iter
    
    return feedErrorArray; 
  }
  
  
  /**
  * @param {Array} a twodimensional list of account names and IDs.
  * @return {Array} A twodimensional array of feed warnings and related information.
  */
  function getFeedWarningArray(accountArray) {
    var feedWarningArray = [];
    
    for(i=0; i < accountArray.length;i++) { // 1. START Iterate through all GMC accounts > return resources array
      var dataFeedsStatuses = ShoppingContent.Datafeedstatuses.list(accountArray[i][1]);
      if(!dataFeedsStatuses.resources)
        continue;
      for(j=0;j < dataFeedsStatuses.resources.length;j++) { // 2. START Resource Iterator: Iterate through all resources aka feeds   
        if(dataFeedsStatuses.resources[j].warnings) { // 3. Check if warnings exist
          for(k=0;k < dataFeedsStatuses.resources[j].warnings.length; k++) { // 3.a) START warning Iterator: Iterate through all warnings
            var singleFeedInfo = [];
            singleFeedInfo.push(accountArray[i][0],dataFeedsStatuses.resources[j].datafeedId,
                                dataFeedsStatuses.resources[j].warnings[k].message.substring(0,100).replace(',','\,'),
                                dataFeedsStatuses.resources[j].warnings[k].code.replace(',','\,'),
                                dataFeedsStatuses.resources[j].warnings[k].count);
            var warningExampleMax = dataFeedsStatuses.resources[j].warnings[k].examples.length > 3 ? 3 : dataFeedsStatuses.resources[j].warnings[k].examples.length;
            var exampleConcatString ='';
            
            for(m=0; m < warningExampleMax; m++) { // 3.a.1 START Concatenate warning examples
              var singleExampleValue = dataFeedsStatuses.resources[j].warnings[k].examples[m].value ? dataFeedsStatuses.resources[j].warnings[k].examples[m].value : 'noValue';
              var singleExampleSuffix = m < warningExampleMax - 1 ? '\n' : '';
              var singleExampleString = dataFeedsStatuses.resources[j].warnings[k].examples[m].itemId + ': ' + singleExampleValue.substring(0,10).replace(',','\,') + singleExampleSuffix;
              exampleConcatString += singleExampleString;
            } // 3.a.1 END Concatenate warning examples
            singleFeedInfo.push(exampleConcatString);
            feedWarningArray.push(singleFeedInfo);
          } // 3.a) END warning Iterator
        } 
        else { // 3.b)) No warning Default Value
          var singleFeedInfo = [];
          singleFeedInfo.push(accountArray[i][0],dataFeedsStatuses.resources[j].datafeedId, 'no warnings', '-', 0, 'no examples');
          feedWarningArray.push(singleFeedInfo);
        } // 3. END warning Iter
      } // 2. END Resource Iter 
    } // 1. END Account Iter
    return feedWarningArray; 
  }
  
  
  /**
  * @param {string} the account ID of the multi-client merchant center
  * @return {Array} A twodimensional array of feed data quality issues and related information.
  */
  function getFeedDataQualityArray(agencyAccountId, accountArray) {
    
    var feedDataQualityArray = [];
    var accountstatuses = ShoppingContent.Accountstatuses.list(agencyAccountId);
    var accountList = acctArraytToObjArray(accountArray);
    
    for(j=0;j < accountstatuses.resources.length;j++) { // 1. START Resource Iterator: Iterate through all resources aka issues   
        if(accountstatuses.resources[j].dataQualityIssues) { // 3. Check if issues exist
          for(k=0;k < accountstatuses.resources[j].dataQualityIssues.length; k++) { // 3.a) START issues Iterator: Iterate through all issues
            var singleIssueInfo = [];
            singleIssueInfo.push(accountList[accountstatuses.resources[j].accountId], accountstatuses.resources[j].dataQualityIssues[k].severity,
                                accountstatuses.resources[j].dataQualityIssues[k].numItems,
                                accountstatuses.resources[j].dataQualityIssues[k].id,
                                accountstatuses.resources[j].dataQualityIssues[k].lastChecked);
            var issueExampleMax = accountstatuses.resources[j].dataQualityIssues[k].exampleItems.length > 3 ? 3 : accountstatuses.resources[j].dataQualityIssues[k].exampleItems.length;
            var exampleConcatString ='';
            
            for(m=0; m < issueExampleMax; m++) { // 3.a.1 START Concatenate issue examples
              var singleExampleValue = accountstatuses.resources[j].dataQualityIssues[k].exampleItems[m].submittedValue ? accountstatuses.resources[j].dataQualityIssues[k].exampleItems[m].submittedValue : 'noValue';
              var singleExampleSuffix = m < issueExampleMax - 1 ? '\n' : '';
              var singleExampleString = accountstatuses.resources[j].dataQualityIssues[k].exampleItems[m].itemId + ': ' + singleExampleValue.substring(0,10).replace(',','\,') + singleExampleSuffix;
              exampleConcatString += singleExampleString.replace('online:de:DE:','').replace('online:de:AT:','');
            } // 3.a.1 END Concatenate issue examples
            singleIssueInfo.push(exampleConcatString);
            feedDataQualityArray.push(singleIssueInfo);
          } // 3.a) END issue Iterator
        } 
        else { // 3.b)) No issue Default Value
          var singleIssueInfo = [];
          singleIssueInfo.push(accountstatuses.resources[j].accountId,'none', 'none', 'no id', 'no examples');
          feedWarningArray.push(singleIssueInfo);
        } // 3. END issue Iter
      } // 1. END issue Iter 
    
    return feedDataQualityArray;
  }
  
  
  function acctArraytToObjArray(accountArray) {
    var accountObjectList = {};
    for (var i = 0; i < accountArray.length; ++i)
      accountObjectList[accountArray[i][1]] = accountArray[i][0];
    return accountObjectList;
  }

}

Nach dem ersten Testlauf im kopierten Demosheet sollten die eigenen Merchant Center-Daten einlaufen, soweit pro Tab vorhanden. Dabei arbeitet das Skript ohne Historisierung, es wird stets der aktuelle Zustand ohne Kenntnis vorheriger dargestellt. Bei vollständigem und korrektem Durchlauf zeigt sich folgendes Verlaufsprotokoll:


Es werden zunächst alle vorgefundenen Merchantkonten mit Name und ID ausgegeben, anschließend folgt eine Erfolgsmeldung  pro Bereich bzw. Tab.


Abschließend sollte das Scheduling auf „täglich” gestellt werden, um stets aktuelle Daten zu erhalten. Dabei sollte der Zeitpunkt NACH dem täglichen Merchant-Import liegen, falls nur einmal pro Tag. Im Feeds-Tab ist das Datum der letzten Aktualisierung erkennbar.

2. Zusammenführung separater MCCs und Einzelkonten in eine Übersicht

Das obige MCC-Skript deckt einen spezifischen Problemfall NICHT ab, welcher bei AdWords elegant gelöst und im Merchant Center NICHT möglich ist: Das Umziehen  von Merchant-Konten unter andere MCCs oder Händler-IDs. Mit dem „Five-Accounts-Per-Email“ AdWords-Update von Herbst 2016 kann man zwar mehrere Händler IDs in ein Merchant Center integrieren, es benötigt aber ein Skript pro Händler ID.

Im Screenshot-Fall oben ist „Demo Shop“ eine andere Händler-ID als das norisk-Überkonto und benötigt daher ein separates Skript. Während dieser Fall im Endkundenkontext nur bei Firmenübernahmen eintritt, tritt er im Agenturalltag bei Übernahme bestehender Konten regelmäßig auf: Das neue Einzelkonto kann nicht unter das Agentur-Hauptkonto gehängt werden, es muss als “Inselkonto” weiter parallel existieren – oder man wagt den Neustart, was in der Praxis aber nie getan wird.

Wir wollen im Folgenden eine Lösung für die Verwaltung multipler, übernommener Inselkonten zeigen, für die wir aus dem obigen MCC-Skript eine Variation erstellt haben. Diese unterscheidet sich in zwei Punkten vom MCC-Skript:

1. Die Variation verzichtet auf die MCC-Ebene und erwartet nur eine Merchant Center ID in der Konfiguration. Der deskriptive accountName wird im Feeds Tab aufgeführt.

2. Die Variation fügt die Daten in die bestehende Tabelle UNTERHALB in die nächste leere Zeile hinzu – und sortiert anschließend erneut absteigend nach Items. Im Demosheet könnte sich z.B. Agrarshop aus einem separaten Merchantcenter Konto später in die Liste „eingliedern“.

Die „Eingliederungs“-Methodik bleibt bei Abarbeitung der Bereiche Feeds, Errors etc. unverändert. Damit arbeitet SingleAccount Variation ergänzend, während die MCC-Version sich stets als Hauptschreiber oder Überschreiber sieht.

Für die Verortung des Skripts gibt es zwei Möglichkeiten:

  • Mittels dem „5-Accounts-per-email“ Prinzip habt ihr mit Eurem übergreifenden User Zugriff auf das separate GMC-Konto. In diesem Fall läuft das Skript im einzelnen AdWords Konto und nutzt die separate GMC-Authentizifierung.
  • Sowohl AdWords und GMC-Zugriff laufen über einen separaten Inseluser, da in der Zeit vor Herbst 2016 eingerichtet oder weil die 5 Konten pro User aufgebraucht sind. Wenn Euer übergreifender User zum Beispiel „allaccounts@“ und Euer separater Merchant-User [email protected] lautet, sollte dieser ebenfalls AdWordszugriff erhalten und dort das Skript hinterlegt werden.

 

Der User [email protected] wird im zweiten Fall NICHT die Berechtigung zur Skriptausführung haben, weil für die Daten aus der Shopping API keine Merchant-Center-Verknüpfung vorliegt. Dies zeigt sich beim gelben Balken im SingleAccount Skript über den allaccounts@ User.

Skriptcode für SingleAccount zum Download

Hier ist der CODE zum Copy&Pasten in Euer GMC Inselkonto:

https://github.com/norisk/AdWords-Scripts/blob/master/2017_GMC_SingleAcct-Addon_MerchantCenterMonitoring_v1.1.1

/**
*   Merchant Center Dashboard
*   Version: 1.1.0
*   @author: Christopher Gutknecht
*   norisk GmbH
*   [email protected]
* 
*   Demo-Spreadsheet Here: https://docs.google.com/spreadsheets/d/1SD-1xOeRxk5BRkGc-6WdFy2JQOMrC6ZqZguVoHoH-kQ/edit?usp=sharing
*   >> Summary: This script appends Merchant data of single, STANDALONE Merchant Center account to the main spreadsheet. Can also be used in isolation
*
* THIS SOFTWARE IS PROVIDED BY norisk GMBH ''AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL norisk GMBH BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
// Reference https://developers.google.com/shopping-content/v2/reference/v2/

function main() {
  var singleAccountId = '1000000000';
  var accountName = 'OnlineStore';
  var accountArray = [[accountName,singleAccountId]];
  var SPREADSHEET_ID = 'SHEET-ID'; // insert the ID of your copied sheet
  var SHEETNAMES = ['feeds', 'errors', 'warnings', 'dataQualityIssues'];
  
  var feedArray = [];
  feedArray = getFeedArray(accountArray);
  
  // 1. Write to Feed Sheet
  var ss = SpreadsheetApp.openById(SPREADSHEET_ID);
  printNewDataToSheet(SHEETNAMES[0], feedArray, 4);
  
  // 2.Write to Error Sheet
  var feedErrorArray = getFeedErrorArray(accountArray);
  printNewDataToSheet(SHEETNAMES[1], feedErrorArray, 5);
  
  // 3.Write to Warning Sheet
  var feedWarningArray = getFeedWarningArray(accountArray);
  printNewDataToSheet(SHEETNAMES[2], feedWarningArray, 5);
  
  // 4.Write to Data Quality Issue Sheet
  var feedDataQualityArray = getFeedDataQualityArray(accountArray);
  printNewDataToSheet(SHEETNAMES[3], feedDataQualityArray, 3);
  
  //////////
  //// Helper Functions
  //////////

  function printNewDataToSheet(SHEETNAME,dataArray,sortColumn) {
    var sheet = ss.getSheetByName(SHEETNAME);
    var lastReportRow = getLastReportRow(SHEETNAME);
    
    var dataRange = sheet.getRange(lastReportRow, 1, dataArray.length, dataArray[0].length);
    dataRange.setValues(dataArray);
    var sortRange = sheet.getRange("A:I");
    sortRange.sort({column: sortColumn, ascending: false});
    
    Logger.log(SHEETNAME + ' list successfully printed to ' + SHEETNAME + ' sheet.');  
  } 
  
  
  function getFeedArray(accountArray){
    for(i=0;i<accountArray.length;i++) {
      var datafeeds = ShoppingContent.Datafeeds.list(accountArray[i][1]);
      for(j=0;j<datafeeds.resources.length;j++) {
        var singlefeedInfo = [accountArray[i][0], datafeeds.resources[j].targetCountry,datafeeds.resources[j].fileName];
        var dataFeedsStatuses = ShoppingContent.Datafeedstatuses.get(accountArray[i][1], datafeeds.resources[j].id);
        singlefeedInfo.push(dataFeedsStatuses.itemsTotal,dataFeedsStatuses.itemsValid, dataFeedsStatuses.itemsTotal - dataFeedsStatuses.itemsValid, dataFeedsStatuses.processingStatus, dataFeedsStatuses.lastUploadDate.slice(0,10),
          datafeeds.resources[j].fetchSchedule.hour + " Uhr");
        feedArray.push(singlefeedInfo);
      }
    }
    return feedArray;
  } 
  
  /**
  * @param {Array} a twodimensional list of account names and IDs.
  * @return {Array} A twodimensional array of feed errors and related information.
  */
  function getFeedErrorArray(accountArray) {
    var feedErrorArray = [];
    
    for(i=0; i < accountArray.length;i++) { // 1. START Iterate through all GMC accounts > return resources array
      var dataFeedsStatuses = ShoppingContent.Datafeedstatuses.list(accountArray[i][1]);
      
      for(j=0;j < dataFeedsStatuses.resources.length;j++) { // 2. START Resource Iterator: Iterate through all resources aka feeds   
        if(dataFeedsStatuses.resources[j].errors) { // 3. Check if errors exist
          for(k=0;k < dataFeedsStatuses.resources[j].errors.length; k++) { // 3.a) START Error Iterator: Iterate through all errors
            var singleFeedInfo = [];
            singleFeedInfo.push(accountArray[i][0],dataFeedsStatuses.resources[j].datafeedId,
                                dataFeedsStatuses.resources[j].errors[k].message.substring(0,100).replace("Insufficient product identifiers", "Insuff Product Ident").replace(',','\,'),
                                dataFeedsStatuses.resources[j].errors[k].code.replace(',','\,'),
                                dataFeedsStatuses.resources[j].errors[k].count);
            var errorExampleMax = dataFeedsStatuses.resources[j].errors[k].examples.length > 3 ? 3 : dataFeedsStatuses.resources[j].errors[k].examples.length;
            var exampleConcatString ='';
            
            for(m=0; m < errorExampleMax; m++) { // 3.a.1 START Concatenate error examples
              var singleExampleValue = dataFeedsStatuses.resources[j].errors[k].examples[m].value ? dataFeedsStatuses.resources[j].errors[k].examples[m].value : 'noValue';
              var singleExampleSuffix = m < errorExampleMax - 1 ? '\n' : '';
              var singleExampleString = dataFeedsStatuses.resources[j].errors[k].examples[m].itemId + ': ' + singleExampleValue.substring(0,10).replace(',','\,') + singleExampleSuffix;
              exampleConcatString += singleExampleString;
            } // 3.a.1 END Concatenate error examples
            singleFeedInfo.push(exampleConcatString);
            feedErrorArray.push(singleFeedInfo);
          } // 3.a) END Error Iterator
        } 
        else { // 3.b)) No Error Default Value
          var singleFeedInfo = [];
          singleFeedInfo.push(accountArray[i][0],dataFeedsStatuses.resources[j].datafeedId, 'no errors', '-', 0, 'no examples');
          feedErrorArray.push(singleFeedInfo);
        } // 3. END Error Iter
      } // 2. END Resource Iter 
    } // 1. END Account Iter
    
    return feedErrorArray; 
  }
  
  /**
  * @param {Array} a twodimensional list of account names and IDs.
  * @return {Array} A twodimensional array of feed warnings and related information.
  */
  function getFeedWarningArray(accountArray) {
    var feedWarningArray = [];
    
    for(i=0; i < accountArray.length;i++) { // 1. START Iterate through all GMC accounts > return resources array
      var dataFeedsStatuses = ShoppingContent.Datafeedstatuses.list(accountArray[i][1]);
      Logger.log("dataFeedsStatuses:" + dataFeedsStatuses.resources);
      for(j=0;j < dataFeedsStatuses.resources.length;j++) { // 2. START Resource Iterator: Iterate through all resources aka feeds   
        if(dataFeedsStatuses.resources[j].warnings) { // 3. Check if warnings exist
          for(k=0;k < dataFeedsStatuses.resources[j].warnings.length; k++) { // 3.a) START warning Iterator: Iterate through all warnings
            var singleFeedInfo = [];
            singleFeedInfo.push(accountArray[i][0],dataFeedsStatuses.resources[j].datafeedId,
                                dataFeedsStatuses.resources[j].warnings[k].message.substring(0,100).replace(',','\,'),
                                dataFeedsStatuses.resources[j].warnings[k].code.replace(',','\,'),
                                dataFeedsStatuses.resources[j].warnings[k].count);
            var warningExampleMax = dataFeedsStatuses.resources[j].warnings[k].examples.length > 3 ? 3 : dataFeedsStatuses.resources[j].warnings[k].examples.length;
            var exampleConcatString ='';
            
            for(m=0; m < warningExampleMax; m++) { // 3.a.1 START Concatenate warning examples
              var singleExampleValue = dataFeedsStatuses.resources[j].warnings[k].examples[m].value ? dataFeedsStatuses.resources[j].warnings[k].examples[m].value : 'noValue';
              var singleExampleSuffix = m < warningExampleMax - 1 ? '\n' : '';
              var singleExampleString = dataFeedsStatuses.resources[j].warnings[k].examples[m].itemId + ': ' + singleExampleValue.substring(0,10).replace(',','\,') + singleExampleSuffix;
              exampleConcatString += singleExampleString;
            } // 3.a.1 END Concatenate warning examples
            singleFeedInfo.push(exampleConcatString);
            feedWarningArray.push(singleFeedInfo);
          } // 3.a) END warning Iterator
        } 
        else { // 3.b)) No warning Default Value
          var singleFeedInfo = [];
          singleFeedInfo.push(accountArray[i][0],dataFeedsStatuses.resources[j].datafeedId, 'no warnings', '-', 0, 'no examples');
          feedWarningArray.push(singleFeedInfo);
        } // 3. END warning Iter
      } // 2. END Resource Iter 
    } // 1. END Account Iter
    return feedWarningArray; 
  }
  
  
  /**
  * @param {string} the account ID of the multi-client merchant center
  * @return {Array} A twodimensional array of feed data quality issues and related information.
  */
  function getFeedDataQualityArray(accountArray) {
    
  var feedDataQualityArray = [];
  var accountstatuses = ShoppingContent.Accountstatuses.get(accountArray[0][1], accountArray[0][1]);

  if(accountstatuses.dataQualityIssues) { // 3. Check if issues exist
    for(k=0;k < accountstatuses.dataQualityIssues.length; k++) { // 3.a) START issues Iterator: Iterate through all issues
      var singleIssueInfo = [];
      singleIssueInfo.push(accountArray[0][0], accountstatuses.dataQualityIssues[k].severity,
                          accountstatuses.dataQualityIssues[k].numItems,
                          accountstatuses.dataQualityIssues[k].id,
                          accountstatuses.dataQualityIssues[k].lastChecked);
      var issueExampleMax = accountstatuses.dataQualityIssues[k].exampleItems.length > 3 ? 3 : accountstatuses.dataQualityIssues[k].exampleItems.length;
      var exampleConcatString ='';
      
      for(m=0; m < issueExampleMax; m++) { // 3.a.1 START Concatenate issue examples
        var singleExampleValue = accountstatuses.dataQualityIssues[k].exampleItems[m].submittedValue ? accountstatuses.dataQualityIssues[k].exampleItems[m].submittedValue : 'noValue';
        var singleExampleSuffix = m < issueExampleMax - 1 ? '\n' : '';
        var singleExampleString = accountstatuses.dataQualityIssues[k].exampleItems[m].itemId + ': ' + singleExampleValue.substring(0,10).replace(',','\,') + singleExampleSuffix;
        exampleConcatString += singleExampleString.replace('online:de:DE:','').replace('online:de:AT:','');
      } // 3.a.1 END Concatenate issue examples
      singleIssueInfo.push(exampleConcatString);
      feedDataQualityArray.push(singleIssueInfo);
    } // 3.a) END issue Iterator
  } 
  else { // 3.b)) No issue Default Value
    var singleIssueInfo = [];
    singleIssueInfo.push(accountArray[0][0],'none', 'none', 'no id', 'no examples');
    feedWarningArray.push(singleIssueInfo);
  } // 3. END issue Iter
  
  return feedDataQualityArray;
}
  
  function getLastReportRow(SHEETNAME) {
    var lastRowSheet = ss.getSheetByName(SHEETNAME);
    var column = lastRowSheet.getRange('A:A');
    var values = column.getValues(); // get all data in one call
    var ct = 0;
    while ( values[ct] && values[ct][0] != "" ) {
      ct++;
    }
    return (ct+1);
  }
  
}

 

Nach erfolgreichem Testdurchlauf sollte das Skript eine oder mehrere zusätzliche Zeilen in das Spreadsheet schreiben. Hinweis: Bei mehrfacher Ausführung des SingleAccount-Skripts hintereinander werden die Werte doppelt geschrieben, was aber bei täglichem Scheduling kein Problem darstellt. Beim Scheduling sollte ZWINGEND (!) beachtet werden, dass das SingleAccount-Skript zeitlich HINTER das MCC-Skript getaktet wird, sonst werden die SingleAccount-Werte überschrieben.

3. BONUS: Tägliche Email der Feeds-Übersichtstabelle

In unserer Agenturarbeit erzeugen wir zusätzlich zum Spreadsheet eine tägliche Emailübersicht in Form einer einfachen HTML-Tabelle, um proaktiv des tagesaktuellen Stand importierter und abgelehnter Produkte zu erhalten.

Die in der Email enthaltene Tabelle entspricht dem 1.Feeds-Tab und wird ohne Styling ausgegeben, um Unterschiede in Mailclients zu verhindern. In der Abbildung unten sieht man eine beispielhafte Struktur.

Zwischenzeitlich wurde das Spreadsheet in eine HTML-Datei umgewandelt und die vollständige Tabellenformatierung inklusive Farben über das CSS-Inlining Feature der Mailchimp API in eine „hübsche“ Emailtabelle umgewandelt, welche jedoch von einigen Mailclients wie Outlook nicht unterstützt wird – daher blieb es ein nettes Experiment.

Hier ist der CODE zum Copy&Pasten in AdWords oder Apps Scripts, wenn Ihr eine Email erhalten wollt:

https://github.com/norisk/AdWords-Scripts/blob/master/2017_GMC_Addon_FeedTableSender

/** 
* Helper Function: HTML Table Sender
* @author: Christopher Gutknecht 
* norisk GmbH 
* [email protected]
*/

function main() {
  
  // CONFIG START
  var SHEET_URL = "$$ Spreadsheet-URL $$";  // Example: https://docs.google.com/spreadsheets/d/1SD-1xOeRxk5BRkGc-6WdFy2JQOMrC6ZqZguVoHoH-kQ/edit#gid=0
  var SHEET_ID = "$$ Spreadsheet-ID $$"; //  // Example: 1SD-1xOeRxk5BRkGc-6WdFy2JQOMrC6ZqZguVoHoH-kQ
  var SHEET_NAME = "feeds";
  var RECIPIENT = ["[email protected]"];
  var SCRIPT_NAME = 'GMC-Overview_Email';
  // CONFIG END
  
  var ss = SpreadsheetApp.openById(SHEET_ID);
  var spreadsheet = ss.getSheetByName(SHEET_NAME);
   
  var range = spreadsheet.getRange(2, 1, getLastReportRow(spreadsheet), 9); 
  var feedData = range.getValues();
  
  sendArrayAsEmail(SCRIPT_NAME,feedData,RECIPIENT,SHEET_URL);
 
  // FUNCTION DEFINITIONS
  function sendArrayAsEmail(SCRIPT_NAME, printArray, RECIPIENT, SHEET_URL) {
   
   var currentdate = new Date(); 
   var datetime = currentdate.getDate() + "."
                + (currentdate.getMonth()+1)  + "." 
                + currentdate.getFullYear() + " , "  
                + currentdate.getHours() + ":"  
                + currentdate.getMinutes();
     
    var subject = SCRIPT_NAME + ': Summary Results for ' + datetime;
    var body = subject;
    var htmlBody = '<html><body><br><br> >>> Please check the <b>ITEM ERRORS (!)</b> per row.<br><br>';
    htmlBody += '<table border="1" width="95%" style="border-collapse:collapse;">';
    
    htmlBody += "<tr>";
    for(var i=0; i<printArray[0].length; i++){
      htmlBody += '<td align="center">'+printArray[0][i]+'</td>';
    }
    htmlBody += "<tr>";
    
    for(var i=1; i<printArray.length; i++) {
          htmlBody += "<tr>";
          for(var j=0; j<printArray[i].length; j++){
              htmlBody += "<td>"+printArray[i][j]+"</td>";
          }
          htmlBody += "</tr>";
    }
    
    htmlBody += '<br/ ><br/ >';
    htmlBody += "</table></html></body>";
    var options = { htmlBody : htmlBody };
    for(var i in RECIPIENT) {
      MailApp.sendEmail(RECIPIENT[i], subject, body, options);
      Logger.log('Email sent to ' + RECIPIENT[i]);
    }
    Logger.log("Done in full!");
  }
  
  /*
  * @param spreadsheet {object}
  * @return {integer}
  */
  function getLastReportRow(spreadsheet) {
    var column = spreadsheet.getRange('A:A');
    var values = column.getValues(); // get all data in one call
    var ct = 0;
    while ( values[ct] && values[ct][0] != "" ) {
      ct++;
    }
    return (ct+1);
  } 
}

 

Wir haben das Skript bewusst separat aufgesetzt, da wir im SEA-Alltag unser Merchant-Center-MCC mit mehreren Inselaccounts in eine Spreadsheetübersicht zusammenführen. Prinzipiell ist es ohne Inselaccounts direkt in das erste Skript integrierbar. Im Ergebnis läuft Euer Shoppingfeed-Monitoring nun im Passivmode und wird Euch täglich in Eure Email-Inbox serviert.

 

 

FAZIT: Multikonto-Merchant Monitoring leicht gemacht

Da Google keine gleichwertigen Bordmittel zur Übersicht bereitstellt, haben wir mit etwas Skriptarbeit diese Herausforderung für Euch gelöst. Für uns ist die Übersicht mit der Email ein unverzichtbares Tool zur übergreifenden Beobachtung geworden, da größere Ablehnungen meist völlig unerwartet und (gefühlt bevorzugt) an Tagen der Urlaubsvertretung kommen, wenn man nicht überall tief in alle Accounts schauen kann.

Jetzt seid Ihr gefragt: Habt Ihr das obige Skript getestet und habt Fragen? Habt ihr Ideen für einen weiteren Ausbau der bisherigen Features? Kennt Ihr andere, elegantere Lösungen? Lasst es uns in den Kommentaren wissen!

Zum Abschluss ein großes Danke an Thomas und Marcel für die bisherigen Erwähnungen dieses Skripts: