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

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