Showing posts with label account maintenance. Show all posts
Showing posts with label account maintenance. Show all posts

Saturday, November 30, 2013

AdWords Account Audit Checklist Using AdWords Scripts

I was browsing through some old WordStream blog posts and noticed this one from Phil Kowalski about an AdWords Account Audit Checklist. It seemed like most of that work could be automated using AdWords scripts so I figured I'd give it a shot.

The following script runs through as many of the checks as possible and tries to report areas to check out first. This would be useful for making sure that an account you take over has at least the basics going for it.

To see the results, run the script in your account, then click on the "View details" link and hit the "Logs" button. You should see all the warnings that the script found. Try it out and let me know what additional checks you might add.

Thanks,
Russ

/************************************
* AdWords Account Audit Checklist
* Version 1.1
* ChangeLog v1.1 - Fixed issue with extension selector.
* Based on the blog post by Phil Kowalski
* http://www.wordstream.com/blog/ws/2013/07/02/adwords-account-audit-checklist
* Created By: Russ Savage
* FreeAdWordsScripts.com
************************************/
function main() {
  //1. Campaigns
  //  a. Target the right locations
  var includedLocList = ['United States','Canada']; // <-- the list of places your campaigns should be targeting
  verifyTargetedLocations(includedLocList);
  
  var excludedLocList = ['Europe']; // <-- the list of places your campaigns should be excluding
  verifyExcludedLocations(excludedLocList);
  
  //  b. Language - Can't be done using scripts yet :(
  //  c. Search vs Display
  verifySearchAndDisplay();
  
  //  d. Check Mobile Strategy
  verifyMobileModifiers();
  
  //2. AdGroups
  //  a. Check for AdGroups with more than 20-30 keywords
  var ADGROUP_SIZE = 25; // <-- this is the max number of keywords you want in an AdGroup
  verifyAdGroupSize(ADGROUP_SIZE);
  
  //  b. Check for topic. Difficult to do with scripts
  //  c. Check for ads
  var NUMBER_OF_ADS = 3; // <-- this is the minimum number of ads in an AdGroup
  verifyAdGroupNumberOfAds(NUMBER_OF_ADS);
  
  //3. Keywords
  //  a. Check for MatchTypes
  printMatchTypes();
  
  //4. Search Queries
  //  This analysis is probably worth it's own script
  
  //5. Other
  //  a. Conversion Tracking
  verifyConversionTracking();
  
  //  b. AdExtensions
  verifyAdExtensions();
}

function verifyConversionTracking() {
  //Assume that if the account has not had a conversion in 7 days, something is wrong.
  var campsWithConversions = AdWordsApp.campaigns()
    .withCondition('Status = ENABLED')
    .forDateRange('LAST_7_DAYS')
    .withCondition('Conversions > 0')
    .get().totalNumEntities();
  if(campsWithConversions == 0) {
    warn('Account is probably missing conversion tracking.');
  }
}

function verifyAdExtensions() {
  var campIter = AdWordsApp.campaigns().withCondition('Status = ENABLED').get();
  while(campIter.hasNext()) {
    var camp = campIter.next();
    var phoneNumExtCount = camp.extensions().phoneNumbers().get().totalNumEntities();
    if(phoneNumExtCount == 0) {
      warn('Campaign: "'+camp.getName()+'" is missing phone number extensions.');
    }
    var siteLinksExtCount = camp.extensions().sitelinks().get().totalNumEntities();
    if(siteLinksExtCount < 6) {
      warn('Campaign: "'+camp.getName()+'" could use more site links. Currently has: '+siteLinksExtCount);
    }
    var mobileAppsExtCount = camp.extensions().mobileApps().get().totalNumEntities();
    if(mobileAppsExtCount == 0) {
      warn('Campaign: "'+camp.getName()+'" is missing mobile apps extension.');
    }
  }
}

function printMatchTypes() {
  var numBroad = AdWordsApp.keywords()
    .withCondition('Status = ENABLED')
    .withCondition('AdGroupStatus = ENABLED')
    .withCondition('CampaignStatus = ENABLED')
    .withCondition('KeywordMatchType = BROAD')
    .get().totalNumEntities();
  var numPhrase = AdWordsApp.keywords()
    .withCondition('Status = ENABLED')
    .withCondition('AdGroupStatus = ENABLED')
    .withCondition('CampaignStatus = ENABLED')
    .withCondition('KeywordMatchType = PHRASE')
    .get().totalNumEntities();
  var numExact = AdWordsApp.keywords()
    .withCondition('Status = ENABLED')
    .withCondition('AdGroupStatus = ENABLED')
    .withCondition('CampaignStatus = ENABLED')
    .withCondition('KeywordMatchType = EXACT')
    .get().totalNumEntities();
  var total = numBroad+numPhrase+numExact;
  var percBroad = Math.round(numBroad/total*100);
  var percPhrase = Math.round(numPhrase/total*100);
  var percExact = Math.round(numExact/total*100);
  info('Out of a total of: '+total+' active keywords in your account:');
  info('\tBroad: '+numBroad+' or '+percBroad+'%');
  info('\tPhrase: '+numPhrase+' or '+percPhrase+'%');
  info('\tExact: '+numExact+' or '+percExact+'%');
}

function verifyAdGroupNumberOfAds(requiredNumberOfAds) {
  var agIter = AdWordsApp.adGroups()
    .withCondition('Status = ENABLED')
    .withCondition('CampaignStatus = ENABLED')
    .get();
  while(agIter.hasNext()) {
    var ag = agIter.next();
    var adCount = ag.ads().withCondition('Status = ENABLED').get().totalNumEntities();
    if(adCount < requiredNumberOfAds) {
      warn('Campaign: "'+ag.getCampaign().getName()+'" AdGroup: "'+ag.getName()+'" does not have enough ads: '+adCount);
    }
    if(adCount > (requiredNumberOfAds+2)) {
      warn('Campaign: "'+ag.getCampaign().getName()+'" AdGroup: "'+ag.getName()+'" has too many ads: '+adCount);
    }
  }
}

function verifyAdGroupSize(size) {
  var agIter = AdWordsApp.adGroups()
    .withCondition('Status = ENABLED')
    .withCondition('CampaignStatus = ENABLED')
    .get();
  while(agIter.hasNext()) {
    var ag = agIter.next();
    var kwSize = ag.keywords().withCondition('Status = ENABLED').get().totalNumEntities();
    if(kwSize >= size) {
      warn('Campaign: "'+ag.getCampaign().getName()+'" AdGroup: "'+ag.getName()+'" has too many keywords: '+kwSize);
    }
  }
}

function verifyMobileModifiers() {
  var campIter = AdWordsApp.campaigns().withCondition('Status = ENABLED').get();
  while(campIter.hasNext()) {
    var camp = campIter.next();
    var desktop = camp.targeting().platforms().desktop().get().next();
    //var tablet = camp.targeting().platforms().tablet().get().next();
    var mobile = camp.targeting().platforms().mobile().get().next();
    //check for mobile modifiers
    if(desktop.getBidModifier() == 1 && mobile.getBidModifier() == 1) {
      warn('Campaign: "'+camp.getName()+'" has no mobile modifier set.');
    }
  }
}

function verifyTargetedLocations(locList) {
  var campIter = AdWordsApp.campaigns().withCondition('Status = ENABLED').get();
  while(campIter.hasNext()) {
    var camp = campIter.next();
    var locIter = camp.targeting().targetedLocations().get();
    reportOnLocations(camp,locIter,locList);
  } 
}

function verifyExcludedLocations(locList) {
  var campIter = AdWordsApp.campaigns().withCondition('Status = ENABLED').get();
  while(campIter.hasNext()) {
    var camp = campIter.next();
    var locIter = camp.targeting().excludedLocations().get();
    reportOnLocations(camp,locIter,locList);
  } 
}

function reportOnLocations(camp,locIter,locList) {
  var campLocList = [];
  while(locIter.hasNext()) {
    var loc = locIter.next();
    campLocList.push(loc.getName());
    if(!locList) {
      warn('Campaign: "'+camp.getName()+'" targeting: "'+loc.getName()+'"');
    }
  }
  if(locList && campLocList.sort() != locList.sort()) {
    for(var i in campLocList) {
      if(locList.indexOf(campLocList[i]) == -1) {
        warn('Campaign: "'+camp.getName()+'" incorrectly targeting: "'+campLocList[i]+'"');
      }
    }
    for(var i in locList) {
      if(campLocList.indexOf(locList[i]) == -1) {
        warn('Campaign: "'+camp.getName()+'" not targeting: "'+locList[i]+'"');
      }
    }
  }
}

function verifySearchAndDisplay() {
  var API_VERSION = { includeZeroImpressions : false };
  var cols = ['CampaignId','CampaignName','AdNetworkType1','Impressions'];
  var report = 'CAMPAIGN_PERFORMANCE_REPORT';
  var query = ['select',cols.join(','),'from',report,'during','LAST_30_DAYS'].join(' ');
  var results = {}; // { campId : { agId : [ row, ... ], ... }, ... }
  var reportIter = AdWordsApp.report(query, API_VERSION).rows();
  while(reportIter.hasNext()) {
    var row = reportIter.next();
    if(results[row.CampaignId]) {
      warn('Campaign: "'+row.CampaignName+'" is targeting the Display and Search networks.');
    } else {
      results[row.CampaignId] = row;
    }
  }
  return results;
}

function warn(msg) {
  Logger.log('WARNING: '+msg);
}

function info(msg) {
  Logger.log(msg);
}

Monday, March 4, 2013

Fixing Capitalization Errors in Your Ads

One of the issues that marketers face when they have systems for automatically building creatives from feeds is quality control. I recently ran into an issue where I created around 1,000 new ads where the city name was IN ALL CAPS. Google of course doesn't like that much and I didn't like the idea of having to manually update 1,000 ads, so I put together the following script.

The script runs through your account for all of the disapproved ads and tries to find ones with words in ALL CAPS. I'm not sure what Google's limit on caps is, but in the script below, it looks for anything with 3 or more capital letters in a row. Then it replaces them, creates a new ad, and if it was successful, deletes the old ad.

I ran into some trouble trying to identify the reasons the ads were disapproved. It is not yet available in the API, but it has been requested. The other issue was that sometimes, the new ad you create will fail, which will cause you to delete the old one and not create a new one. I solved that one by counting the ads in the adgroup before and after I created the new ad in order to make sure my ad had been created. Again, I posted the feature request in the adwords scripting forums.

Thanks,
Russ

//-----------------------------------
// Fix Ads with EXCESSIVE CAPITALIZATION
// Created By: Russ Savage
// FreeAdWordsScripts.com
//-----------------------------------
function main() {
  var find_caps = /[A-Z]{3,}/g;
  var SEP = '~~@~~'; // this needs to be something you would never put in your ads.
  var ad_iter = AdWordsApp.ads().withCondition("ApprovalStatus = 'DISAPPROVED'").get();
  
  while(ad_iter.hasNext()) {
    var ad = ad_iter.next();
    var old_ad_cnt = get_ad_count(ad.getAdGroup());
    var old_ad_str = [ad.getHeadline(),ad.getDescription1(),ad.getDescription2(),ad.getDisplayUrl()].join(SEP);
    var new_ad_str = old_ad_str;
    Logger.log("Before:"+old_ad_str);
    var m = "";
    while((m = find_caps.exec(new_ad_str)) != null) {
      new_ad_str = replace_all(new_ad_str,m[0],init_cap(m[0]),false);
    }
    Logger.log("After:"+new_ad_str);
    if(old_ad_str != new_ad_str) {
      var [new_headline,new_desc1,new_desc2,new_disp_url] = new_ad_str.split(SEP);
      ad.getAdGroup().createTextAd(new_headline, new_desc1, new_desc2, new_disp_url, ad.getDestinationUrl());
      var new_ad_cnt = get_ad_count(ad.getAdGroup());
      if(new_ad_cnt == (old_ad_cnt+1)) {
        ad.remove();
      }
    } else {
      Logger.log("Skipping because no changes were made."); 
    }
  }
  
  function init_cap(s) {
    return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
  }
  
  // This function was adapted from: http://dumpsite.com/forum/index.php?topic=4.msg8#msg8 
  function replace_all(original,str1, str2, ignore) {
    return original.replace(new RegExp(str1.replace(/([\/\,\!\\\^\$\{\}\[\]\(\)\.\*\+\?\|\<\>\-\&])/g,"\\$&"),(ignore?"gi":"g")),(typeof(str2)=="string")?str2.replace(/\$/g,"$$$$"):str2);
  }
  
  function get_ad_count(ad_group) {
    var ad_iter = ad_group.ads().get();
    var new_ad_cnt = 0;
    while(ad_iter.hasNext()) {
      ad_iter.next();
      new_ad_cnt++;
    }
    return new_ad_cnt;
  }
}

Tuesday, December 11, 2012

Auto Add ValueTrack Params To All Destination URLs

Today we have a script that will help make sure all of your keywords are tagged with the appropriate ValueTrack parameters.  You can use this to ensure that all of your tracking params are added correctly to your urls.

You can add your own query string parameters to the mapping at the top of the script.  For example, if all the keywords in your account need to have the parameter "channel=sem" in their destination url, you could add that to the URL_PARAMS_TO_ADD mapping at the top of the file and it will be added to all your urls.

Also, I updated the script to allow you to add whatever values you want from the account into the url. See the examples for adding Campaign and AdGroup names to the url if you like.

Thanks,
Russ

/******************************************
* Auto Add ValueTrack (and other) Params If Not There
* Created By: Russ Savage
* Version 1.1
* ChangeLog v1.1
*  - Added the ability to add function calls into the parameters to add
* FreeAdWordsScripts.com
******************************************/
function main() {
   
  var URL_PARAMS_TO_ADD = { 
    "kw" : "{keyword}",
    "crid" : "{creative}",
    "mt" : "{matchtype}",
    "ntwk" : "{network}",
    "ap" : "{adposition}",
    "dv" : "{device}",
    "dvm" : "{devicemodel}",
    //"camp" : getCampaignName,  <----- This function is defined below
    //"ag" : getAdGroupName  <----- and so is this one
  };
   
  var kw_iter = AdWordsApp.keywords()
                  .withCondition("DestinationUrl != ''")
                  .get();
  
  while(kw_iter.hasNext()) {
    var kw = kw_iter.next();
    var destUrl = kw.getDestinationUrl();
    if(!destUrl || destUrl === '') { continue; }
    var toAppend = [];
    for(var key in URL_PARAMS_TO_ADD) {
      if(destUrl.indexOf(key) >= 0) { continue; }
      var value = URL_PARAMS_TO_ADD[key];
      
      //check to see if we should call a function
      if(typeof(value) === 'function') {
        value = encodeURI(value(kw));
      }
      toAppend.push(key+"="+value);
    }
    if(destUrl.indexOf('?') >= 0) {
      kw.setDestinationUrl(destUrl + "&"+toAppend.join('&'));
    } else {
      kw.setDestinationUrl(destUrl + "?"+toAppend.join('&'));
    }
    Logger.log(kw.getDestinationUrl());
  }
}

// As long as the function call is passed a kw object, you can insert
// whatever value you want into the url
function getCampaignName(kw) {
  return kw.getCampaign().getName();
}

function getAdGroupName(kw) {
  return kw.getAdGroup().getName();
}

Monday, December 3, 2012

Automating Maintenance Tasks with AdWords Scripting Part 2

Let's continue with automating the maintenance tasks found in AdWords for Dummies with AdWords scripting. Let's continue with the maintenance that is run on the 15th of the month.


Reward keywords that are bringing in cheap traffic.


Similar to the previous post, you will need to have set up conversion tracking for all of this to work.  We can actually use a variation of the previous script to accomplish this.  Really we are doing the same thing just increasing bids instead of decreasing them.

//-----------------------------------
// Increase Bids Cheap Conversion Keywords
// Created By: Russ Savage
// FreeAdWordsScripts.com
//-----------------------------------
function main() {
  //For keywords with less than $5 CPC, let's pump those bids up by 35%
  var AMAZING_COST_PER_CONV = 5;
  var AMAZING_BID_INCREASE_AMOUNT = .35;
  
  //For keywords with between $5 and $8 CPCs, we will only increase the bids by 20%
  var GREAT_COST_PER_CONV = 8;
  var GREAT_BID_INCREASE_AMOUNT = .20;
  
  var kw_iter = AdWordsApp.keywords()
    .withCondition("Status = ENABLED")
    .get();
  
  while(kw_iter.hasNext()) {
    var kw = kw_iter.next();
    var kw_stats = kw.getStatsFor("LAST_30_DAYS");
    var cost = kw_stats.getCost();
    var conversions = kw_stats.getConversions();
    if(conversions > 0) {
      var cost_per_conversion = (cost/(conversions*1.0));
      
      if(cost_per_conversion <= AMAZING_COST_PER_CONV) {
        kw.setMaxCpc(kw.getMaxCpc() * (1+AMAZING_BID_INCREASE_AMOUNT)); 
      }
      else if(cost_per_conversion <= GREAT_COST_PER_CONV) {
        kw.setMaxCpc(kw.getMaxCpc() * (1+GREAT_BID_INCREASE_AMOUNT));
      }
    }else{
      //no conversions on this keyword
      //we will deal with that later
      continue;
    }
  }
}
Stay tuned for the next and final installment where we de-clutter our accounts by deleting the keywords with no conversions.

Thanks,
Russ