Performance Max is a black box.
Extremely little data about its performance is shared with us via the Google Ads interface.
Yet, we all crave this data.
In fact, if I had to choose between a Google Sheet with yesterday’s PPC stats and my morning coffee…
my Harar French press would win.
BUT, the thing is:
We are PPC Professionals. We want to optimize based on performance data.
Reduce wasted ad spend in areas with low click value.
Increase clicks from targets that show great results.
Yet Google is making it harder to do our jobs each and every day by hiding valuable data in the interface.
Luckily for us, the Google Ads API allows us to pull some more data from the Google Ads platform.
And we can use a script to do some nice things with this data!
Imagine having a Google sheet at your fingertips that shows what search categories are trending in your Performance Max campaigns.
Easily see how the PMax algorithm is changing, and matching your assets to new user queries.
Quickly see what types of searches these newly inserted products are being matched to.
Promptly respond to downward trends in search terms that showed great conversion value in the past.
And there’s more.
You can use the insights to:
– add trending search categories as new (broad) keywords to your standard search campaigns
– tailor your creatives based on trends
– add popular search categories to your landing pages and Merchant Center feed descriptions to boost performance
I will take any additional insight I can get since Google gives so little on PMax.
That’s why I created this script.
Here’s a script to monitor how Performance Max is matching your assets to user queries, and what is changing in the search behavior of your audience.
The script creates a report in Google Sheets.
The report lists the search categories that show a significant increase or drop in impressions.
It compares last week’s data to the week before.
The output will look something like this:
Rising search categories present new opportunities for growth. Maybe even opportunities for completely new products/service offerings 🙂
Or, if PMax decided to match your assets to new irrelevant search terms you might want to change some assets and/or add negatives.
Declining search categories might require a reallocation of budget.
NB: If you like this script and run standard search campaigns, be sure to also check out my Google Ads Script – Trending Search Terms.
INSTRUCTIONS:
- 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. - Create a new Google Sheet
(tip for chrome users: simply type ‘sheets.new’ in the address bar) - Add the complete URL of the spreadsheet to the script (line 18)
- Add your email address to the script (line 19)
- Add the name of your Google Ads account to the subject of emails (line 28)
- Authorize and Preview
- Schedule to run weekly (I prefer Mondays at 6AM)
Subscribe to my mailing list to receive more scripts and updates on how to start learning to write your own Google Ads Scripts.
/** * * Report PMax Trending Search Categories * * Creates a Google Sheets report containing trending search categories in your Performance Max campaigns * Compares data from last 7 days to the 7 days before that period * * @author: Nils Rooijmans * * Version 1.0 * * contact nils@nilsrooijmans.com for questions and a High Performance MCC version of the script */ /*** [REQUIRED] ADD YOUR SETTINGS HERE ***/ var SPREADSHEET_URL = ""; // insert a new blank spreadsheet url between the quotes var EMAIL_ADDRESSES = ""; // insert email addresses of people that want to get the report between the quotes, seperate multipe email addresses by a comma /*** [OPTIONAL] YOU MIGHT WANT TO CHANGE SOME CONFIGURATIONS HERE ***/ var IMPRESSIONS_MIN_ABS_DIFFERENCE = 100; // ignore search categories with absolute difference that is smaller then the value you set here var IMPRESSIONS_MIN_REL_DIFFERENCE = 0.5; // ignore search categories with relative difference that is smaller then the value you set here var SEND_EMAIL = true; // set to false if you do not want to send alert email var EMAIL_SUBJECT = "[GAds Script] - Performance Max - Trending Search Categories"; // subject of emails, you might want to include your account name here /*** DO NOT CHANGE ANYTHING BELOW THIS LINE ***/ function main () { var campaignIds = getCampaignIds(); if (campaignIds.length == 0) { console.log("The account currently has zero Performance Max campaigns that are enabled. We're done here."); return; } console.log("The account currently has %s Performance Max campaigns that are enabled", campaignIds.length ); var trendingSearchCategories = processCampaigns(campaignIds); if (trendingSearchCategories.length == 0) { console.log("The account has zero Performance Max campaigns that show trending search categories. We're done here."); return; } var nrOftrendingSearchCategories = trendingSearchCategories.length; console.log("The account has %s trending search categories in Performance Max campaigns that are enabled", nrOftrendingSearchCategories); var sheet = prepareOutputSheet('clear_sheet'); addOutputToSheet(trendingSearchCategories, sheet); sendEmail(nrOftrendingSearchCategories); console.log("We're done."); } function processCampaigns(campaignIds) { var trendingSearchCategories = []; for (var i=0; i<campaignIds.length; i++) { var campaignId = campaignIds[i]; var campaignTrendingSearchCategories = getCampaignTrendingSearchCategories(campaignId); if (campaignTrendingSearchCategories.length == 0) { console.log("Campaign id '%s' has zero trending search categories.", campaignId); continue; } console.log("Campaign id '%s' has %s trending search categories.", campaignId, campaignTrendingSearchCategories.length); for (var j=0; j<campaignTrendingSearchCategories.length; j++) { trendingSearchCategories.push(campaignTrendingSearchCategories[j]); } } return trendingSearchCategories; } function getCampaignTrendingSearchCategories(campaignId) { var searchCategorieStatsLast7Days = {}; var searchCategorieStatsPeriodBefore = {}; searchCategorieStatsLast7Days = getSearchCategorieStats(campaignId, "last_7_days"); searchCategorieStatsPeriodBefore = getSearchCategorieStats(campaignId, "period_before_last_7_days"); var trendingSearchCategories; trendingSearchCategories = getTrendingSearchCategories(searchCategorieStatsLast7Days,searchCategorieStatsPeriodBefore); var campaignName = getCampaignNameById(campaignId); var rows = []; for (var searchCategory in trendingSearchCategories) { var impressions_period1 = trendingSearchCategories[searchCategory].impressions_1; var impressions_period2 = trendingSearchCategories[searchCategory].impressions_2; var absoluteDiff = impressions_period1-impressions_period2; var relativeDiff = (impressions_period1-impressions_period2)/impressions_period2; rows.push([campaignName, searchCategory, impressions_period1, impressions_period2, absoluteDiff, relativeDiff]); } return rows; } function getTrendingSearchCategories(dataPeriod_1,dataPeriod_2) { var trendingSearchCategories = {}; for (var searchCategory in dataPeriod_1) { if (dataPeriod_1.hasOwnProperty(searchCategory)) { var impressions1 = dataPeriod_1[searchCategory]; if (dataPeriod_2.hasOwnProperty(searchCategory)) { var impressions2 = dataPeriod_2[searchCategory]; } else { var impressions2 = 0; } if (Math.abs(impressions1-impressions2) > IMPRESSIONS_MIN_ABS_DIFFERENCE && Math.abs((impressions1-impressions2)/impressions2) > IMPRESSIONS_MIN_REL_DIFFERENCE ) { var searchCategoryObject = {}; searchCategoryObject.impressions_1 = impressions1; searchCategoryObject.impressions_2 = impressions2; trendingSearchCategories[searchCategory] = searchCategoryObject; } } } // now add searchCategories that were not present in data from period_1 for (var searchCategory in dataPeriod_2) { if (dataPeriod_2.hasOwnProperty(searchCategory)) { var impressions2 = dataPeriod_2[searchCategory]; if (!dataPeriod_1.hasOwnProperty(searchCategory)) { // searchCategory not present in data from period_1 var impressions1 = 0; if (Math.abs(impressions1-impressions2) > IMPRESSIONS_MIN_ABS_DIFFERENCE && Math.abs((impressions1-impressions2)/impressions2) > IMPRESSIONS_MIN_REL_DIFFERENCE ) { var searchCategoryObject = {}; searchCategoryObject.impressions_1 = impressions1; searchCategoryObject.impressions_2 = impressions2; trendingSearchCategories[searchCategory] = searchCategoryObject; } } } } return trendingSearchCategories; } function getSearchCategorieStats(campaignId, period) { var searchCategoryObject = {}; var periodString; switch(period) { case "today" : periodString = " DURING TODAY"; break; case "yesterday" : periodString = " DURING YESTERDAY"; break; case "day_before_yesterday" : periodString = " BETWEEN '" + dates(2) + "' AND '" + dates(2) +"'"; break; case "last_7_days" : periodString = " BETWEEN '" + dates(7) + "' AND '" + dates(1) +"'"; break; case "period_before_last_7_days" : periodString = " BETWEEN '" + dates(14) + "' AND '" + dates(8) +"'"; break; case "last_7_days_last_year" : periodString = " BETWEEN '" + dates(372) + "' AND '" + dates(366) +"'"; break; default : Logger.log("### ERROR: Could not recognize the period"); } var gaqlQuery= "SELECT campaign.name, campaign_search_term_insight.campaign_id, campaign_search_term_insight.category_label, metrics.impressions FROM campaign_search_term_insight WHERE campaign_search_term_insight.campaign_id = "+campaignId+" AND segments.date "+periodString; //console.log("gaqlQuery: "+gaqlQuery); var results = AdsApp.search(gaqlQuery); while (results.hasNext()) { var result = results.next(); var searchCategory = result.campaignSearchTermInsight.categoryLabel; var impressions = result.metrics.impressions; if (searchCategoryObject.hasOwnProperty(searchCategory)) { searchCategoryObject[searchCategory] += impressions; } else { searchCategoryObject[searchCategory] = impressions; } } return searchCategoryObject; } // returns an array with campaign ids of all enabled performance max campaigns function getCampaignIds() { var campaignIds = []; var gaqlQuery = "SELECT campaign.id FROM campaign WHERE campaign.advertising_channel_type = 'PERFORMANCE_MAX' AND campaign.status = 'ENABLED'"; var results = AdsApp.search(gaqlQuery); while (results.hasNext()) { var result = results.next(); var campaignId = result.campaign.id; campaignIds.push(campaignId); } return campaignIds; } // return date x days before today function dates(x){ var MILLIS_PER_DAY = 1000 * 60 * 60 * 24; var now = new Date(); var date = new Date(now.getTime() - x * MILLIS_PER_DAY); var timeZone = AdsApp.currentAccount().getTimeZone(); var output = Utilities.formatDate(date, timeZone, 'yyyy-MM-dd'); return output; } function getCampaignNameById(campaignId) { var gaqlQuery = "SELECT campaign.name FROM campaign WHERE campaign.id = '"+campaignId+"'"; var results = AdsApp.search(gaqlQuery); if (results.hasNext()) { var result = results.next(); return result.campaign.name; } else { console.log("No campaign with id: "+campaignId); return; } } function prepareOutputSheet(param1) { var spreadsheet = SpreadsheetApp.openByUrl(SPREADSHEET_URL); if (!spreadsheet) { Logger.log("Cannot open new reporting spreadsheet") ; return ; } var sheet = spreadsheet.getActiveSheet(); if (!sheet) { Logger.log("Cannot open new reporting sheet") ; return ; } if(param1=='clear_sheet') { console.log("Clearing the output sheet"); sheet.clear(); sheet.clearConditionalFormatRules(); sheet.clearContents(); sheet.clearFormats(); var filter = sheet.getFilter(); if (filter) { filter.remove(); } } var numberOfRows=sheet.getLastRow() ; console.log("NR of rows in output sheet: "+numberOfRows); if (numberOfRows == 0) { // the sheet has no header addHeaderToOutputSheet(sheet); } return sheet; } function addHeaderToOutputSheet(sheet) { console.log("Adding header to the output sheet"); var header = [ "Campaign Name", "Search Category", "Impressions Last 7 Days", "Impressions 7 Days Before", "Diff", "Relative Diff (%)" ]; sheet.appendRow(header); var range=sheet.getRange(1,1,1,header[0].length); range.setFontWeight("bold"); sheet.setFrozenRows(1); } function addOutputToSheet(output, sheet) { if (!(output.length > 0)) return; // nothing to add to sheet // set width of columns sheet.setColumnWidth(1, 300); sheet.setColumnWidth(2, 300); sheet.setColumnWidth(3, 200); sheet.setColumnWidth(4, 200); // create conditional formatting rules to highlight big differences var rules = sheet.getConditionalFormatRules(); var diffRange = [sheet.getRange("F2:F"+ (2+output.length))]; var rule = SpreadsheetApp.newConditionalFormatRule().whenNumberGreaterThan(1).setFontColor("#32CD32").setRanges(diffRange).build(); rules.push(rule); var rule = SpreadsheetApp.newConditionalFormatRule().whenNumberLessThan(-0.5).setFontColor("#FF8C00").setRanges(diffRange).build(); rules.push(rule); sheet.setConditionalFormatRules(rules); var numberOfRows=sheet.getLastRow() ; sheet.insertRowsBefore(2, output.length); // add empty rows below header row var startRow = 2; var range=sheet.getRange(startRow, 1, output.length, output[0].length) ; range.setValues(output) ; range.sort([{column: 1, ascending: false}, {column: 5, ascending: false}]); sheet.getRange(2, 6, sheet.getLastRow()).setNumberFormat("0.0%"); // add filter var filterRange = sheet.getRange(1, 1, output.length+1, output[0].length); filterRange.createFilter(); console.log("Number of rows added to output sheet: "+output.length+"\n\n"); } function sendEmail(number) { var emailBody = "\nNumber of trending search categories in your PMax campaigns: " + number + "\n" + "See details: "+ SPREADSHEET_URL+ "\n---\n\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" + "This email is generated by a copy of the free Google Ads Script - Report PMax Trending Search Categories, (C) Nils Rooijmans \n" + "---\n"; if (SEND_EMAIL) { MailApp.sendEmail(EMAIL_ADDRESSES, EMAIL_SUBJECT, emailBody); Logger.log("Sending mail"); } }
Join thousands of PPC geeks who already have access: