Showing posts with label reader requests. Show all posts
Showing posts with label reader requests. Show all posts

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

Saturday, June 22, 2013

Ad Creative Test Automation Script

This is a script I've been working on for a little while and I think it's really useful. I hope you do too.

Every SEM manager worth anything is constantly running creative tests. And many of you, since you are reading this blog, probably use scripts in some way to make that easier. But how do you figure out exactly when the tests start? They could be different for every AdGroup.

I have seen some people use labels, some people use schedules, but I never really liked having to keep track of those things. So I created this script which will keep track of the start dates for creative tests automatically.

The approach I took stores a copy of your ad ids in a google spreadsheet. Then it checks the current ads in the AdGroup against the ads from the spreadsheet and figures out if something changed by looking at the ad ids. If it has, the script assumes a new test has started and notes the date in the spreadsheet accordingly. Using this method, a marketer can be sure that the script is comparing statistics from the proper time frame to determine a test winner.

But then a reader mentioned that they wanted to be able to choose the metric to measure the ads by and also use a threshold to only start measuring after a certain number of clicks in the AdGroup. So I added that ability to the script as well. You only need to adjust a few parameters at the top of the script and you have a fully functional creative testing script in place.

Whenever it takes action on an ad by pausing it, the results will be sent to you in an email so that you can create a new challenger ad for that AdGroup.

I did a few things differently with this script. I always hate making my readers go into Google Docs and create a blank spreadsheet for my scripts to use as storage. So to get around that, this script will check for the existence of a special label in the account where the spreadsheet id is stored. If it finds one, it will use it. And if it is missing (like it will be on the first run), it will create the spreadsheet and label. This way, no messy spreadsheet urls in the scripts. It is a little bit of a workaround but until Google allows us to store additional config data for scripts, I thought this was an ok way to handle this.

As always, I'm open to your feedback on if this script is useful to you. What other metrics do you judge ad performance by? What else is this script missing?

Thanks,
Russ

/************************************
* Ad Creative Test Automation Script
* Version: 1.3
* Changelog v1.3 - Data is written to the spreadsheet a little faster
* Changelog v1.2 - Added additional threshold options
* Changelog v1.1 - Fixed issue with dates in email
* Created By: Russ Savage
* FreeAdWordsScripts.com
************************************/
// You can use any of the same values here as you can for METRIC below
var THRESHOLD = { metric : 'Clicks', value : 100 };
var TO = ['example@example.com'];
  
//Try any of these values for METRIC:
//AverageCpc, AverageCpm, AveragePageviews, AveragePosition, 
//AverageTimeOnSite, BounceRate, Clicks, ConversionRate, 
//Conversions, Cost, Ctr, Impressions
var METRIC = 'Ctr';
var ASC_OR_DESC = 'ASC'; //ASC - pause ad with lowest value, DESC - pause ad with highest value
  
function main() {
  //Start by finding what has changed in the account
  var ad_map = buildCurrentAdMap();
  var prev_ad_map = readMapFromSpreadsheet();
  prev_ad_map = updatePreviousAdMap(prev_ad_map,ad_map);
  writeMapToSpreadsheet(prev_ad_map);
    
  //Now run through the adgroups to find creative tests
  var ag_iter = AdWordsApp.adGroups().get();
  var paused_ads = [];
  while(ag_iter.hasNext()) {
    var ag = ag_iter.next();
    if(!prev_ad_map[ag.getId()]) { continue; }
      
    //Here is the date range for the test metrics
    var last_touched_str = _getDateString(prev_ad_map[ag.getId()].last_touched,'yyyyMMdd');
    var get_today_str = _getDateString(new Date(),'yyyyMMdd');
      
    var ag_stats = ag.getStatsFor(last_touched_str, get_today_str);
    if(ag_stats['get'+THRESHOLD.metric]() >= THRESHOLD.value) {
      var ad_iter = ag.ads().withCondition('Status = ENABLED')
                            .forDateRange(last_touched_str, get_today_str)
                            .orderBy(METRIC+" "+ASC_OR_DESC).get();
      var ad = ad_iter.next();
      var metric = ad.getStatsFor(last_touched_str, get_today_str)['get'+METRIC]();
      ad.pause();
      paused_ads.push({a : ad, m : metric});
    }
  }
  sendEmailForPausedAds(paused_ads);
}
  
// A function to send an email with an attached report of ads it has paused
function sendEmailForPausedAds(ads) {
  if(ads.length == 0) { return; } //No ads paused, no email
  var email_body = '"' + ['CampaignName','AdGroupName','Headline','Desc1','Desc2','DisplayUrl',METRIC].join('","') + '"\n';
  for(var i in ads) {
    var ad = ads[i].a;
    var metric = ads[i].m;
    email_body += '"' + [ad.getCampaign().getName(),
                         ad.getAdGroup().getName(),
                         ad.getHeadline(),
                         ad.getDescription1(),
                         ad.getDescription2(),
                         ad.getDisplayUrl(),
                         metric
                        ].join('","') +
                  '"\n';
  }
  var date_str = _getDateString(new Date(),'yyyy-MM-dd');
  var options = { attachments: [Utilities.newBlob(email_body, 'text/csv', "FinishedTests_"+date_str+'.csv')] };
  var subject = 'Finished Tests - ' + date_str;
  for(var i in TO) {
    MailApp.sendEmail(TO[i], subject, 'See attached.', options);
  }
}
  
//Given two lists of ads, this checks to make sure they are the same.
function sameAds(ads1,ads2) {
  for(var i in ads1) {
    if(ads1[i] != ads2[i]) { return false; }
  }
  return true;
}
  
//This reads the stored data from the spreadsheet
function readMapFromSpreadsheet() {
  var ad_map = {};
  var sheet = SpreadsheetApp.openById(findSpreadsheetId()).getActiveSheet();
  var data = sheet.getRange('A:C').getValues();
  for(var i in data) {
    if(data[i][0] == '') { break; }
    var [ag_id,last_touched,ad_ids] = data[i];
    ad_map[ag_id] = { ad_ids : (''+ad_ids).split(','), last_touched : new Date(last_touched) };
  }
  return ad_map;
}
  
//This will search for a label containing the spreadsheet id
//If one isn't found, it will create a new one and the label as well
function findSpreadsheetId() {
  var spreadsheet_id = "";
  var label_iter = AdWordsApp.labels().withCondition("Name STARTS_WITH 'history_script:'").get();
  if(label_iter.hasNext()) {
    var label = label_iter.next();
    return label.getName().split(':')[1]; 
  } else {
    var sheet = SpreadsheetApp.create('AdGroups History');
    var sheet_id = sheet.getId();
    AdWordsApp.createLabel('history_script:'+sheet_id, 'stores sheet id for adgroup changes script.');
    return sheet_id;
  }
}
  
//This will store the data from the account into a spreadsheet
function writeMapToSpreadsheet(ad_map) {
  var toWrite = [];
  for(var ag_id in ad_map) {
    var ad_ids = ad_map[ag_id].ad_ids;
    var last_touched = ad_map[ag_id].last_touched;
    toWrite.push([ag_id,last_touched,ad_ids.join(',')]);
  }
  writeToSpreadsheet(toWrite);
}

// Write the keyword data to the spreadsheet
function writeToSpreadsheet(toWrite) {
  var sheet = SpreadsheetApp.openById(findSpreadsheetId()).getActiveSheet();
  sheet.clear();
  
  var numRows = sheet.getMaxRows();
  if(numRows < toWrite.length) {
    sheet.insertRows(1,toWrite.length-numRows); 
  }
  var range = sheet.getRange(1,1,toWrite.length,toWrite[0].length);
  range.setValues(toWrite);
}
  
//This builds a map of the ads in the account so that it is easy to compare
function buildCurrentAdMap() {
  var ad_map = {}; // { ag_id : { ad_ids : [ ad_id, ... ], last_touched : date } }
  var ad_iter = AdWordsApp.ads().withCondition('Status = ENABLED').get();
  while(ad_iter.hasNext()) {
    var ad = ad_iter.next();
    var ag_id = ad.getAdGroup().getId();
    if(ad_map[ag_id]) {
      ad_map[ag_id].ad_ids.push(ad.getId());
      ad_map[ag_id].ad_ids.sort();
    } else {
      ad_map[ag_id] = { ad_ids : [ad.getId()], last_touched : new Date() };
    }
  }
  return ad_map;
}
  
//This takes the old ad map and the current ad map and returns an
//updated map with all changes.
function updatePreviousAdMap(prev_ad_map,ad_map) {
  for(var ag_id in ad_map) {
    var current_ads = ad_map[ag_id].ad_ids;
    var previous_ads = (prev_ad_map[ag_id]) ? prev_ad_map[ag_id].ad_ids : [];
    if(!sameAds(current_ads,previous_ads)) {
      prev_ad_map[ag_id] = ad_map[ag_id];
    }
  }
  return prev_ad_map;
}
  
//Helper function to format the date
function _getDateString(date,format) {
  return Utilities.formatDate(date,AdWordsApp.currentAccount().getTimeZone(),format); 
}

Friday, April 5, 2013

Dynamically Adjust Campaign Budgets v2.0

UPDATE 2013-04-06: Made sure daily budgets are multiplied by 30.5 to turn them into monthly budgets (line 150).

UPDATE 2013-04-06: Made sure that when the budgets are reset, they are divided by 30.5 to turn them into daily budgets (line 95).

UPDATE 2013-04-07: A big thank you to FoxSUP for helping me track down an issue with updating the budgets. Fixed line 78 to multiply the current budget by 1+to_change instead of just to_change. Also fixed a bug in calculating the change(line 56).


This is an update to a previous script I put together to dynamically adjust campaign budgets. There have been a few asks from some of the comments and I figured it would be easier to combine them into a new post.

The script will now keep track of the budgets you set at the beginning of the month via a Google Spreadsheet. You can also fill in the LABEL value if you only want this to operate on campaigns with that label name. Leaving it blank operates on all campaigns.

Thanks,
Russ

/********************************
* Dynamically Adjust Campaign Budgets v2.1
* Changelog v2.1 - Fixed opening of spreadsheet
* Created By: Russ Savage
* FreeAdWordsScripts.com
********************************/
// Let's set some constants
var TIMEFRAME = "THIS_MONTH";
//if the campaign is not in the spreadsheet, the budget is reset
//to this value at the beginning of the month.
var DEFAULT_BUDGET = 100; 
var SPREADSHEET_URL = "PLACE EMPYT SPREADSHEET URL HERE";
var LABEL = ""; //Fill in if you only want to operate on campaigns with this label
 
var SIG_FIGS = 1000; //this means round all calculations to 3 decimal places
var MONTHLY_BUDGET = 0; // we will set this later
 
function main() {
  MONTHLY_BUDGET = _pull_budget_data_from_spreadsheet();
  var tot_cost_mtd = _get_total_cost();
  var is_first_of_the_month = ((new Date()).getDate() == 1);
  is_first_of_the_month = (is_first_of_the_month && ((new Date()).getHours() == 0));
  Logger.log("Total cost: " + tot_cost_mtd + ", Monthly budget:" + MONTHLY_BUDGET);
   
  if(is_first_of_the_month) {
    _reset_budgets();
  } else {
    _adjust_campaign_budget(tot_cost_mtd);
  }
   
}
 
// Returns the total cost for the set TIMEFRAME
function _get_total_cost() {
  var camp_iter = (LABEL == "") ? AdWordsApp.campaigns().get() :
                                  AdWordsApp.campaigns()
                                    .withCondition("LabelNames CONTAINS_ANY ['"+LABEL+"']")
                                    .get();
   
  var tot_cost = 0;
  while(camp_iter.hasNext()) {
    tot_cost += camp_iter.next().getStatsFor(TIMEFRAME).getCost();
  }
  return tot_cost;
}
 
// Calculates run rate and adjusts campaign bids as needed.
function _adjust_campaign_budget(my_tot_cost) {
  var today = new Date();
  // Accounting for December
  var eom = (today.getMonth() == 11) ? new Date(today.getFullYear()+1,0,1) : 
                                       new Date(today.getFullYear(),today.getMonth()+1,1);
  var days_left = Math.round((eom-today)/1000/60/60/24);
  var days_spent = today.getDate();
  var run_rate = Math.round(my_tot_cost/days_spent*SIG_FIGS)/SIG_FIGS;
  var projected_total = my_tot_cost + (run_rate * days_left);
  var perc_over = Math.round(((MONTHLY_BUDGET-projected_total)/projected_total)*SIG_FIGS)/SIG_FIGS; 
  _change_spend(perc_over,my_tot_cost);
}
 
//Adjusts the budget for a given campaign based on percentage of total spend
//Note: if the cost of a campaign is $0 mtd, the budget is not changed.
function _change_spend(perc_to_change,my_tot_cost) {
  var camp_iter = (LABEL == '') ? AdWordsApp.campaigns()
                                    .withCondition("Status = ENABLED")
                                    .get() :
                                  AdWordsApp.campaigns()
                                    .withCondition("Status = ENABLED")
                                    .withCondition("LabelNames CONTAINS_ANY ['"+LABEL+"']")
                                    .get();
   
  while(camp_iter.hasNext()) {
    var camp = camp_iter.next();
    var camp_cost = camp.getStatsFor(TIMEFRAME).getCost();
    var perc_of_total = Math.round(camp_cost/my_tot_cost*SIG_FIGS)/SIG_FIGS;
    //If there is no cost for the campaign, let's not change it.
    var to_change = (perc_of_total) ? (perc_of_total*perc_to_change) : 0;
    camp.setBudget(camp.getBudget()*(1+to_change));
  }
}
 
// Resets the budget unevenly
function _reset_budgets() {
  var camp_budget_map = _pull_campaign_data_from_spreadsheet();
  var camp_iter = (LABEL == '') ? AdWordsApp.campaigns()
                                    .withCondition("Status = ENABLED")
                                    .get() :
                                  AdWordsApp.campaigns()
                                    .withCondition("Status = ENABLED")
                                    .withCondition("LabelNames CONTAINS_ANY ['"+LABEL+"']")
                                    .get();
  while(camp_iter.hasNext()) {
    var camp = camp_iter.next();
    if(camp_budget_map[camp.getName()]) {
      camp.setBudget(camp_budget_map[camp.getName()]/30.5);
    } else {
      camp.setBudget(DEFAULT_BUDGET);
    }
  }
}
 
function _pull_campaign_data_from_spreadsheet() {
  var spreadsheet = getSpreadsheet(SPREADSHEET_URL);
  var sheet = spreadsheet.getActiveSheet();
  var data = sheet.getRange("A:B").getValues();
  if(data[0][0] == "") {
    //This means this is the first run and we should populate the data.
    _populate_spreadsheet(sheet);
    data = sheet.getRange("A:B").getValues();
  }
  var campaign_budget_map = {};
  for(var i in data) {
    if(i == 0) { continue; } //ignore the header
    if(data[i][0] == "") { break; } //stop when there is no more data
    campaign_budget_map[data[i][0]] = parseFloat(data[i][1]);
  }
  return campaign_budget_map;
}
 
function _pull_budget_data_from_spreadsheet() {
  var spreadsheet = getSpreadsheet(SPREADSHEET_URL);
  var sheet = spreadsheet.getActiveSheet();
  var data = sheet.getRange("A:B").getValues();
  if(data[0][0] == "") {
    //This means this is the first run and we should populate the data.
    _populate_spreadsheet(sheet);
    data = sheet.getRange("A:B").getValues();
  }
  var tot_budget = 0;
  for(var i in data) {
    if(i == 0) { continue; } //ignore the header
    if(data[i][1] == "") { break; } //stop when there is no more data
    tot_budget += parseFloat(data[i][1]);
  }
  return tot_budget;
}
 
function _populate_spreadsheet(sheet) {
  sheet.clear();
  sheet.appendRow(['Campaign Name','Monthly Budget']);
  var camp_iter = (LABEL == '') ? AdWordsApp.campaigns()
                                    .withCondition("Status = ENABLED")
                                    .get() :
                                  AdWordsApp.campaigns()
                                    .withCondition("Status = ENABLED")
                                    .withCondition("LabelNames CONTAINS_ANY ['"+LABEL+"']")
                                    .get();
  while(camp_iter.hasNext()) {
    var camp = camp_iter.next();
    sheet.appendRow([camp.getName(),(camp.getBudget()*30.5)]);
  }
}
 
function getSpreadsheet(spreadsheetUrl) {
  return SpreadsheetApp.openByUrl(spreadsheetUrl);
}

Monday, March 18, 2013

Dynamically Adjust Campaign Budgets

UPDATE 2013-04-05: There is an updated version of this script. Check it out. Dynamically Adjust Campaign Budgets v2.0.

UPDATE 2013-04-07: A big thank you to FoxSUP for helping me track down an issue with updating the budgets. Fixed line 78 to multiply the current budget by 1+to_change instead of just to_change. Also fixed a bug in calculating the change (line 76).

Here is a request from a reader:
I manage many small business PPC accounts, and some of these accounts have multiple campaigns, and they usually have a relatively small monthly click budget. I'm looking for a way to pause ALL campaigns if the entire account has spent over a certain amount month-to-date.

This is actually a pretty easy script to put together. Below is a script that will do just that. You can set the MONTHLY_BUDGET at the beginning of the script and run this script daily on your account. Once the campaigns have a total cost greater than the budget specified, it will pause all the campaigns in the account.

Then, on the first of the next month, it will enable those campaigns once again. If you make no changes to the script below, it should do just that. But let's go one step further.

You actually have the power to get and set your campaign budgets using scripts. So let's say your monthly budget is $100, but you want to make sure your ads are spaced out through the month. I have added a function below called _adjust_campaign_budget() which can be enabled through the flag ADJUST_BUDGETS at the top of the script.

The script will then attempt to calculate a run rate for your campaign to figure out if you are going to meet your budget or not. If you are going to go over, it will lower the budget of each campaign (weighted by campaign cost) so that you come in at your target. If you are going to under-spend, it will also attempt to increase your campaign budget to try to allow you to hit your goal. I have also added a _reset_budgets() function to the end that gets called on the first of the month. If you run this script more frequently, you should enable the code for checking if it is the first hour of the first day of the month.

Now understandably, this script comes with a few cautions. THIS SCRIPT MAY CAUSE YOU TO SPEND A LOT OF MONEY. I'm sure the campaigns I was testing this on were quite a bit smaller than your campaigns, with budgets to match.

Thanks,
Russ

/******************************************
* Keep Your Campaigns In Budget
* Version 1.1
* ChangeLog v1.1 
*   - cleaned up code
*   - added ability for any dates
* Created By: Russ Savage
* FreeAdWordsScripts.com
******************************************/
// Let's set some constants
var MONTHLY_BUDGET = 5000.00;

//If you want to work with a monthly budget, leave START_DATE and END_DATE blank.
var TIMEFRAME = "THIS_MONTH";
//But if you want to work with a specific timeframe, fill these in.
//Use the format yyyyMMdd, so for Jan 12th, 2014, you would put 20140112.
var START_DATE = '';
var END_DATE = '';
 
//Set this to true if you want to adjust budgets or
//keep set to false if you want to just pause all the campaigns
//when you hit your budget
var ADJUST_BUDGETS = false;
var DECIMAL_PLACES = 3;
 
function main() {
  var totalCostMTD = getTotalCost();
  var isFirstOfTheMonth = ((new Date()).getDate() == 1);
  if(START_DATE && END_DATE) {
    var today = new Date();
    today.setHours(0,0,0,0);
    var startDate = new Date(START_DATE.substring(0,4),
                             parseFloat(START_DATE.substring(4,6))-1,
                             START_DATE.substring(6,8));
    isFirstOfTheMonth = (startDate.getTime() == today.getTime());
  }
  //if you run this script more than once per day, uncomment the next line
  //isFirstOfTheMonth = (isFirstOfTheMonth && ((new Date()).getHour() == 0));
  Logger.log("Total cost: " + totalCostMTD + 
           ", Monthly budget:" + MONTHLY_BUDGET +
           ", isFirstOfTheMonth: "+isFirstOfTheMonth);
   
  if(ADJUST_BUDGETS) {
    if(isFirstOfTheMonth) {
      resetBudgets();
    } else {
      adjustCampaignBudget(totalCostMTD);
    }
  } else {
    if(totalCostMTD >= MONTHLY_BUDGET) {
      //If we have hit the limit, pause all ads
      enableOrDisableCampaigns(true);
    } else {
      // let's check if it's the first day of the month
      if((new Date()).getDate() == 1) {
        //enable all the campaigns
        enableOrDisableCampaigns(false);
      }
    }
  }
}
 
// Returns the total cost for the set TIMEFRAME
function getTotalCost() {
  var campIter = AdWordsApp.campaigns().get();
   
  var totalCost = 0;
  while(campIter.hasNext()) {
    if(START_DATE && END_DATE) {
      totalCost += campIter.next().getStatsFor(START_DATE,END_DATE).getCost();
    } else {
      totalCost += campIter.next().getStatsFor(TIMEFRAME).getCost();
    }
  }
  return totalCost;
}
 
// Enables or Disables All Campaigns In Account
function enableOrDisableCampaigns(shouldDisable) {
  var campIter = AdWordsApp.campaigns().get();
  while(campIter.hasNext()) { 
    if(shouldDisable) { 
      campIter.next().pause(); 
    } else { 
      campIter.next().enable(); 
    }
  }
}
 
// Calculates run rate and adjusts campaign bids as needed.
function adjustCampaignBudget(myTotalCost) {
  var today = new Date();
  // Accounting for December
  var eom;
  if(START_DATE && END_DATE) {
    eom = new Date(END_DATE.substring(0,4),
                   parseFloat(END_DATE.substring(4,2))-1,
                   END_DATE.substring(6,2));
  } else {
    eom = (today.getMonth() == 11) ? new Date(today.getFullYear()+1,0,1) : 
                                     new Date(today.getFullYear(),today.getMonth()+1,1);
  }
  var daysLeft = Math.round((eom-today)/1000/60/60/24);
  var daysSpent;
  if(START_DATE && END_DATE) {
    var startDate = new Date(START_DATE.substring(0,4),
                             parseFloat(START_DATE.substring(4,2))-1,
                             START_DATE.substring(6,2));
    daysSpent = Math.round((today-startDate)/1000/60/60/24);
  } else {
    daysSpent = today.getDate();
  }
  var runRate = round(myTotalCost/daysSpent);
  var projectedTotal = myTotalCost + (runRate * daysLeft);
  var percOver = round((MONTHLY_BUDGET-projectedTotal)/projectedTotal);
   
  changeSpend(percOver,myTotalCost);
}
 
//Adjusts the budget for a given campaign based on percentage of total spend
//Note: if the cost of a campaign is $0 mtd, the budget is not changed.
function changeSpend(percToChange,myTotalCost) {
  var campIter = AdWordsApp.campaigns().withCondition("Status = ENABLED").get();
   
  while(campIter.hasNext()) {
    var camp = campIter.next();
    var campCost = (START_DATE && END_DATE) ? camp.getStatsFor(START_DATE,END_DATE).getCost()
                                            : camp.getStatsFor(TIMEFRAME).getCost();
    var percOfTotal = round(campCost/myTotalCost);
    //If there is no cost for the campaign, let's not change it.
    var toChange = (percOfTotal) ? (percOfTotal*percToChange) : 0;
    camp.setBudget(camp.getBudget()*(1+toChange));
  }
}
 
// Resets the budget evenly across all campaigns
function resetBudgets() {
  Logger.log('Resetting budgets at the first of the period.');
  var campIter = AdWordsApp.campaigns().withCondition("Status = ENABLED").get();
  var campCount = 0;
  while(campIter.hasNext()) {
    campCount++;
    campIter.next();
  }
  campIter = AdWordsApp.campaigns().withCondition("Status = ENABLED").get();
  while(campIter.hasNext()) {
    campIter.next().setBudget(MONTHLY_BUDGET/campCount);
  }
}

// A helper function to make rounding a little easier
function round(value) {
  var decimals = Math.pow(10,DECIMAL_PLACES);
  return Math.round(value*decimals)/decimals;
}