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

Sunday, November 17, 2013

Track Adwords Script Runs with Google Analytics

The other day, I was looking into how I could report on how many times my AdWords scripts were running. I figured that since I use Google Analytics reporting for everything else, maybe I should try to use it for reporting on my scripts as well.

So using some information gathered around the web, I put together this simple script to push data into Google Analytics. This will register each script run as a pageview. If you want to keep track of account ids where the script is running, you will need to add AdWordsApp.currentAccount().getCustomerId() somewhere in the CAMPAIGN or PAGE values. Once you have this code in your script, just make a call to beacon(); at the beginning of your code. And since many tracking systems mimic the Google Analytics tracking format, I'm sure this can be easily adapted to other systems.

Thanks,
Russ

/********************************
* Track Script Runs in Google Analytics
* Created By: Russ Savage
* FreeAdWordsScripts.com
********************************/
function beacon() {
  var TAG_ID = 'UA-XXXXXXXX-X';
  var CAMPAIGN_SOURCE = 'adwords';
  var CAMPAIGN_MEDIUM = 'scripts';
  var CAMPAIGN_NAME = 'Your Script Name And Version';
  var HOSTNAME = 'www.freeadwordsscripts.com';
  var PAGE = '/Some/Virtual/Page/Similar/To/Campaign/Name/Probably';
  var DOMAIN_LINK = 'http://'+HOSTNAME+PAGE;

  //Pulled from: http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
  var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, 
    function(c) {var r = Math.random()*16|0,v=c=='x'?r:r&0x3|0x8;return v.toString(16);});
  
  var url = 'http://www.google-analytics.com/collect?';
  var payload = {
    'v':1,'tid':TAG_ID,'cid':uuid,    
    't':'pageview','cs':CAMPAIGN_SOURCE,'cm':CAMPAIGN_MEDIUM,'cn':CAMPAIGN_NAME,
    'dl':DOMAIN_LINK
  };
  var qs = '';
  for(var key in payload) {
    qs += key + '=' + encodeURIComponent(payload[key]) + '&';
  }
  url += qs.substring(0,qs.length-1);
  UrlFetchApp.fetch(url);
}

Monday, November 11, 2013

Building Entity Deep Links with AdWords Scripts

I was trying to build a report on problems in an account and I was thinking to myself, "Man, it sure would be nice to deep link directly to the entity that was having issues." Then I realized that is the script change logs could do it, I probably could too.

It turns out, there are two magic numbers that you need in order to get this to work. When you login to your account, in the url, you will see __u= and __c=. According to this blog post, these values are the 'effectiveUserId' and 'customerId' respectively. Unfortunately, there isn't a way to access these values when using scripts, so you will have to manually copy them into the script below.

After that, you can include the function in all your scripts and deep link to your heart's content. It isn't the prettiest thing in the world but it is self contained so it should be easy to copy into the bottom of your scripts.

Thanks,
Russ

  // Link to the Keyword Tab of the AdGroup
  Logger.log(getUrl(someAdGroupEntity,'Keywords'));
  // Link to the Ads Tab of the AdGroup
  Logger.log(getUrl(someAdGroupEntity,'Ads'));      
  // Link to Location Settings Tab of the Campaign
  Logger.log(getUrl(comeCampaignEntity,'Settings:Locations')); 


/***********************************
* Build Deep Link Urls for Entities
* Version 1.0 
* Created By: Russ Savage
* FreeAdWordsScripts.com
***********************************/
function getUrl(entity,tab) {
  var customerId = '__c from the url';
  var effectiveUserId = '__u from the url';
  var decodedTab = getTab(tab);  
  
  var base = 'https://adwords.google.com/cm/CampaignMgmt?';
  var url = base+'__c='+customerId+'&__u='+effectiveUserId+'#';
  
  if(typeof entity['getBudget'] !== 'undefined') {
    //A Campaign
    return url+'c.'+entity.getId()+'.'+decodedTab+'&app=cm';
  }
  if(typeof entity['createKeyword'] !== 'undefined') {
    //An AdGroup
    return url+'a.'+entity.getId()+'_'+entity.getCampaign().getId()+'.'+decodedTab+'&app=cm';
  }
  if(typeof entity['getMatchType'] !== 'undefined') {
    //A Keyword
    return url+'a.'+entity.getAdGroup().getId()+'_'+entity.getCampaign().getId()+'.key&app=cm';
  }
  if(typeof entity['getHeadline'] !== 'undefined') {
    //An Ad
    return url+'a.'+entity.getAdGroup().getId()+'_'+entity.getCampaign().getId()+'.create&app=cm';
  }
  return url+'r.ONLINE.di&app=cm';
  
  function getTab(tab) {
    var mapping = {
      'Ad groups':'ag','Settings:All settings':'st_sum',
      'Settings:Locations':'st_loc','Settings:Ad schedule':'st_as',
      'Settings:Devices':'st_p','Ads':'create',
      'Keywords':'key','Audiences':'au','Ad extensions':'ae',
      'Auto targets':'at','Dimensions' : 'di'
    };
    if(mapping[tab]) { return mapping[tab]; }
    return 'key'; //default to keyword tab
  }
}