Google Ads - Negative Keywords Script - Account Version

Was dieses Skript macht:

Wenn "Auto Exclude" in den "Skript-Einstellungen" auf "YES" gesetzt ist:

Werden für AdGroups mit dem Label "Automate Negatives" alle Suchbegriffe der letzten 7 Tage mit den Keywords in der Adgroup abgeglichen. Wenn der Suchbegriff nicht exakt mit einem Keyword in der Adgroup übereinstimmt, wird er als negatives Keyword hinzugefügt.

In diesem Modus wird das Skript nur reporten. Keine Keywords werden automatisch ausgeschlossen.

Den Reporting Zeitraum kannst Du in den Einstellungen festlegen. Das Skript schließt nur enge Varianten von exact matches aus (Du kannst also, Phrase- und Broad Matches weiterhin in derselben Ad-Group verwenden).

Wenn "Auto Exclude" auf "NO" gesetzt ist: Das Skript nimmt keine Änderungen am Konto vor, sondern gibt nur Berichte darüber aus, was "getan worden wäre".

Implementierung des Skripts

1. Labele die Adgroups, die du in deinem Konto anpassen möchtest, mit dem Label "Automate negatives", achte auf Groß- und Kleinschreibung!

2. Erstelle eine Kopie dieses Google Sheets und füge die URL Deines Sheets in Zeile 2 des Skripts ein:
Kopiere dieses Sheet.

3. Klicke auf "Ausführen" und autorisiere das Skript

4. In den ersten beiden Grafiken unter "Übersichtsdiagramme" kannst du die Kosten und Konversionen nach Treffertyp sehen. In den beiden darauf folgenden Grafiken kannst du sehen, welche Suchbegriffe ausgeschlossen und welche nicht ausgeschlossen sind (alle nicht exakten Treffer werden ausgeschlossen)

Überblick über Kosten und Conversions der Search terms. Unten sieht man, wieviele Kosten und Conversions vom Skript ausgeschlossen werden (oder werden würden falls auto exclude auf "No" steht).

5. In "Performance Deepdive" kannst du auch die Leistung der Ad-Groups auf CPL- und ROAS-Ebene überprüfen.

6. Wenn die Daten und Ausschlüsse für dich sinnvoll sind, kannst du die Einstellung in den "Skript-Einstellungen" ändern, um die Search Terms künftig automatisch auszuschließen

Datenanalyseinformationen: Es kann vorkommen, dass der Prozentsatz der exakten Übereinstimmung (enge Variante) höher ist als die tatsächlich ausgeschlossenen Kosten, wenn das Skript ausgeführt wird. Dies geschieht, wenn ein Exact Match kürzlich hinzugefügt wurde. Im Abfragebericht wird er immer noch als enge Variante für vergangene Daten angezeigt. Tatsächlich handelt es sich jedoch bereits um einen exact Match.

Code für das Skript (Copy & Paste in Google Ads)

var dataToWrite = [];

function main() {
  
  // Provide the Google Sheets URL here
  var spreadsheetURL = "YOUR_SHEET_URL_HERE";
  
  
  //no changes below here please
  if (spreadsheetURL.indexOf('docs.google.com') == -1) {
    Logger.log("Please make sure to provide a valid SpreadsheetURL")
  }

  var spreadsheet = SpreadsheetApp.openByUrl(spreadsheetURL);
  var sheet = spreadsheet.getSheetByName("Search Term Data"); // Negatives are in the first sheet
  var settingsSheet = spreadsheet.getSheetByName("Script Settings");

  clearSheetExceptHeadline(spreadsheetURL, "Search Term Data")
  
  //Script settings
  var automateNegatives = settingsSheet.getRange("D8").getValue();
  var timeframe = settingsSheet.getRange("D4").getValue()
  var reportingTimeframe = getDateRangeForTimeframe(timeframe);
  
  // Iterate through all ad groups in the account
  var adGroupsIterator = AdsApp.adGroups().get();

  while (adGroupsIterator.hasNext()) {
    var adGroup = adGroupsIterator.next();
    var adGroupId = adGroup.getId();
    var adGroupName = adGroup.getName();
    
    // Check if the ad group has a label named "Automate negatives"
    if (hasLabel(adGroup, "Automate negatives")) {
      // Fetch all keywords in the ad group
      var keywordsIterator = adGroup.keywords().get();
      var adGroupKeywords = [];

      while (keywordsIterator.hasNext()) {
        var keyword = keywordsIterator.next();
        adGroupKeywords.push(keyword.getText());
      }

      // Get the search terms for the current ad group ordered by clicks in the last 30 days
      var searchTermsQuery = "SELECT Query, Cost, Clicks, Conversions, ConversionValue, QueryMatchTypeWithVariant FROM SEARCH_QUERY_PERFORMANCE_REPORT " +
        "WHERE AdGroupId = " + adGroupId +
        " AND Clicks > 0 " +
        "DURING " + reportingTimeframe + " " +
        "ORDER BY Clicks DESC ";

      var searchTermsIterator = AdsApp.report(searchTermsQuery).rows();

      while (searchTermsIterator.hasNext()) {
        var searchTerm = searchTermsIterator.next();
        var searchTermText = searchTerm["Query"].trim();
        var searchTermCost = searchTerm["Cost"]
        var searchTermClicks = searchTerm["Clicks"]
        var searchTermConversions = searchTerm["Conversions"]
        var searchTermConversionValue = searchTerm["ConversionValue"]
        var matchTypeVariant = searchTerm["QueryMatchTypeWithVariant"]
        var currentDate = new Date();
        
        // Check if the search term is not in the ad group's keywords
        if (adGroupKeywords.indexOf(searchTermText) === -1 && matchTypeVariant == "exact (close variant)") {
          if(automateNegatives == "Yes"){
            adGroup.createNegativeKeyword("[" + searchTermText + "]");
            Logger.log("Excluded search term in ad group '" + adGroupName + "': " + searchTermText);
          }
          
          // Log to Google Sheets
          var rowData = [currentDate, adGroupName, "[" + searchTermText + "]", searchTermCost, searchTermClicks, searchTermConversions, searchTermConversionValue, matchTypeVariant, "Search Term Excluded"];
          dataToWrite.push(rowData);
          
          
        }
        else {var rowData = [currentDate, adGroupName, "[" + searchTermText + "]", searchTermCost, searchTermClicks, searchTermConversions, searchTermConversionValue, matchTypeVariant, "Search Term Not Excluded"];
          dataToWrite.push(rowData);}
      }
    }
  }
  if (dataToWrite.length > 0) {
    var sheet = SpreadsheetApp.openByUrl(spreadsheetURL).getSheetByName("Search Term Data");
    sheet.getRange(sheet.getLastRow() + 1, 1, dataToWrite.length, dataToWrite[0].length).setValues(dataToWrite);
  }
  else {Logger.log("No data to log. Please make sure at least one adgroup contains the label 'Automate negatives' ")};
}

// Helper function to check if an ad group has a specific label
function hasLabel(adGroup, labelName) {
  // Convert the labelName parameter to lowercase for case-insensitive comparison
  var normalizedLabelName = labelName.toLowerCase();

  // Get all labels for the adGroup
  var labels = adGroup.labels().get();

  // Iterate through all labels
  while (labels.hasNext()) {
    var label = labels.next();
    // Compare the label name in lowercase to the normalized label name
    if (label.getName().toLowerCase() === normalizedLabelName) {
      return true; // Return true if a match is found
    }
  }
  return false; // Return false if no matching label is found
}


function clearSheetExceptHeadline(spreadsheetURL, sheetName) {
  // Open the spreadsheet by URL
  var spreadsheet = SpreadsheetApp.openByUrl(spreadsheetURL);
  
  // Access the specified sheet by name
  var sheet = spreadsheet.getSheetByName(sheetName);
  
  // Check if the sheet exists
  if (!sheet) {
    Logger.log("Sheet not found: " + sheetName);
    return; // Exit the function if the sheet does not exist
  }
  
  //remove filters first
  var filter = sheet.getFilter();
  if (filter) {
    filter.remove();}
    
  // Get the number of rows and columns to clear
  var lastRow = sheet.getLastRow();
  var lastColumn = sheet.getLastColumn();
  
  // Ensure there are rows to clear beyond the headline
  if (lastRow > 1) {
    // Clear all content starting from the second row
    sheet.getRange(2, 1, lastRow - 1, lastColumn).clearContent();

    // After clearing the content, delete the empty rows to avoid having a large gap.
    // This action is not immediate, hence checking if the sheet still has more than one row before attempting to delete.
    var rowsAfterClear = sheet.getLastRow();
    if (rowsAfterClear > 1) {
      // Delete rows from the second row to the end of the sheet, adjusting for the deletion offset.
      sheet.deleteRows(2, rowsAfterClear - 1);
    }
  }
}

  function getDateRangeForTimeframe(timeframe) {
  var today = new Date();
  var endDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1); // Set to yesterday
  var startDate = new Date(); // Initialized but will be overwritten

  switch (timeframe) {
    case 'LAST_7_DAYS':
      startDate.setDate(endDate.getDate() - 7);
      break;
    case 'LAST_14_DAYS':
      startDate.setDate(endDate.getDate() - 14);
      break;
    case 'LAST_30_DAYS':
      startDate.setDate(endDate.getDate() - 30);
      break;
    case 'LAST_90_DAYS':
      startDate.setDate(endDate.getDate() - 90);
      break;
    case 'LAST_180_DAYS':
      startDate.setDate(endDate.getDate() - 180);
      break;
    default:
      throw new Error('Invalid timeframe specified');
  }

  // Format dates as YYYYMMDD
  function formatAsYYYYMMDD(date) {
    var dd = date.getDate();
    var mm = date.getMonth() + 1; // January is 0!
    var yyyy = date.getFullYear();
    if (dd < 10) dd = '0' + dd;
    if (mm < 10) mm = '0' + mm;
    return '' + yyyy + mm + dd;
  }

  var startDateStr = formatAsYYYYMMDD(startDate);
  var endDateStr = formatAsYYYYMMDD(endDate);
  
  // Return formatted date range
  return startDateStr + ',' + endDateStr;
}