Sunday, October 20, 2013

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;
}

Tuesday, July 30, 2013

Figuring Out When Your Ad, AdGroup, Keyword, or Campaign Was Created

Knowing when an Ad (or entity) was created is impossible using scripts. That information is simply not tracked in AdWords. The next best thing is to find out when your Ad first started receiving impressions and assume that is when it was created (If an Ad is created but no one sees it, does it really exist?).

So in order to help me keep track of when my Ads entities were created, I put together the following script to apply labels to my Ads entities with the date of the first impression. That way, I can find out what ads entities I created and make sure I don't take action on anything that is too young. I can also make changes to all the ads entities built on a given day relatively easily in the AdWords UI by just selecting the right label.

Thanks,
Russ



/**************************************
* Track Entity Creation Date
* Version 1.4
* Changelog v1.4
*  - Removed apiVersion from reporting call
* Changelog v1.3
*  - Updated script to handle all entities
* Changelog v1.2
*  - Fixed an issue with comparing dates
* ChangeLog v1.1
*  - Updated logic to work with larger accounts
* Created By: Russ Savage
* http://www.FreeAdWordsScripts.com
**************************************/
//All my labels will start with this. For example: Created:2013-05-01
var LABEL_PREFIX = 'Created:';
var DAYS_IN_REPORT = 30;
var ENTITY = 'ad'; //or adgroup or keyword or campaign
 
function main() {
  //First we get the impression history of our entity
  var ret_map = getImpressionHistory();
  //Then we apply our labels
  applyLabels(ret_map);
}
 
//Function to apply labels to the ads in an account
function applyLabels(ret_map) {
  var iter;
  if(ENTITY === 'campaign') { iter = AdWordsApp.campaigns().get(); }
  if(ENTITY === 'adgroup') { iter = AdWordsApp.adGroups().get(); }
  if(ENTITY === 'ad') { iter = AdWordsApp.ads().get(); }
  if(ENTITY === 'keyword') { iter = AdWordsApp.keywords().get(); }
  
  while(iter.hasNext()) {
    var entity = iter.next();
    var id = entity.getId();
    if(ret_map[id]) {
      var label_name = LABEL_PREFIX+Utilities.formatDate(ret_map[id], AdWordsApp.currentAccount().getTimeZone(), "yyyy-MM-dd");
      createLabelIfNeeded(label_name);
      entity.applyLabel(label_name);
    }
  }
}
 
//This is a helper function to create the label if it does not already exist
function createLabelIfNeeded(name) {
  if(!AdWordsApp.labels().withCondition("Name = '"+name+"'").get().hasNext()) {
    AdWordsApp.createLabel(name);
  }
}
 
//A helper function to find the date days ago
function getDateDaysAgo(days) {
  var the_past = new Date();
  the_past.setDate(the_past.getDate() - days);
  return Utilities.formatDate(the_past,AdWordsApp.currentAccount().getTimeZone(),"yyyyMMdd");
}
 
//A helper function to compare dates.
//Copied from: http://goo.gl/uW48a
function diffDays(firstDate,secondDate) {
  var oneDay = 24*60*60*1000; // hours*minutes*seconds*milliseconds
  return Math.round(Math.abs((firstDate.getTime() - secondDate.getTime())/(oneDay))); 
}
 
function getImpressionHistory() {
  var API_VERSION = { includeZeroImpressions : false };
  var first_date = new Date('10/23/2000');
  var max_days_ago = diffDays(first_date,new Date());
  var cols = ['Date','Id','Impressions'];
  var report = { 
    'campaign' : 'CAMPAIGN_PERFORMANCE_REPORT',
    'adgroup' : 'ADGROUP_PERFORMANCE_REPORT',
    'ad' : 'AD_PERFORMANCE_REPORT',
    'keyword' : 'KEYWORDS_PERFORMANCE_REPORT'}[ENTITY];
  var ret_map = {};
  var prev_days_ago = 0;
  for(var i = DAYS_IN_REPORT; i < max_days_ago; i+=DAYS_IN_REPORT) {
    var start_date = getDateDaysAgo(i);
    var end_date = getDateDaysAgo(prev_days_ago);
    var date_range = start_date+','+end_date;
    Logger.log('Getting data for ' + date_range);
    var query = ['select',cols.join(','),'from',report,'during',date_range].join(' ');
    var report_iter = AdWordsApp.report(query, API_VERSION).rows();
    if(!report_iter.hasNext()) { Logger.log('No more impressions found. Breaking.'); break; } // no more entries
    while(report_iter.hasNext()) { 
      var row = report_iter.next();
      if(ret_map[row['Id']]) {
        var [year,month,day] = (row['Date']).split('-');
        var from_row = new Date(year, parseFloat(month)-1, day);
        var from_map = ret_map[row['Id']];
         
        if(from_row < from_map) {
          ret_map[row['Id']] = from_row;
        }
      } else {
        var [year,month,day] = (row['Date']).split('-');
        ret_map[row['Id']] = new Date(year, parseFloat(month)-1, day);
      }
    }
    prev_days_ago = i;
  }
  return ret_map;
}

Thursday, July 11, 2013

Set AdParams at the AdGroup Level from a Google Spreadsheet

I've had a few people ask me how to update my previous script about Setting AdParams at the Keyword Level to be able to set them at the AdGroup level. Here is an update to that script that does just that.

To use it, you will need to create a new Google Spreadsheet and load it with the adgroup name, param 1 value, and param 2 value in columns A, B, and C respectively (with column headers). Then copy that url into the script and you should be good to go.

Thanks,
Russ

/************************************************
* Update Ad Params by Ad Groups
* Version 1.1
* ChangeLog v1.1
*  - Added the ability to enable param1 or 2 individually
*  - Looks for Keywords on all sheets
*  - Runs much faster
* Created By: Russ Savage
* FreeAdWordsScripts.com
************************************************/
var SPREADSHEET_URL = "PUT YOUR SPREADSHEET URL HERE";
var SET_PARAM1 = true;
var SET_PARAM2 = false;
var DATA_RANGE = 'A:D'; // A - CampaignName, B - AdGroupName, 
                        // C - Param1, D - Param2

function main() {
  var spreadsheet = SpreadsheetApp.openByUrl(SPREADSHEET_URL);
  var allSheets = spreadsheet.getSheets();
  var allData = [];
  for(var i in allSheets) {
    var sheet = allSheets[i];
    var data = sheet.getRange(DATA_RANGE).getValues();
    data.shift(); //get rid of headers
    allData = allData.concat(data);
  }
  
  var allDataHash = {};
  for(var i in allData) {
    var row = allData[i];
    if(row[0] === '') { continue; }
    var rowKey = [row[0],row[1]].join('~~!~~');
    allDataHash[rowKey] = { param1 : row[2], param2: row[3] };
  }
  
  var kwIter = AdWordsApp.keywords()
    .withCondition('CampaignStatus = ENABLED')
    .withCondition('AdGroupStatus = ENABLED')
    .withCondition('Status = ENABLED')
    .get();
  
  while(kwIter.hasNext()) { 
    var kw = kwIter.next();
    var campName = kw.getCampaign().getName();
    var adGroupName = kw.getAdGroup().getName();
    var rowKey = [campName,adGroupName].join('~~!~~');
    if(allDataHash[rowKey]) {
      if(SET_PARAM1) { kw.setAdParam(1, allDataHash[rowKey].param1); }
      if(SET_PARAM2) { kw.setAdParam(2, allDataHash[rowKey].param2); }
    }
  }
}

Monday, July 1, 2013

Campaign and Keyword Performance Reporting

Many of the daily or weekly reporting tasks that your boss asks you to put together can be automated using AdWords Scripts. One of my readers passed along the following request for a script that would save him time each day putting together some of his performance reports. Maybe it can save you some time as well.

The following script will build out a Google Spreadsheet with five tabs:
  1. Campaign Performance Summary for the Past 7 Days
  2. Campaign Performance Month to Date
  3. Campaign Performance for Last Month
  4. Keyword Performance Summary for the Past 7 Days
  5. Daily Keyword Performance for the Past 7 Days

All you need to do to get this up and running is to create a New Google Spreadsheet and paste the url into the script. Then you can schedule this script to run daily and when you get to work in the morning, all of your data will be refreshed.

Thanks,
Russ

/************************************
* Campaign and Keyword Summary Report
* Version: 1.2
* Changelog v1.2 - Fixed INVALID_PREDICATE_ENUM_VALUE
* ChangeLog v1.1 - Removed apiVersion from reporting call
* Created By: Russ Savage
* FreeAdWordsScripts.com
************************************/
var SPREADSHEET_URL = "PASTE GOOGLE SPREADSHEET URL HERE";

function main() {
  //These names are important. change them with caution
  var tabs = ['camp_perf_7_days','camp_perf_mtd','camp_perf_last_month','keyword_perf_7_days','keyword_perf_7_days_daily'];
  for(var i in tabs) {
    var results = runQuery(tabs[i]);
    writeToSpreadsheet(tabs[i],results);
  }
}

//Helper function to get or create the spreadsheet
function getSheet(tab) {
  var s_sheet = SpreadsheetApp.openByUrl(SPREADSHEET_URL);
  var sheet;
  try {
    sheet = s_sheet.getSheetByName(tab);
    if(!sheet) {
      sheet = s_sheet.insertSheet(tab, 0);
    }
  } catch(e) {
    sheet = s_sheet.insertSheet(tab, 0);
  }
  return sheet
}

//Function to write the rows of the report to the sheet
function writeToSpreadsheet(tab,rows) {
  var to_write = convertRowsToSpreadsheetRows(tab,rows);
  var s_sheet = SpreadsheetApp.openByUrl(SPREADSHEET_URL);
  var sheet = getSheet(tab);
  sheet.clear();
  
  var numRows = sheet.getMaxRows();
  if(numRows < to_write.length) {
    sheet.insertRows(1,to_write.length-numRows); 
  }
  var range = sheet.getRange(1,1,to_write.length,to_write[0].length);
  range.setValues(to_write);
}

//A generic function used to build and run the report query
function runQuery(tab) {
  var API_VERSION = { includeZeroImpressions : false };
  var cols = getColumns(tab);
  var report = getReport(tab);
  var date_range = getDateRange(tab);
  var where = getWhereClause(tab);
  var query = ['select',cols.join(','),'from',report,where,'during',date_range].join(' ');
  var report_iter = AdWordsApp.report(query, API_VERSION).rows();
  var rows = [];
  while(report_iter.hasNext()) { 
    rows.push(report_iter.next());
  }
  return rows;
}
 
//This function will convert row data into a format easily pushed into a spreadsheet
function convertRowsToSpreadsheetRows(tab,rows) {
  var cols = getColumns(tab);
  var ret_val = [cols];
  for(var i in rows) {
    var r = rows[i];
    var ss_row = [];
    for(var x in cols) {
      ss_row.push(r[cols[x]]);
    }
    ret_val.push(ss_row);
  }
  return ret_val;
}

//Based on the tab name, this returns the report type to use for the query
function getReport(tab) {
  if(tab.indexOf('camp_') == 0) {
    return 'CAMPAIGN_PERFORMANCE_REPORT';
  }
  if(tab.indexOf('keyword_') == 0) {
    return 'KEYWORDS_PERFORMANCE_REPORT';
  }
  throw new Exception('tab name not recognized: '+tab);
}

//Based on the tab name, this returns the where clause for the query
function getWhereClause(tab) {
  if(tab.indexOf('camp_') == 0) {
    return 'where CampaignStatus = ENABLED';
  }
  if(tab.indexOf('keyword_') == 0) {
    return 'where CampaignStatus = ENABLED and AdGroupStatus = ENABLED and Status = ENABLED';
  }
  throw new Exception('tab name not recognized: '+tab);
}

//Based on the tab name, this returns the columns to add into the report
function getColumns(tab) {
  var ret_array = [];
  if(tab.indexOf('daily') >= 0) {
    ret_array.push('Date');
  }
  ret_array.push('CampaignName');
  ret_array.push('CampaignStatus');
  
  if(tab.indexOf('keyword_') == 0) {
    ret_array = ret_array.concat(['AdGroupName',
                                  'AdGroupStatus',
                                  'Id',
                                  'KeywordText',
                                  'KeywordMatchType']);
  }
  return ret_array.concat(['Clicks',
                           'Impressions',
                           'Ctr',
                           'AverageCpc',
                           'Cost',
                           'AveragePosition',
                           'Conversions',
                           'ConversionRate',
                           'ConversionValue']);
}

//Based on the tab name, this returns the date range for the data.
function getDateRange(tab) {
  if(tab.indexOf('7_days') >= 0) {
    return 'LAST_7_DAYS';
  }
  if(tab.indexOf('mtd') >= 0) {
    return 'THIS_MONTH';
  }
  if(tab.indexOf('last_month') >= 0) {
    return 'LAST_MONTH';
  }
  throw new Exception('tab name not recognized: '+tab);
}