Google Ads Script – Search Partner Alerts

Here’s a true story.

Two months ago we decided to add another conversion action to one of my client’s accounts.
You know the drill; add some more conversion actions (like form fills, newsletter signups, etc) to your account so you can start optimizing for them.

What we didn’t know was that Google sneaked in a new default setting aaaallll the way down the page when you create a new conversion:

It’s really easy to miss this new “feature” and the default setting is ‘ON’!
This basically means that if you do not opt out, all your campaigns using pure Manual Bidding will switch to Enhanced CPC.
Yep…really…

So this is what happened to my client’s account.
We set up the new conversion. Glimpsed over this wonderful new option. Clicked ‘Create and Continue’. And there it was:

CPC’s skyrocketed all over the place.

Almost all of our campaigns in that specific account are using Manual Bidding (supported by bidding scripts).
Switching to Enhanced CPC pretty much made our bids useless.
Where did big G get the idea that people who are still running manual bidding would want to secretly be moved into Enhanced CPC ?!?

Maneuvers like this make me wanna switch jobs. And I love my job!

Sigh.

All nice and well this rambling about Enhanced CPC, but what the F does this have to do with Search Partners (see title of post) you might ask.

Here’s the thing:
I discovered to my absolute horror that Search Partners are running on something like Enhanced CPC by default.
It’s called “Smart pricing”. And there is no way to opt-out!

So, even if your campaigns are running on pure Manual Bidding mode, when Search Partners are enabled in your campaign (default setting), Google will take the liberty to adjust your bids above your Max CPC bid for keywords in your campaign.
Here’s a link explaining how “Smart pricing has historically powered manual bids for search partners.” and “Starting in October 2018, Smart Bidding may be used for search partner sites if you have conversion tracking in place. ” :
https://support.google.com/google-ads/answer/9145501?hl=en , and
here’s a link to Google Support with their explanation on Smart Pricing: https://support.google.com/google-ads/answer/2604607?hl=en

That shit pisses me off, so I decided to write some scripts:
1. to alert me on high Avg CPCs from clicks in the search partner network
2. to compare the ROAS performance of the Search Partners versus the Google Network

The first script reports many examples of search terms in the search network that show Avg CPC’s  >200% over my Max CPC bid. I use this report for negative keyword management.

The second script I am sharing today.

This script compares the performance of the Search Partners to the Google Search Network
In case of a significant difference in ROAS performance difference the issue is logged in a sheet and an alert is sent via email.
That way you know when Search Partners are hurting your performance due to this Smart pricing thing, and you can opt out of the Search Partner network for these campaigns.

The output will look something like this:

Search Partner ROAS
Search Partner ROAS

The Diff columns report differences relative to clicks from Google, ie:
(Avg CPC from search partners – Avg CPC from Google) / Avg CPC Google

 

➥  ACTION: Schedule this script to run monthly.

(Don’t worry if you have never run a script before. You do not need any coding skills. It is as simple as copy-paste.)
INSTRUCTIONS:

  1. See the script code below. Install the script in your account.
    Don’t worry if you have never done this before. You do not need any coding skills. It is as simple as copy-paste. Simply follow these instructions on how to set up and schedule Google Ads scripts.
  2. Create a new Google Sheet
    (tip for Chrome users: simply type ‘sheets.new’ in the address bar)
  3. Add the complete URL of the spreadsheet to the script (line 17)
  4. Add your email address to the script (line 18)
  5. Preview
  6. Schedule to run monthly.

 


/**
* This script compares the performance of the Search Parntners to the Google Search Network
* In case performance difference is greater than the set threshold,
* the issue is logged in a sheet and an alert is send via email.
*
* V1.2: added support for 'LAST_YEAR' period
* V1.1: added support for different periods
*
* @author: Nils Rooijmans
*
* For questions, a high performance MCC version, or a version that compares ROAS instead of CPA ->
* contact nils@nilsrooijmans.com 
*/



/*** ADD YOUR CONFIGURATION HERE ***/
 
var SPREADSHEET_URL = "";  //insert a new blank spreadsheet url between quotes
var EMAIL_ADDRESS = ""; //insert your email between quotes
 
var CLICK_THRESHOLD = 50; // the minimum numer of clicks per campaign on the search partner network, ignore all campaigns with lower number
var PERIOD = "LAST_365_DAYS"; // period of analyses, can be either "LAST_30_DAYS", "LAST_60_DAYS", "LAST_90_DAYS", "LAST_365_DAYS" or "THIS_YEAR"
var CPA_DIFF_THRESHOLD = 0.2; // send alerts for campaigns with greater difference (use value of 1 for 100%, 0.1 for 10% etc)

/*** EMAIL CONFIGURATION SETTINGS ***/
var SEND_EMAIL = true;

var EMAIL_SUBJECT = "Search Partners Performance Alert";
var EMAIL_BODY =
    "\n\n"+
    "***\n"+
    "\n"+
    "This script compares the performance of the Search Partners to the Google Search Network:\n"+
    "\n"+
    "For all enabled campaigns that have over "+CLICK_THRESHOLD+" clicks on the Search Partner network during "+PERIOD+" \n"+
    "		check if Search Partners CPA is over "+CPA_DIFF_THRESHOLD*100+"% worse compared to Google Network CPA \n"+
    "		if so, alerts are logged in Google Sheet: "+SPREADSHEET_URL+" \n"+
    "\n"+
    "If there is an alert an email is sent to:\n"+ EMAIL_ADDRESS +"\n\n"+ 
    "---\n"+
    "This email is generated by a copy of the free Google Ads Script - Search Partner CPA Performance Alert, (C) Nils Rooijmans \n" +
    "For more FREE Google Ads Scripts to improve your results and make your working day feel like a breeze, visit https://nilsrooijmans.com \n" + 
    "---\n"; 


//var IGNORE_CAMPAIGNS_LABEL = "Ignore Search Partner Performance Alerts"; 

var totalNrOfIssues = 0;
var issues =[];


function main() {
  // please contact nils@watercoolertopics.com for questions and the script	

  //prepare the Spreadsheet
  var ss = SpreadsheetApp.openByUrl(SPREADSHEET_URL);
  var sheet = ss.getActiveSheet();
  sheet.clear(); //remove earlies alerts
  var header = [
    "Account Name", 
    "Campaign Name", 
    "Clicks GoogleNetwork",
    "Clicks SearchPartners",
    "Diff",
    "Avg CPC GoogleNetwork",
    "Avg CPC SearchPartners",
    "Diff",
    "Cost GoogleNetwork",
    "Cost SearchPartners",
    "Diff",
    "Conversions GoogleNetwork",
    "Conversions SearchPartners",
    "Diff",
    "Cost/Conv GoogleNetwork",
    "Cost/Conv SearchPartners",
    "Diff"
  ];
  sheet.appendRow(header);
 
  var accountName = AdsApp.currentAccount().getName();

  var startDate = getStartDate(PERIOD);
  var endDate = getEndDate(PERIOD);
  var period = startDate+","+endDate;
  Logger.log("period: "+period);
  
  // get the campaigns that have enough clicks on the search partner network and store stats in array of stats objects
  var report = AdsApp.report(
      'SELECT CampaignName, CampaignId, Clicks, Cost, AverageCpc, Conversions ' +
      'FROM CAMPAIGN_PERFORMANCE_REPORT ' +
    	'WHERE CampaignStatus = "ENABLED" ' +
      'AND AdNetworkType2 = "SEARCH_PARTNERS" ' +
    	'AND Clicks > '+CLICK_THRESHOLD+ 
      ' DURING '+period);
   
  var rows = report.rows();

  var campaignSearchPartnerStatsObjArray = [];
  var campaignGoogleNetworkStatsObjArray = [];
  var campaignIds = [];

  while (rows.hasNext()) {
    var row = rows.next();
    var campaignSearchPartnerStatsObj = {};
    
    campaignSearchPartnerStatsObj.campaignName = row['CampaignName'];
    campaignSearchPartnerStatsObj.campaignId = row['CampaignId'];
    campaignSearchPartnerStatsObj.clicks = row['Clicks'];
    campaignSearchPartnerStatsObj.cost = numericalize(row['Cost']);
    campaignSearchPartnerStatsObj.avgcpc = numericalize(row['AverageCpc']); 
    campaignSearchPartnerStatsObj.conversions = numericalize(row['Conversions']);
    campaignSearchPartnerStatsObj.costPerConversion = (campaignSearchPartnerStatsObj.cost/campaignSearchPartnerStatsObj.conversions).toFixed(2);

    campaignIds.push(campaignSearchPartnerStatsObj.campaignId);
    campaignSearchPartnerStatsObjArray.push(campaignSearchPartnerStatsObj);
  }

  Logger.log("Number of enabled campaigns with at least "+CLICK_THRESHOLD+" clicks on the Search Partner Network: "+campaignIds.length);

  if (campaignIds.length > 0) {  
    // store Google Network stats for the list of campaignids in array of stats objects
    var report = AdsApp.report(
        'SELECT CampaignName, CampaignId, Clicks, Cost, AverageCpc, Conversions ' +
        'FROM CAMPAIGN_PERFORMANCE_REPORT ' +
        'WHERE CampaignId IN ['+campaignIds.join(",")+']' +
        ' AND AdNetworkType2 = "SEARCH" ' +
        ' DURING '+period);

    var rows = report.rows();    

    while (rows.hasNext()) {
      var row = rows.next();
      campaignGoogleNetworkStatsObj = {};
      campaignGoogleNetworkStatsObj.campaignName = row['CampaignName'];
      campaignGoogleNetworkStatsObj.campaignId = row['CampaignId'];
      campaignGoogleNetworkStatsObj.clicks = row['Clicks'];
      campaignGoogleNetworkStatsObj.cost = numericalize(row['Cost']);
      campaignGoogleNetworkStatsObj.avgcpc = numericalize(row['AverageCpc']); 
      campaignGoogleNetworkStatsObj.conversions = numericalize(row['Conversions']);
      campaignGoogleNetworkStatsObj.costPerConversion = (campaignGoogleNetworkStatsObj.cost/campaignGoogleNetworkStatsObj.conversions).toFixed(2);

      campaignGoogleNetworkStatsObjArray.push(campaignGoogleNetworkStatsObj);
    }

    // add issues to array of issues
    for (var i=0; i<campaignIds.length;i++) {
      var campaignId = campaignIds[i];
      var campaignGoogleNetworkStatsObj = getObject(campaignGoogleNetworkStatsObjArray, campaignId);
      var campaignSearchPartnerStatsObj = getObject(campaignSearchPartnerStatsObjArray, campaignId);

      var absCpaDiff = Math.abs( (campaignSearchPartnerStatsObj.costPerConversion-campaignGoogleNetworkStatsObj.costPerConversion)/campaignGoogleNetworkStatsObj.costPerConversion ); 
      if ( absCpaDiff > CPA_DIFF_THRESHOLD) {
        var issue = [
          accountName, 
          campaignGoogleNetworkStatsObj.campaignName, 
          campaignGoogleNetworkStatsObj.clicks,
          campaignSearchPartnerStatsObj.clicks,
          ((campaignSearchPartnerStatsObj.clicks-campaignGoogleNetworkStatsObj.clicks)/campaignGoogleNetworkStatsObj.clicks).toFixed(2),
          campaignGoogleNetworkStatsObj.avgcpc,
          campaignSearchPartnerStatsObj.avgcpc,
          ((campaignSearchPartnerStatsObj.avgcpc-campaignGoogleNetworkStatsObj.avgcpc)/campaignGoogleNetworkStatsObj.avgcpc).toFixed(2),
          campaignGoogleNetworkStatsObj.cost,
          campaignSearchPartnerStatsObj.cost,
          ((campaignSearchPartnerStatsObj.cost-campaignGoogleNetworkStatsObj.cost)/campaignGoogleNetworkStatsObj.cost).toFixed(2),
          campaignGoogleNetworkStatsObj.conversions,
          campaignSearchPartnerStatsObj.conversions,
          ((campaignSearchPartnerStatsObj.conversions-campaignGoogleNetworkStatsObj.conversions)/campaignGoogleNetworkStatsObj.conversions).toFixed(2),
          campaignGoogleNetworkStatsObj.costPerConversion,
          campaignSearchPartnerStatsObj.costPerConversion,
          ((campaignSearchPartnerStatsObj.costPerConversion-campaignGoogleNetworkStatsObj.costPerConversion)/campaignGoogleNetworkStatsObj.costPerConversion).toFixed(2)
        ];
        issues.push(issue);
        totalNrOfIssues++;
      }  
    }
  }
  
  if (totalNrOfIssues > 0) { // there is at least one issue

    Logger.log("Total NR of Issues: "+totalNrOfIssues);
    
    var lastRow = sheet.getLastRow();

    // write issues to sheet
    var range = sheet.getRange(lastRow+1, 1, totalNrOfIssues, header.length);
    range.setValues(issues);
    range.sort({column: 17, ascending: true}); // sort by difference in Cost/Conv 
    //sheet.sort(13, true); 

    // send the email
    if(SEND_EMAIL) {
      var emailSubject = "[Gads Script] "+AdsApp.currentAccount().getName()+" | "+EMAIL_SUBJECT;     
      var emailBody = "\nNumber of campaigns with significant difference in perfomance of Search Partners versus Google Network: "+totalNrOfIssues+EMAIL_BODY;
      MailApp.sendEmail(EMAIL_ADDRESS, emailSubject, emailBody) ;
    } 
  }

  Logger.log("End of execution: " + Utilities.formatDate(new Date(), "GMT+1", "yyyy-MM-dd'T'HH:mm:ss'Z'"));   
  
}


function getObject(objArray, campaignId) {
  for (var i=0;i<objArray.length;i++){
    if (objArray[i].campaignId == campaignId) {
      return objArray[i];
    }
  }
  return null;
}

function numericalize(string){
  return parseFloat(string.toString().replace(/\,/g, ''));
} 


function getTodaysDate() {
  return new Date();
}

function getStartDate(period) {
  
  var startDate;
  var today = getTodaysDate();
  
  switch(period) {

    case "LAST_YEAR" : 
      startDate = (today.getFullYear()-1)+"0101";
      break;
      
    case "THIS_YEAR" : 
      startDate = today.getFullYear()+"0101";
      break;
      
    case "LAST_30_DAYS" :
      startDate =  dateToISO112String(new Date(Date.now() - 30*864e5)); // 864e5 == 86400000 == 24*60*60*1000 )
      break;

    case "LAST_60_DAYS" :
      startDate =  dateToISO112String(new Date(Date.now() - 60*864e5)); // 864e5 == 86400000 == 24*60*60*1000 )
      break;

    case "LAST_90_DAYS" :
      startDate =  dateToISO112String(new Date(Date.now() - 90*864e5)); // 864e5 == 86400000 == 24*60*60*1000 )
      break;

    case "LAST_365_DAYS" :
      startDate =  dateToISO112String(new Date(Date.now() - 365*864e5)); // 864e5 == 86400000 == 24*60*60*1000 )
      break;
      
    default:
      startDate ="20200101";
  }
  
  return startDate;
}


function getEndDate(period) {

  var endDate;
  
  var today = getTodaysDate();
  var yesterday = new Date(Date.now() - 864e5); // 864e5 == 86400000 == 24*60*60*1000 
 
  switch(period) {
      
    case "LAST_YEAR" : 
      endDate = (today.getFullYear()-1)+"1231";
      break;
      
    case "LAST_MONTH" :
      endDate = "lastDayLastMonth" //TODO
      break;
      
    default:
      endDate = dateToISO112String(yesterday);
  }
  
  return endDate;
}


// convert date to Format 112 ISO string in the form YYYYMMDD
function dateToISO112String(date) { 
   
  var year = date.getUTCFullYear();
  var month = date.getUTCMonth()+1;
  if (month < 10) {
    month = "0"+month;
  }
    
  var day = date.getUTCDate();
  if (day < 10) {
    day = "0"+day;
  }  
  var ISO112String = year.toString()+month.toString()+day.toString(); // YYYMMDD fornat
  
  return ISO112String;
}

 

Join thousands of PPC geeks who already have access:

If the button above isn’t working for you, you can sign up here to get access.