-
-
Save siliconvallaeys/a579409e0f94d8269744d6460b9cb466 to your computer and use it in GitHub Desktop.
function main() { | |
/****************************************** | |
* PMax Search Terms Report | |
* @version: 1.0 | |
* @authors: Frederick Vallaeys (Optmyzr) | |
* ------------------------------- | |
* Install this script in your Google Ads account (not an MCC account) | |
* to generate a spreadsheet containing the search terms in your Performance Max campaigns. | |
* The spreadsheet also includes data about category labels (groupings of search terms). | |
* Metrics include conversion value, conversions, clicks, and impressions | |
* -------------------------------- | |
* v2 (Mar 14, 2024): adds a new column that says if the search terms exists as a keyword in a search campaign. adds better error handling. | |
* -------------------------------- | |
* For more PPC tools and scripts, visit www.optmyzr.com. | |
******************************************/ | |
var minImp = 10; // Limit the output to only items with at least this many impressions | |
var spreadsheetUrl = ""; // leave blank to generate a new spreadsheet or add your own URL to overwrite the data in an existing spreadsheet | |
var reportLastNDays = 30; // The number of days to include in the report | |
var EMAILADDRESS = ""; // enter your email address | |
// Don't edit below this line unless you know how to write scripts | |
//----------------------------------------------------------------- | |
let pmaxOnly = 1; | |
let allCategoryLabels = [ | |
[ | |
"Campaign Name", | |
"Category Label", | |
"Conv Val", | |
"Conv", | |
"Clicks", | |
"Imp", | |
], | |
]; | |
let allSearchTerms = [ | |
[ | |
"Campaign Name", | |
"Category Label", | |
"Subcat", | |
"Search Term", | |
"Conv Val", | |
"Conv", | |
"Clicks", | |
"Imp", | |
"Exists as Keyword?", | |
], | |
]; | |
let keywordsList = getAllSearchCampaignKeywords(); | |
var dateRange = getDateRange(reportLastNDays); | |
try { | |
let baseQuery = ` | |
SELECT | |
campaign.id, | |
campaign.name, | |
metrics.clicks, | |
metrics.impressions, | |
metrics.conversions, | |
metrics.conversions_value | |
FROM campaign | |
WHERE campaign.status != 'REMOVED' | |
AND metrics.impressions > 0 | |
AND segments.date BETWEEN ${dateRange} | |
AND metrics.impressions >= ${minImp} | |
`; | |
// Modify the SQL query if pmaxOnly is true | |
if (pmaxOnly) { | |
baseQuery += | |
" AND campaign.advertising_channel_type = 'PERFORMANCE_MAX' "; | |
} | |
baseQuery += "ORDER BY metrics.conversions DESC"; | |
let campaignIdsQuery = AdsApp.report(baseQuery); | |
let rows = campaignIdsQuery.rows(); | |
while (rows.hasNext()) { | |
let campaignRow = rows.next(); | |
let campaignId = campaignRow["campaign.id"]; | |
Logger.log( | |
campaignRow["campaign.id"] + " " + campaignRow["campaign.name"] | |
); | |
// Search Labels Report | |
let categoryLabelsQuery = ` | |
SELECT | |
campaign_search_term_insight.category_label, | |
campaign_search_term_insight.id, | |
metrics.clicks, | |
metrics.impressions, | |
metrics.conversions, | |
metrics.conversions_value | |
FROM | |
campaign_search_term_insight | |
WHERE | |
segments.date BETWEEN ${dateRange} | |
AND campaign_search_term_insight.campaign_id = '${campaignId}' | |
AND metrics.impressions >= ${minImp} | |
ORDER BY | |
metrics.conversions DESC | |
`; | |
let categoryLabelsQueryResult = AdsApp.report(categoryLabelsQuery); | |
let categoryLabelsResults = categoryLabelsQueryResult.rows(); | |
while (categoryLabelsResults.hasNext()) { | |
let categoryLabelsRow = categoryLabelsResults.next(); | |
let categoryLabelId = | |
categoryLabelsRow["campaign_search_term_insight.id"]; | |
//Logger.log(categoryLabelId + ". " + categoryLabelsRow['campaign_search_term_insight.category_label'] + " " + categoryLabelsRow['metrics.impressions']); | |
allCategoryLabels.push([ | |
campaignRow["campaign.name"], | |
categoryLabelsRow[ | |
"campaign_search_term_insight.category_label" | |
], | |
categoryLabelsRow["metrics.conversions_value"].toFixed(2), | |
categoryLabelsRow["metrics.conversions"].toFixed(1), | |
categoryLabelsRow["metrics.clicks"], | |
categoryLabelsRow["metrics.impressions"], | |
]); | |
// Search Terms Report | |
let searchTermsQuery = ` | |
SELECT | |
metrics.clicks, | |
metrics.impressions, | |
metrics.conversions, | |
metrics.conversions_value, | |
segments.search_term, | |
segments.search_subcategory | |
FROM | |
campaign_search_term_insight | |
WHERE | |
segments.date BETWEEN ${dateRange} | |
AND campaign_search_term_insight.campaign_id = '${campaignId}' | |
AND campaign_search_term_insight.id = '${categoryLabelId}' | |
`; | |
let searchTermsQueryResult = AdsApp.report(searchTermsQuery); | |
let searchTermsResults = searchTermsQueryResult.rows(); | |
// Inside the loop where search terms are being processed | |
while (searchTermsResults.hasNext()) { | |
let searchTermsRow = searchTermsResults.next(); | |
if (searchTermsRow["metrics.impressions"] >= minImp) { | |
let searchTermText = searchTermsRow[ | |
"segments.search_term" | |
].toLowerCase(); // Normalize search term text for comparison | |
let existsAsKeyword = keywordsList[searchTermText] | |
? "Yes" | |
: "No"; | |
allSearchTerms.push([ | |
campaignRow["campaign.name"], | |
categoryLabelsRow[ | |
"campaign_search_term_insight.category_label" | |
], | |
searchTermsRow["segments.search_subcategory"], | |
searchTermsRow["segments.search_term"], | |
searchTermsRow["metrics.conversions_value"].toFixed( | |
2 | |
), | |
searchTermsRow["metrics.conversions"].toFixed(1), | |
searchTermsRow["metrics.clicks"], | |
searchTermsRow["metrics.impressions"], | |
existsAsKeyword, // New column indicating if the search term exists as a keyword | |
]); | |
} | |
} | |
} | |
} | |
if (!spreadsheetUrl) { | |
var ss = SpreadsheetApp.create("PMax Search Terms", 10000, 20); | |
var spreadsheetUrl = ss.getUrl(); | |
} else { | |
var ss = SpreadsheetApp.openByUrl(spreadsheetUrl); | |
} | |
let categoriesSheet = ss.getSheetByName("categories") | |
? ss.getSheetByName("categories").clear() | |
: ss.insertSheet("categories"); | |
if (allCategoryLabels.length > 1) { | |
// Check if there's more than just the header row | |
categoriesSheet | |
.getRange( | |
1, | |
1, | |
allCategoryLabels.length, | |
allCategoryLabels[0].length | |
) | |
.setValues(allCategoryLabels); | |
} | |
let termsSheet = ss.getSheetByName("terms") | |
? ss.getSheetByName("terms").clear() | |
: ss.insertSheet("terms"); | |
if (allSearchTerms.length > 1) { | |
// Check if there's more than just the header row | |
termsSheet | |
.getRange(1, 1, allSearchTerms.length, allSearchTerms[0].length) | |
.setValues(allSearchTerms); | |
} | |
var subject = "PMax Search Terms Report Ready"; | |
var body = | |
"The PMax Search Terms Report has been generated and is available at: " + | |
spreadsheetUrl + | |
"\n\nReport covers the last " + | |
reportLastNDays + | |
" days." + | |
"\n\nThis is an automated email sent by Google Ads Script."; | |
} catch (e) { | |
Logger.log("Error: " + e.message); | |
var subject = "PMax Search Terms Report Failed"; | |
var body = | |
"The PMax Search Terms Report encountered an error: " + | |
e + | |
"\n\nThis is an automated email sent by Google Ads Script."; | |
} | |
Logger.log("spreadsheet: " + spreadsheetUrl); | |
// Send the email | |
var recipientEmail = EMAILADDRESS; | |
MailApp.sendEmail(recipientEmail, subject, body); | |
} | |
// function to get date range | |
function getDateRange(numDays) { | |
const endDate = new Date(); | |
const startDate = new Date(); | |
startDate.setDate(endDate.getDate() - numDays); | |
const format = (date) => | |
Utilities.formatDate( | |
date, | |
AdsApp.currentAccount().getTimeZone(), | |
"yyyyMMdd" | |
); | |
return `${format(startDate)} AND ${format(endDate)}`; | |
} | |
function getAllSearchCampaignKeywords() { | |
let keywordsList = {}; | |
let keywordIterator = AdsApp.keywords() | |
.withCondition("Status = ENABLED") | |
.withCondition("CampaignStatus = ENABLED") | |
.withCondition("AdGroupStatus = ENABLED") | |
.get(); | |
while (keywordIterator.hasNext()) { | |
let keyword = keywordIterator.next(); | |
let keywordText = keyword.getText().toLowerCase(); // Normalize keyword text for comparison | |
keywordsList[keywordText] = true; // Use a map for efficient lookup | |
} | |
return keywordsList; | |
} |
I love this script. Thanks so much Frederick Vallaeys.
I've tried it on three accounts. On the first account, it works great. On the other two accounts I get the same errors the others have noted above.
I do not believe this is the script's fault, because when I go to download (export) the report manually using the insights tab in Google Ads, Google refuses to process that as well. I'm going to open a case with support about the account failing but am not optimistic that they will be helpful.
Thank you so much for that script! I will share it at my channel with direct link to Github and author mention.
Thanks for the amazing script! It gives great insight in the campaigns. We have a question: would it be possible to add a colum in the Google Sheets to show which keywords have been excluded already?
Hi, I set up the script a few weeks ago and it worked until this latest update. I received this notification "PMax search terms report failed". Do you know what's happening? Could you help me?
Thanks for your help.