Wednesday, October 30, 2013

Disable Ads and Keywords For Out of Stock Items

UPDATE 2016-01-26: In response to some common issues, this script has been updated to include some enhancements. Please see the change log notes for v1.2.

As a follow up to my questions about cool scripts for Black Friday / Cyber Monday, today I put together a script to run through your urls and check if the item is out of stock on the website. If it is, we will pause the AdGroup.

This script has some of the same elements of my script on checking for broken links in your account, but it actually pulls the html source of each page and searches for a configurable string that lets it know when it is out of stock.

Let's walk through an example. I love some of the quirky gifts I find on ModCloth.com. But like any online store, some items go out of stock. Here is one I found while testing this script.
In order to get this script to work, I need to find out what is different about the page when it goes out of stock. If I right click and view the page source, and search for the work "stock", I can see a few different places where it is used. One of them is the following that says "in_stock":false.
That looks promising. I check on an in stock item and sure enough, "in_stock":true is on that page.

Alright, so now I know what text I need to use to fill in the OUT_OF_STOCK_TEXT variable in my code. Now each site is going to be a little different, so I have a simple script that uses the same url logic as the complete script that you can use for testing.

Once you find some HTML text in the source of the landing page that identifies if an item is out of stock, you should be good to go on the full script. There are a few other options in the script that allow you to enable or disable various url manipulations in the script. And remember, this will pause only the Ads or Keywords that link to the page with the out of stock item.

Thanks,
Russ

Monday, October 21, 2013

Use AdWords Scripts To Automate Black Friday

Black Friday is quickly approaching for retailers in the United States. I have some ideas for ways that you can automate your campaigns using AdWords Scripts, but I'd love to get your thoughts on some of them.  Here's what I can think of.  Please comment with the ones that you'd like to see most.

  • Countdown Script - Similar to this script from Google but read the information from a Google doc and add support for timezones.
  • Add Pricing, Inventory, and percent discount to Creatives - Use a Google Spreadsheet to update prices and inventory for your items. That info will be read by the script get updated in the Ads hourly.
  • Automatically Pause Ads as Items Go Out of Stock - This script will monitor your landing pages and pause the ads once it sees that the item is no longer in stock.
  • Update Mobile Modifiers and Location Modifiers for Retail Locations Throughout the Day - This script would start with a boost to specific location and mobile modifiers for your campaigns and then slowly ramp them back down throughout the day. Set the end time and it will automatically calculate the ramp down.
Those are just a few ideas I had.  Do you have a better idea? Would you like to see one of the scripts above?  Let me know by leaving a comment.  I'll implement the winner by the end of October.


Thanks,
Russ

Sunday, October 20, 2013

Use GDrive To Load A Single AdWords Script Into Multiple Accounts

One of the downsides to using AdWords Scripts is that you need to log into each account and set up the script. For most people, that isn't a problem the first few times. But as you start seeing the value of some of these scripts, there might be a set of them that you want to put in all your accounts. Setting them up is fine, until you find a bug in your code and have to go in and update all 20 copies of the script in each account.

Well in this post, I put together a simple way to keep a single copy of your script and load it into any number of AdWords accounts. Then, if you ever have a change to make, you can update a single version of the script and all the accounts will instantly start using the new code.

The code for this consists of two parts. The first snippet of code is the generic code that you need to place in each one of your accounts. This code references a single Google Spreadsheet (here is a sample one for you to copy: http://goo.gl/y6hPfy) that it uses to know what scripts it is supposed to run. The spreadsheet has only 3 columns: A description which is just used for logging, the location of the script in Google Drive, and the Object Name in the script. Don't worry about that right now, I will describe it better in the next section. Finally, it loads the script file and executes the main function.

/************************************
 * Generic Script Runner
 * Version 1.0
 * Created By: Russ Savage
 * FreeAdWordsScripts.com
 ***********************************/
function main() {
  //See http://goo.gl/KvINmD for an example spreadsheet.
  var scriptConfigId = 'Your Spreadsheet Id Goes Here';
  var sheet = SpreadsheetApp.openById(scriptConfigId).getActiveSheet();
  var data = sheet.getRange('A:C').getValues();
  for(var i in data) {
    if(i == 0) { continue; }
    var [description, location, classname] = data[i];
    if(!location) { continue; }
    Logger.log('Running "'+description+'" from location: '+location);
    var scriptFile = getFile(location);
    var scriptText = scriptFile.getBlob().getDataAsString();
    eval(scriptText);
    var script = eval('new '+classname+'();');
    script.main();
  }
}
 
//This function gets the file from GDrive
function getFile(loc) {
  var locArray = loc.split('/');
  var folder = getFolder(loc);
  if(folder.getFilesByName(locArray[locArray.length-1]).hasNext()) {
    return folder.getFilesByName(locArray[locArray.length-1]).next();
  } else {
    return null;
  }
}
 
//This function finds the folder for the file and creates folders if needed
function getFolder(folderPath) {
  var folder = DriveApp.getRootFolder();
  if(folderPath) {
    var pathArray = folderPath.split('/');
    for(var i in pathArray) {
      if(i == pathArray.length - 1) { break; }
      var folderName = pathArray[i];
      if(folder.getFoldersByName(folderName).hasNext()) {
        folder = folder.getFoldersByName(folderName).next();
      }
    }
  }
  return folder;
}

Now that we have a generic piece of code that reads the description, location, and object name from a spreadsheet and executes code, we need to make some slight modifications to some of our existing scripts to get it to work.

One of my favorite scripts is the one about Finding Anomalies in Your Account. In order for the script to be run from another script, we need to convert it into an object with a single public function. This same technique should work on almost all of the scripts from FreeAdWordsScripts.com.

First, surround the entire script in a function call and give it whatever name you like.
function Anomalies() {
 // Copy and Paste the code from:
 // http://goo.gl/IT1UcV
};
Next, you need to update the main function to be a public method so that we can call it from our generic script.
this.main = function() {
 // Don't make any changes to the body of the main method
}

A full version of the updated code can be found here: Find Anomalies in Your Account Object Version.

Now you can save this new script somewhere in your GDrive and update the Location and Object Name (Anomalies in this case) in your config spreadsheet.

Now you should be good to go. You can add as many scripts as you like to the config spreadsheet but keep in mind that the 30 minute limit still applies.

Thanks,
Russ

Manage Your AdWords Creatives Using Excel

Since my last post, Google has been busy releasing a ton of new features for AdWords scripts.  Among them was the ability to interact directly with your Google Drive files.  This is awesome for a few reasons and one of them is that it allows you to manage your account through spreadsheets that are automatically synced back to your Google AdWords account every hour.

There is the opportunity to create keywords, manage Ads, AdGroups, or Ad Extensions all using the power of Excel spreadsheets. Still not convinced? You can also have all your account metrics automatically delivered to your Google Drive and automatically downloaded to your computer each day.

So I thought I would demonstrate how you can manage your Creatives using this new functionality.

The first thing you need to do is install Google Drive onto your computer. It's pretty easy to do but does require Admin rights on your machine.  If you can't install it right now, no need to worry.  You can manage your SiteLinks by updating the files in Google Drive via your browser.

Now take a look at the script below. There are a few config options that you should set.  FOLDER_PATH is the path to the folder where you want to store these files.  For example, 'AdWords/Creatives'. If the folder path doesn't exist, this script will create it for you.

The next parameter is FILE_NAME, which would be the file that the Creative data will be stored in. In this example, the file must be written as a .CSV.  Google makes it pretty easy to parse CSV files using the Utilities.parseCsv function.

This script allows you to make changes to headlines, descriptions, urls, status, and device preferences all from the csv file using Excel.  Then, simply save the file back to Google drive and the next time the script runs (daily or hourly), your changes will be reflected.

What other AdWords features might be easier to manage through Excel rather than through some of the UIs out there?

Thanks,
Russ

/******************************************
 * Manage AdWords Ads Using Excel
 * Version 1.0
 * Author: Russ Savage
 * FreeAdWordsScripts.com
 ****************************************/
var FOLDER_PATH = 'AdWordsData'; //The path where the file will be stored on GDrive
var FILE_NAME = 'creatives.csv'; //The name of the file on GDrive
 
var INCLUDE_STATS = true; // Set to false to remove stats from the file
var DATE_RANGE = 'LAST_30_DAYS'; //The date range for the stats
 
var INCLUDE_PAUSED_ADS = true; //Set to false to only report on active ads
var DELETE_ORIGINAL_ADS = true; //When set to false, the original ads will be paused instead of deleted
 
function main() {
  var file = getFile(FILE_NAME,FOLDER_PATH);
  if(!file) {
    file = createNewFile(FILE_NAME,FOLDER_PATH,buildCreativesFile());
    return;
  }
  
  var fileContent = file.getBlob().getDataAsString();
  var creatives = {};
  if(fileContent) {
    creatives = parseFileContent(fileContent);
  }
  
  var creativeIter = getAdIterator();
  while(creativeIter.hasNext()) {
    var creative = creativeIter.next();
    var adId = creative.getId();
    if(creatives[adId]) {
      //ok we found the ad.
      //Checking status
      var isEnabledFile = (creatives[adId]['Status'] === 'Enabled');
      if(creative.isEnabled() != isEnabledFile) {
        if(isEnabledFile) { 
          creative.enable(); 
        } else { 
          creative.pause(); 
        }
      }
      
      if(hadAdChanged(creative,creatives[adId])) {
        if(DELETE_ORIGINAL_ADS) {
          creative.remove();
        } else {
          creative.pause();
        }
        createNewAd(creative.getAdGroup(),creatives[creative.getId()]);
      }
    }
  }
  file.setContent(buildCreativesFile());
}
 
//Helper function to create a new ad
function createNewAd(ag,newAd) {
  var optArgs = {
    isMobilePreferred: (newAd['Device'] === 'Mobile') ? true : false
  };
  ag.createTextAd(newAd['Headline'],newAd['Desc1'],newAd['Desc2'],newAd['DisplayUrl'],newAd['DestinationUrl'],optArgs);
}
 
//This checks to see if the ad has been changed
function hadAdChanged(ad,oldAd) {
  var newAdText = [ad.getHeadline(),
                   ad.getDescription1(),
                   ad.getDescription2(),
                   ad.getDisplayUrl(),
                   ad.getDestinationUrl(),
                   (ad.isMobilePreferred()) ? 'Mobile' : 'Desktop'].join('~~!~~');
  var oldAdText = [oldAd['Headline'],
                   oldAd['Desc1'],
                   oldAd['Desc2'],
                   oldAd['DisplayUrl'],
                   oldAd['DestinationUrl'],
                   oldAd['Device']].join('~~!~~');
  Logger.log(newAdText);
  Logger.log(oldAdText);
  return (newAdText !== oldAdText);
}
 
//This builds the creatives file from all the ads in the account.
function buildCreativesFile() {
  var report = getReportColumns();
  var creativeIter = getAdIterator();
  while(creativeIter.hasNext()) {
    var creative = creativeIter.next();
    report += getReportRow(creative);
  }
  return report;
}
 
//This returns the ad iterator based on options.
function getAdIterator() {
  var adSelector = AdWordsApp.ads().withCondition("Type = 'TEXT_AD'"); 
  if(!INCLUDE_PAUSED_ADS) {
    adSelector = adSelector.withCondition('CampaignStatus = ENABLED')
                           .withCondition('AdGroupStatus = ENABLED')
                           .withCondition('Status = ENABLED');
  }
  return adSelector.get();
}
 
//This returns a CSV fow for the report.
function getReportRow(ad) {
  var retVal = [
    ad.getId(),
    ad.getCampaign().getName(),(ad.getCampaign().isPaused()) ? 'Paused' : 'Enabled',
    ad.getAdGroup().getName(),(ad.getAdGroup().isPaused()) ? 'Paused' : 'Enabled',
    ad.getHeadline(),
    ad.getDescription1(),
    ad.getDescription2(),
    ad.getDisplayUrl(),
    ad.getDestinationUrl(),
    (ad.isPaused()) ? 'Paused' : 'Enabled',
    (ad.isMobilePreferred()) ? 'Mobile' : 'Desktop',
  ];
  if(INCLUDE_STATS) {
    var stats = ad.getStatsFor(DATE_RANGE);
    retVal = retVal.concat([
      stats.getImpressions(),
      stats.getClicks(),
      stats.getCtr(),
      stats.getCost(),
      stats.getAverageCpc(),
      stats.getConversions(),
      stats.getConversionRate(),
      stats.getAveragePageviews(),
      stats.getAveragePosition(),
      stats.getAverageTimeOnSite(),
      stats.getBounceRate()
      ]);
  }
  return '"' + retVal.join('","') + '"\n';
}
 
//This returns the column headings used for the report.
function getReportColumns() {
  var columnHeadings = [
    'AdId',
    'CampaignName','CampaignStatus',
    'AdGroupName','AdGroupStatus',
    'Headline',
    'Desc1',
    'Desc2',
    'DisplayUrl',
    'DestinationUrl',
    'Status',
    'Device'];
  if(INCLUDE_STATS) {
    columnHeadings = columnHeadings.concat([
      'Impressions',
      'Clicks',
      'Ctr',
      'Cost',
      'Cpc',
      'Conversions',
      'ConversionRate',
      'AveragePageviews',
      'AvgPosition',
      'AvgTimeOnSite',
      'BounceRate'
      ]);
  }
  return '"' + columnHeadings.join('","') + '"\n';
}   
 
//This function parses the creatives file into an object for processing
function parseFileContent(fileContent) {
  var headers = [];
  var idHash = {};
  var data = Utilities.parseCsv(fileContent);
  for(var i in data) {
    var cells = data[i]
    if(cells.length == 1) { continue; } //skip any empty rows
    if(i == 0) { 
      headers = cells; 
    } else {
      var rowMap = {};
      for(var x in headers) {
        headers[x] = headers[x];
        cells[x] = cells[x];
        rowMap[headers[x]] = cells[x];
      }
      idHash[rowMap['AdId']] = rowMap;
    }
  }
  return idHash;
}
 
//This function gets the file from GDrive
function getFile(fileName,folderPath) {
  var folder = getFolder(folderPath);
  if(folder.getFilesByName(fileName).hasNext()) {
    return folder.getFilesByName(fileName).next();
  } else {
    return null;
  }
}
 
//This function creates a new file on GDrive
function createNewFile(fileName,folderPath,content) {
  if(!fileName) { throw 'createNewFile: Missing filename.'; }
  var folder = getFolder(folderPath);
  
  return folder.createFile(fileName, content);
}
 
//This function finds the folder for the file and creates folders if needed
function getFolder(folderPath) {
  var folder = DriveApp.getRootFolder();
  if(folderPath) {
    var pathArray = folderPath.split('/');
    for(var i in pathArray) {
      var folderName = pathArray[i];
      if(folder.getFoldersByName(folderName).hasNext()) {
        folder = folder.getFoldersByName(folderName).next();
      } else {
        folder = folder.createFolder(folderName);
      }
    }
  }
  return folder;
}