Google Shopping Product Trends Report
Create multiple Account-level Google Shopping product reports based on Item ID and Campaign filters + choose your own (6 total) date ranges to compare.
Google Shopping Product Trends Report - New in Feb 2024!
Create multiple Account-level Google Shopping product reports based on Item ID and Campaign filters + choose your own (6 total) date ranges to compare.
What this script does
Product stats as an average across 6 different time periods.
As an average? E.g. if the lookback window (days) is 365, it's the total clicks across 365 days (to yesterday) divided by 365.
Recommended Schedule: Hourly if looking at today, otherwise daily.
To create a new report, just duplicate an existing report tab then add your settings.
How many reports can I add?
The only limit is the script runtime of 30 minutes.
Should the script timeout, either reduce the number of reports or add further Item ID or Campaign filters.
Don't forget you can install the same script multiple times, just update the Sheet URLs in each.
What if I have suggestions?
Please let me know! Hearing your pain points is the number one way we can make improvements.
Video Walkthrough
The Template
Make a copy of the template, copy your new Sheet's URL, and paste it atop the Script.
The Script
/**
* Google Shopping Trends Report
* @author Charles Bannister
* More scripts at https://shabba.io
* @version 1.1.0
**/
// Template: https://docs.google.com/spreadsheets/d/1oxPn_cd-2lm5nKUof7GMgDDgBrMjfm2ZQOB3kNmgkWU/
// Template: https://docs.google.com/spreadsheets/d/1oxPn_cd-2lm5nKUof7GMgDDgBrMjfm2ZQOB3kNmgkWU
// File > Make a copy or visit https://docs.google.com/spreadsheets/d/1oxPn_cd-2lm5nKUof7GMgDDgBrMjfm2ZQOB3kNmgkWU/copy
let INPUT_SHEET_URL = "YOUR_SPREADSHEET_URL_HERE";
// This setting will pull the Intent should campaigns
// be named with "High Intent", "Medium Intent" or "Low Intent"
const INCLUDE_INTENT = false;
const VERSION = '1.1.0';
const UPDATED_AT = 'April 2024'
//Set to true or false, without quotes
//If true, an email will be sent to support if there's an error
//then, if necessary, we'll look into the problem and get back to you
const SEND_ERROR_EMAIL_TO_SHABBA = false;
const SCRIPT_NAME = 'Barry';
const SHABBA_SCRIPT_ID = 10;
let LOCAL = false;
if (typeof AdsApp === 'undefined') {
LOCAL = true;
}
const NOTES_SHEET_NAME = 'Notes';
let REPORT_DIMENSIONS = [
'Item ID',
'Title',
'Campaigns',
];
if (typeof INCLUDE_INTENT !== 'undefined' && INCLUDE_INTENT) {
REPORT_DIMENSIONS.push('Intent');
}
const METRICS = {
'clicks': 'Clicks per day',
'impressions': 'Impressions per day',
'conversions': 'Conversions per day',
'conversionsValue': 'Conversion Value per day',
'cost': 'Cost per day',
'cos': 'COS%',
}
const LAST_UPDATED_RANGE_NAME = 'B1';
const START_ROW = 17;
const START_COLUMN = 1;
let SPREADSHEET;
if (!LOCAL) {
SPREADSHEET = SpreadsheetApp.openByUrl(INPUT_SHEET_URL);
SPREADSHEET.getSheetByName(NOTES_SHEET_NAME).getRange('B2').setValue(VERSION);
SPREADSHEET.getSheetByName(NOTES_SHEET_NAME).getRange('B4').setValue(UPDATED_AT);
}
const LOOKBACK_WINDOW_KEYS = ['date_1_days', 'date_2_days', 'date_3_days', 'date_4_days', 'date_5_days', 'date_6_days']
function main() {
try {
runTopLevelLogic();
} catch (error) {
sendErrorEmailToAdmin(error);
throw new Error(error.stack);
}
}
function runTopLevelLogic() {
const sheetNames = getSheetNames();
for (let sheetName of sheetNames) {
if (sheetName === NOTES_SHEET_NAME) {
continue;
}
runSheet(sheetName);
}
}
function runSheet(sheetName) {
const settings = new GetSettings(sheetName).getSettings();
console.log('settings', JSON.stringify(settings));
sheetSetup(sheetName);
let productData = {};
updateProductData(settings, sheetName, productData);
let sheetData = new GetSheetArray(productData, settings).getSheetArray();
writeSheetData(sheetData, sheetName);
finalSheetFormatting(sheetName, settings['sort_by_column_number']);
updateLastUpdatedTimestamp(sheetName);
console.log(`Finished updating sheet: ${sheetName}`);
}
function updateLastUpdatedTimestamp(sheetName) {
if (LOCAL) {
return;
}
const reportSheet = SPREADSHEET.getSheetByName(sheetName);
const lastUpdatedRange = reportSheet.getRange(LAST_UPDATED_RANGE_NAME);
const now = new Date();
lastUpdatedRange.setValue(now);
}
function updateProductData(settings, sheetName, productData) {
for (let setting in settings) {
if (!setting.includes('date_') || !setting.includes('_days')) {
continue;
}
const lookbackDays = settings[setting];
updateProductDataForWindow(settings, lookbackDays, sheetName, productData);
}
}
class GetSettings {
constructor(sheetName) {
this.sheetName = sheetName;
this.rangeByName = {
'item_id_contains': 'A7',
'item_id_not_contains': 'B7',
'product_title_contains': 'A10',
'product_title_not_contains': 'B10',
'campaign_contains': 'A13',
'campaign_not_contains': 'B13',
'date_1_days': 'D7',
'date_1_name': 'E7',
'date_2_days': 'D8',
'date_2_name': 'E8',
'date_3_days': 'D9',
'date_3_name': 'E9',
'date_4_days': 'D10',
'date_4_name': 'E10',
'date_5_days': 'D11',
'date_5_name': 'E11',
'date_6_days': 'D12',
'date_6_name': 'E12',
'sort_by_column_number': 'G7',
};
}
getSettings() {
console.log(`Getting settings for sheet: ${this.sheetName}`);
let settings = {};
if (LOCAL) {
//create dummy settings
for (let rangeName in this.rangeByName) {
settings[rangeName] = 'test';
}
//add some real dates for testing
settings['date_1_days'] = 365;
settings['date_1_name'] = 'Year';
settings['date_6_days'] = 0;
settings['date_6_name'] = 'Today';
return settings;
}
const sheet = SPREADSHEET.getSheetByName(this.sheetName);
for (let rangeName in this.rangeByName) {
const cellReference = this.rangeByName[rangeName];
settings[rangeName] = sheet.getRange(cellReference).getValue();
}
return settings;
}
}
class SheetFilter {
constructor(sheetName) {
this.sheetName = sheetName;
}
removeFilter() {
if (LOCAL) {
return;
}
const filterRange = this.getFilterRange();
if (!filterRange) {
return;
}
if (filterRange.getFilter()) {
filterRange.getFilter().remove();
}
}
createFilter() {
if (LOCAL) {
return;
}
const filterRange = this.getFilterRange();
if (!filterRange) {
return;
}
if (!filterRange.getFilter()) {
filterRange.createFilter();
}
}
getFilterRange() {
const reportSheet = SPREADSHEET.getSheetByName(this.sheetName);
const rangeValues = [START_ROW + 1, START_COLUMN, reportSheet.getLastRow(), reportSheet.getLastColumn()];
const filterRange = reportSheet.getRange(...rangeValues);
return filterRange;
}
}
function finalSheetFormatting(sheetName, sortByColumnNumber) {
if (LOCAL) {
return;
}
const reportSheet = SPREADSHEET.getSheetByName(sheetName);
new SheetFilter(sheetName).createFilter();
sortSheetValues(reportSheet, sortByColumnNumber);
}
function sortSheetValues(reportSheet, sortByColumnNumber,) {
if (LOCAL) {
return;
}
reportSheet.getRange(START_ROW + 2, START_COLUMN, getLastRow(reportSheet), reportSheet.getLastColumn()).sort({ column: sortByColumnNumber, ascending: false });
}
/**
* The sheet.getLastRow() method doesn't work as expected
* so I'm using this for now
* @param {object} sheet
* @returns {number}
*/
function getLastRow(sheet) {
const data = sheet.getDataRange().getValues();
let lastRow = START_ROW + 1;
for (let row of data) {
if (row[0] === '') {
break;
}
lastRow++;
}
return lastRow;
}
function updateProductDataForWindow(settings, lookbackDays, sheetName, productData) {
let rawWindowProductData = new GetProductData(settings, lookbackDays, sheetName).getData();
let windowProductData = new GetReportData(rawWindowProductData, lookbackDays).getData();
productData[String(lookbackDays)] = windowProductData;
}
function writeSheetData(sheetData, sheetName) {
if (LOCAL) {
return;
}
const reportSheet = SPREADSHEET.getSheetByName(sheetName);
reportSheet.getRange(START_ROW, START_COLUMN, sheetData.length, sheetData[0].length).setValues(sheetData);
}
function sheetSetup(sheetName) {
new SheetFilter(sheetName).removeFilter();
clearSheetData(sheetName);
}
function clearSheetData(sheetName) {
if (LOCAL) {
return;
}
const reportSheet = SPREADSHEET.getSheetByName(sheetName);
if (reportSheet.getLastRow() < START_ROW + 1) {
return;
}
reportSheet.getRange(START_ROW, START_COLUMN, reportSheet.getLastRow(), reportSheet.getLastColumn()).clear();
}
class GetSheetArray {
constructor(productData, settings) {
this.productData = productData;
this.settings = settings;
}
getSheetArray() {
let reportData = this.productData;
let sheetArray = [this.getMetricsHeader()];
sheetArray.push(this.getSheetArrayHeader());
const firstWindowReportData = reportData[this.settings['date_1_days']];
for (let itemId in firstWindowReportData) {
let arrayRow = [];
arrayRow.push(itemId);
arrayRow.push(firstWindowReportData[itemId].title);
arrayRow.push(firstWindowReportData[itemId].campaigns);
if (typeof INCLUDE_INTENT !== 'undefined' && INCLUDE_INTENT) {
arrayRow.push(firstWindowReportData[itemId].highestCampaignIntent);
}
for (let metric in METRICS) {
for (let lookbackKey of LOOKBACK_WINDOW_KEYS) {
let lookbackDays = this.settings[lookbackKey];
if (itemId in reportData[lookbackDays] === false) {
arrayRow.push(0);
continue;
}
arrayRow.push(reportData[lookbackDays][itemId][metric]);
}
}
sheetArray.push(arrayRow);
}
return sheetArray;
}
getMetricsHeader() {
let header = [];
for (let dimension of REPORT_DIMENSIONS) {
header.push('');
}
for (let metric in METRICS) {
header.push(METRICS[metric]);
for (let i = 0; i < LOOKBACK_WINDOW_KEYS.length - 1; i++) {
header.push('');
}
}
return header;
}
getSheetArrayHeader() {
let header = [];
for (let dimension of REPORT_DIMENSIONS) {
header.push(dimension);
}
for (let metric in METRICS) {
for (let lookbackWindowKey of LOOKBACK_WINDOW_KEYS) {
let lookbackWindowName = this.settings[lookbackWindowKey.replace('days', 'name')];
header.push(lookbackWindowName);
}
}
return header;
}
}
class GetReportData {
constructor(productData, lookbackDays) {
this.productData = productData;
this.lookbackDays = lookbackDays;
}
getData() {
let data = {};
for (let product of this.productData) {
let itemId = product['segments.product_item_id'];
this._formatMetrics(product);
data[itemId] = data[itemId] || {};
data['total'] = data['total'] || {};
data[itemId].title = product['segments.product_title'];
data['total'].title = 'Total';
this._updateItemIdStats(data, itemId, product);
this._updateItemIdStats(data, 'total', product);
data['total'].campaigns = '';
data[itemId].campaigns = typeof data[itemId].campaigns === 'undefined' ? product["campaign.name"] : data[itemId].campaigns + ", " + product['campaign.name'];
if (typeof INCLUDE_INTENT !== 'undefined' && INCLUDE_INTENT) {
data[itemId].highestCampaignIntent = this.getHighestCampaignIntent(data[itemId].campaigns);
data['total'].highestCampaignIntent = '';
}
}
this.averageValues(data);
//round the values
for (let itemId in data) {
data[itemId].clicks = Math.round(data[itemId].clicks * 100) / 100;
data[itemId].impressions = Math.round(data[itemId].impressions * 100) / 100;
data[itemId].conversions = Math.round(data[itemId].conversions * 100) / 100;
data[itemId].conversionsValue = Math.round(data[itemId].conversionsValue * 100) / 100;
data[itemId].cost = Math.round(data[itemId].cost * 100) / 100;
}
return data;
}
_formatMetrics(product) {
product['metrics.clicks'] = parseInt(product['metrics.clicks']);
product['metrics.impressions'] = parseInt(product['metrics.impressions']);
product['metrics.conversions'] = parseFloat(product['metrics.conversions']);
product['metrics.conversions_value'] = parseFloat(product['metrics.conversions_value']);
product['cost'] = product['metrics.cost_micros'] / 1000000;
}
_updateItemIdStats(data, itemId, product) {
data[itemId].clicks = data[itemId].clicks + product['metrics.clicks'] || product['metrics.clicks'];
data[itemId].impressions = data[itemId].impressions + product['metrics.impressions'] || product['metrics.impressions'];
data[itemId].conversions = data[itemId].conversions + product['metrics.conversions'] || product['metrics.conversions'];
data[itemId].conversionsValue = data[itemId].conversionsValue + product['metrics.conversions_value'] || product['metrics.conversions_value'];
data[itemId].cost = data[itemId].cost + product['cost'] || product['cost'];
data[itemId].cos = calculateCostOfSalePercentage(data[itemId].cost, data[itemId].conversionsValue);
}
averageValues(data) {
if (this.lookbackDays === 0) {
return data;
}
//average the values by day
for (let itemId in data) {
data[itemId].clicks = data[itemId].clicks / this.lookbackDays;
data[itemId].impressions = data[itemId].impressions / this.lookbackDays;
data[itemId].conversions = data[itemId].conversions / this.lookbackDays;
data[itemId].conversionsValue = data[itemId].conversionsValue / this.lookbackDays;
data[itemId].cost = data[itemId].cost / this.lookbackDays;
}
}
/**
* Return High, Medium or Low based on which campaigns the product falls under
* @param {string} campaigns
*/
getHighestCampaignIntent(campaigns) {
if (campaigns.includes('High Intent')) {
return 'High';
}
if (campaigns.includes('Medium Intent')) {
return 'Medium';
}
return 'Low';
}
}
class GetProductData {
constructor(settings, lookbackDays, sheetName) {
this.settings = settings;
this.lookbackDays = lookbackDays;
this.sheetName = sheetName;
}
getData() {
if (LOCAL) {
//get the query here just to review/test it
const query = this.getApiQuery();
return this.getMockData();
}
return this.getApiData();
}
getApiData() {
const query = this.getApiQuery();
console.log('api query:', query);
const report = AdsApp.report(query);
const rows = report.rows();
let data = [];
if (!rows.hasNext()) {
console.error('No data found for query:', query);
}
while (rows.hasNext()) {
data.push(rows.next());
}
return data;
}
getApiQuery() {
let query = `SELECT segments.product_item_id, segments.product_title, metrics.clicks,
metrics.conversions, metrics.conversions_value, metrics.cost_micros, metrics.impressions `;
query += ` , campaign.name `;
query += `from shopping_performance_view `;
query += ` where metrics.impressions > 0 `;
query += this.getWhereStringFromSettings();
query += this.getDateRangeString();
return query;
}
getWhereStringFromSettings() {
let whereString = '';
if (this.settings['item_id_contains'] !== '') {
whereString += ` and segments.product_item_id like "%${this.settings['item_id_contains']}%" `;
}
if (this.settings['item_id_not_contains'] !== '') {
whereString += ` and segments.product_item_id not like "%${this.settings['item_id_not_contains']}%" `;
}
if (this.settings['product_title_contains'] !== '') {
whereString += ` and segments.product_title like "%${this.settings['product_title_contains']}%" `;
}
if (this.settings['product_title_not_contains'] !== '') {
whereString += ` and segments.product_title not like "%${this.settings['product_title_not_contains']}%" `;
}
if (this.settings['campaign_contains'] !== '') {
whereString += ` and campaign.name like "%${this.settings['campaign_contains']}%" `;
}
if (this.settings['campaign_not_contains'] !== '') {
whereString += ` and campaign.name not like "%${this.settings['campaign_not_contains']}%" `;
}
return whereString;
}
getDateRangeString() {
let endDate = new Date();
if (this.lookbackDays > 0) {
endDate.setDate(endDate.getDate() - 1);
}
let startDate = new Date();
startDate.setDate(startDate.getDate() - this.lookbackDays);
return ` and segments.date >= '${this.dateRangeStringFromDate(startDate)}' and segments.date <= '${this.dateRangeStringFromDate(endDate)}' `;
}
dateRangeStringFromDate(date) {
//format the date as YYYY-MM-DD
let year = date.getFullYear();
let month = date.getMonth() + 1 < 10 ? `0${date.getMonth() + 1}` : date.getMonth() + 1;
let day = date.getDate() < 10 ? `0${date.getDate()}` : date.getDate();
return `${year}-${month}-${day}`;
}
getMockData() {
return [
{
"segments.product_item_id": "jack102a",
"segments.product_title": "Men's Very Nice Red Jacket with 1,000 pockets for all your things",
"metrics.clicks": "7",
"metrics.impressions": "76",
"metrics.conversions": 0,
"metrics.conversions_value": 0,
"metrics.cost_micros": 10000000,
"campaign.name": "Shopping | Men's Jackets | Low Intent | CPC | UK"
},
{
"segments.product_item_id": "jack102a",
"segments.product_title": "Men's Very Nice Red Jacket with 1,000 pockets for all your things",
"metrics.clicks": "85",
"metrics.impressions": "762",
"metrics.conversions": 4.663353,
"metrics.conversions_value": 232.863323,
"metrics.cost_micros": 100000000,
"campaign.name": "Shopping | Men's Jackets | Medium Intent | tROAS 330% | UK"
},
];
}
}
function calculateCostOfSalePercentage(cost, revenue) {
if (revenue === 0 || cost === 0) {
return 0;
}
let costOfSale = cost / revenue;
let costOfSalePercentage = costOfSale * 100;
let costOfSalePercentageRounded = Math.round(costOfSalePercentage * 100) / 100;
return costOfSalePercentageRounded + '%';
}
function log() {
console.log(...arguments);
}
if (LOCAL) {
main();
}
function getSheetNames() {
if (LOCAL) {
return ['UK'];
}
let sheetNames = [];
for (let sheet of SPREADSHEET.getSheets()) {
sheetNames.push(sheet.getName());
}
return sheetNames;
}
function sendErrorEmailToAdmin(error) {
if (typeof SEND_ERROR_EMAIL_TO_SHABBA === 'undefined') {
return;
}
if (!SEND_ERROR_EMAIL_TO_SHABBA) {
return;
}
const adminEmail = "charles@shabba.io";
const subject = `Shabba.io ${SCRIPT_NAME} Script Error`;
let body = `A user got an error.\n`;
body += `Shabba Script ID: ${SHABBA_SCRIPT_ID}\n`;
if (typeof loser !== 'undefined') {
body += `User ID: ${loser}\n`
} else {
body += `User ID: Unknown\n`
}
body += `They got the following error: \n`
body += error;
body += `Here is the stack: \n`
body += error.stack;
MailApp.sendEmail(adminEmail, subject, body);
}
How to Use
- Copy the script using the button above
- Open your Google Ads account
- Go to Tools & Settings → Bulk Actions → Scripts
- Click the + button to create a new script
- Paste the code and click Authorize
- Configure any settings at the top of the script
- Click Preview to test, then Run when ready
