New Script: Are You Ready for Parallel Tracking?

On October 30th, Google Ads requires all URL’s to be compatible with parallel tracking. To avoid surprises deep in our clients’ accounts, we’ve developed a simple script that I’m sharing today.

If you have tracking templates in your accounts starting with anything but {lpurl} or {unescapedlpurl}, you’ll have to make sure you’re ready for parallel tracking. If you don’t have any of these, you’re fine.

Our script goes through one or more accounts and checks the tracking templates of campaigns, ad groups, ads, and targeting criteria (like keywords). In case it finds a template that needs to support parallel tracking, it makes a note in the script’s logs. That’s all the script does.

Whether a tracking URL actually supports parallel tracking is between you and your tracking service provider. The script cannot determine this.

How to get started

Just copy the following code and run the script. That’s it.

The Code
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Ready for parallel tracking? (English Version)
* © 2018 Martin Roettgerding, Bloofusion Germany GmbH.
* www.bloofusion.de
* 
* If you have tracking templates pointing to a fixed domain, you *might* have a problem with parallel tracking. It doesn't mean that there is a problem, but it should be checked out.
* This script checks for potential problems with parallel tracking at the campaign, adgroup, ad, and adgroup targeting criteria (keywords, audiences, etc.) level. It does not check account level or sitelink settings, nor product data feeds (unaccessible by scripts).
* In any case, you should check the account setting manually: All campaigns (do not select any) > Settings (grey navigation bar) > Account settings (second top navigation bar)
* Check whether a tracking template is being used. If it's empty or starts with either {lpurl} or {unescapedlpurl}, parallel tracking won't be a problem.
*/

// In case you only want to check some of the accounts in an MCC, label them and specify the label's name here:
var accountLabelName = "";

function main() {
  Logger.log("Scripts cannot check account settings, sitelinks or product data feeds.");
  Logger.log("It is recommended to check the account setting as well as product data feeds (if applicable) manually.");
  Logger.log("---------------------------------------------------------------------------------------------------");
  
  // Check if this is running on MCC level or account level.
  if(typeof MccApp !== 'undefined'){
    // Based on https://outshine.com/blog/run-your-adwords-scripts-across-a-lot-of-accounts
    var accountSelector = MccApp.accounts();
    if(accountLabelName) accountSelector = accountSelector.withCondition("LabelNames CONTAINS '" + accountLabelName + "'");
    
    var accountIterator = accountSelector.get();
    var accountIds = [];
    while (accountIterator.hasNext()) {
      var account = accountIterator.next();
      accountIds.push(account.getCustomerId());
    }
    var parallelIds = accountIds.slice(0, 50);
    var sequentialIds = accountIds.slice(50);
    	
    Logger.log(accountIds.length + " accounts to process.");
    var params = { "sequentialIds" : sequentialIds };
    MccApp.accounts().withIds(parallelIds).executeInParallel("checkParallelTrackingStatus", "allDone", JSON.stringify(params));

  }else{
    var params = JSON.stringify({ });
    checkParallelTrackingStatus(params);
  }
}

function checkParallelTrackingStatus(params){
  var currentAccountName = AdWordsApp.currentAccount().getName();
  if(currentAccountName == "") currentAccountName = AdWordsApp.currentAccount().getCustomerId();
  
  var trackingUrlTemplates = {};
  
  trackingUrlTemplates = checkForTrackingUrlTemplates(trackingUrlTemplates, "campaigns");
  trackingUrlTemplates = checkForTrackingUrlTemplates(trackingUrlTemplates, "adgroups");
  trackingUrlTemplates = checkForTrackingUrlTemplates(trackingUrlTemplates, "ads");
  trackingUrlTemplates = checkForTrackingUrlTemplates(trackingUrlTemplates, "criteria");
	
  var foundSomething = false;
  for(var trackingUrlTemplate in trackingUrlTemplates){
    if(!isTrackingUrlTemplateReadyForParallelTracking(trackingUrlTemplate)){
      foundSomething = true;
      var levels = "";
      for(var level in trackingUrlTemplates[trackingUrlTemplate]){
        if(levels == "") levels = level;
        else levels += ", " + level;
      }
        
      Logger.log("-- Potentially problematic tracking url template in account '" + currentAccountName + "': " + trackingUrlTemplate + " (found on the following levels: " + levels + ")");
    }
  }
  if(!foundSomething){
    Logger.log("+ No problems found in account '" + currentAccountName+ "'");
  }

  return JSON.stringify({ "params" :  JSON.parse(params) });
}

function isTrackingUrlTemplateReadyForParallelTracking(trackingUrlTemplate){
  if(trackingUrlTemplate.match(/^\{(lpurl|unescapedlpurl)\}/)) return true;
  return false;
}

function checkForTrackingUrlTemplates(trackingUrlTemplates, level){
  var field = "TrackingUrlTemplate";
  var awql;
  switch(level){
    case "campaigns":
      awql = "SELECT " + field + " FROM CAMPAIGN_PERFORMANCE_REPORT WHERE CampaignStatus = 'ENABLED' AND " + field + " != '{lpurl}' AND " + field + " != ''";
      break;
    case "adgroups":
      awql = "SELECT " + field + " FROM ADGROUP_PERFORMANCE_REPORT WHERE CampaignStatus = 'ENABLED' AND AdGroupStatus = 'ENABLED' AND " + field + " != '{lpurl}' AND " + field + " != ''";
      break;
    case "ads":
      field = "CreativeTrackingUrlTemplate";
      awql = "SELECT " + field + " FROM AD_PERFORMANCE_REPORT WHERE CampaignStatus = 'ENABLED' AND AdGroupStatus = 'ENABLED' AND Status = 'ENABLED' AND " + field + " != '{lpurl}' AND " + field + " != ''";
      break;
    case "criteria":
      awql = "SELECT " + field + ", CriteriaType FROM CRITERIA_PERFORMANCE_REPORT WHERE CampaignStatus = 'ENABLED' AND AdGroupStatus = 'ENABLED' AND Status = 'ENABLED' AND " + field + " != '{lpurl}' AND " + field + " != ''";
      break;
  }
  
  var reportRows = AdWordsApp.report(awql).rows();
  
  // Translate level name into something easier to understand.
  var levelTranslations = {
	'campaigns' : 'Campaign',
	'adgroups' : 'AdGroup',
	'ads' : 'Ad'
  };  
  if(levelTranslations.hasOwnProperty(level)) var translatedLevel = levelTranslations[level];
  else var translatedLevel = level;

  while(reportRows.hasNext()){
    var row = reportRows.next();
    if(row[field] == '{lpurl}' || row[field] == '') continue;
    
    if(level == "criteria"){
      translatedLevel = row['CriteriaType'];
    }
    
    if(!trackingUrlTemplates.hasOwnProperty(row[field])) trackingUrlTemplates[row[field]] = {};
    if(!trackingUrlTemplates.hasOwnProperty(row[field][translatedLevel])) trackingUrlTemplates[row[field]][translatedLevel] = 0;
    trackingUrlTemplates[row[field]][translatedLevel]++;
  }
  return trackingUrlTemplates;
}
  
function allDone(results){
  var params;
  for (var i = 0; i < results.length; i++) { if(results[i].getStatus() == "OK"){ params = JSON.parse(results[i].getReturnValue())['params']; break; } } if(!params){ Logger.log("No accounts have been processed - aborting..."); return; } if(params['sequentialIds'].length > 0){
    Logger.log("There are still " + params['sequentialIds'].length + " accounts to be processed sequentially.");
    
    var accountIterator = MccApp.accounts().withIds(params['sequentialIds']).get();
    while(accountIterator.hasNext()){
      var account = accountIterator.next();
      MccApp.select(account);
      results = checkParallelTrackingStatus(JSON.stringify(params));
      // Logger.log(account.getName() + ": erfolgreich verarbeitet");
    }    
  }
  Logger.log("Processing complete.");
}
[collapse]

Just like the last scripts I’ve shared, this one can be run without configuration. If it is run in an MCC account, it will check all associated accounts, but it can be used in a single account as well. There are no separate versions; the script detects whether it’s run in an MCC or not.

The script can process up to 50 accounts in parallel. If there are more accounts in an MCC, it will try to process the rest sequentially. I used this concept from Dawson Reid, who wrote about it here.

If there are too many accounts to process before time runs out, you can label the accounts you want to process and specify the accountLabelName at the beginning of the script.

Conclusion

Well, that’s it. Let me know if it’s helpful.

About Martin Roettgerding
Martin Roettgerding is the head of SEM at SEO/SEM agency Bloofusion Germany. On Twitter he goes by the name @bloomarty, and you can find him regularly on #ppcchat.