Auction insights script V3: alerting
Nov 21, 2025
This auction insights alert script will notify you when a new competitor enters or leaves your auction insights, or when the data of an existing competitor changes signficantly.
This script builds on top of the Auction Insights Script V2. If you haven't ever set up one of our auction insights scripts, refer to that page for complete setup. We'll only cover the Google Ads scripts setup. Give this page a little scroll to see the full script below :)
Let's get started on the script setup. The first variable you will need to configure is the URL to your master spreadsheet.
var configSpreadsheetUrl = '';
Simply enter the URL between the quotation marks and make sure that you have editor access to this master spreadsheet. If you are unsure how to setup the master spreadsheet, this is completely outlined in the Auction Insights Script V2.
Next, we have the optional competitors to track variable. However, it's important to understand that it will only alert for the competitors we select here. So if you enter 3 competitor names, it will only alert for these three competitors. Leaving it empty will consider all competitors. You can add multiple competitors using a comma seperated list such as 'competitor1', 'competitor2'.
var competitorsToTrack = [];
Next, we have the alerting flag. Set this to true if you want to receive alerts and if you want to log the alerts in your master spreadsheet. Setting it to false means it will not alert or log alerts in the spreadsheet.
var ENABLE_ALERTS = true;
The following step is the alert threshold. This is the minimum percentage change that you want to have before an alert gets triggered. In other words, if I set it to "5", it will only alert increases or decreases greater than 5%.
var ALERT_THRESHOLD = 5;
After the threshold, we configure the metric that we want to alert for. For search this can be Impression share, Overlap rate, Position above rate, Top of page rate, Abs. top of page rate and Outranking share. For Shopping campaigns only Impression share, Outranking share and Overlap rate are available. Please note that this is case sensitive, so add them exactly as they are commented in the script.
var ALERT_METRIC = 'Overlap rate';
Lastly, we get to the date period and the comparison period. This is basically the date range comparison in which you want the alerts to run. For the date period, there are two options: 1) LAST_AVAILABLE_DAY 2) LAST_7_DAYS
If you've selected LAST_AVAILABLE_DAY it will look for the most recent date in which you've got auction insights. You can compare this against three options in the comparison period variable: 1) PREVIOUS_DAY 2) SAME_DAY_LAST_WEEK and 3) AVG_LAST_7_DAYS
If you've selected LAST_7_DAYS, in the date period variable, there's only 1 comparison variable you can select (for now) which is: PREVIOUS_7_DAYS
var ALERT_DATE_PERIOD = 'LAST_AVAILABLE_DAY';
var ALERT_COMPARISON_PERIOD = 'PREVIOUS_DAY';
That's it! Please reach out if you have any questions whatsoever.
//------------------------------------------------------------------------------ // This script visualizes all auction insights data and sends alerts on significant changes. // Works with both Search and Shopping campaigns. // © Adnine //------------------------------------------------------------------------------ function main() { Logger.log('Beep boop scrippity script is starting'); // ============================================================================ // CONFIGURATION // ============================================================================ // Master spreadsheet URL var configSpreadsheetUrl = ''; // COMPETITOR FILTER (optional) // Leave empty array [] to track ALL competitors // Or specify competitors: ['domain1.com', 'domain2.com'] var competitorsToTrack = []; // ============================================================================ // ALERT CONFIGURATION // ============================================================================ // 1. Alerting Toggle var ENABLE_ALERTS = true; // 2. Email Recipients var ALERT_EMAILS = ['']; // 3. Threshold (Percentage change). e.g. 5 means +/- 5% change triggers alert var ALERT_THRESHOLD = 5; // 4. Metric to monitor (Must match column header exactly) // Options: 'Impression share', 'Overlap rate', 'Position above rate', 'Top of page rate', 'Abs. Top of page rate', 'Outranking share' var ALERT_METRIC = 'Overlap rate'; // 5. Date Period // Options: 'LAST_AVAILABLE_DAY', 'LAST_7_DAYS' var ALERT_DATE_PERIOD = 'LAST_AVAILABLE_DAY'; // 6. Comparison Period // If ALERT_DATE_PERIOD is 'LAST_AVAILABLE_DAY', options: 'PREVIOUS_DAY', 'SAME_DAY_LAST_WEEK', 'AVG_LAST_7_DAYS' // If ALERT_DATE_PERIOD is 'LAST_7_DAYS', options: 'PREVIOUS_7_DAYS' var ALERT_COMPARISON_PERIOD = 'PREVIOUS_DAY'; // ============================================================================ Logger.log('Opening config spreadsheet...'); // Read the actual report URL from the config spreadsheet var configSpreadsheet = SpreadsheetApp.openByUrl(configSpreadsheetUrl); Logger.log('Config spreadsheet opened successfully'); var configSheet = configSpreadsheet.getSheetByName('CONFIG'); // If CONFIG sheet doesn't exist, rename the first sheet to CONFIG if (!configSheet) { Logger.log('CONFIG sheet not found, renaming first sheet...'); configSheet = configSpreadsheet.getSheets()[0]; configSheet.setName('CONFIG'); } var spreadsheetUrl = configSheet.getRange('A1').getValue(); if (!spreadsheetUrl) { Logger.log('ERROR: No spreadsheet URL found in config spreadsheet cell A1'); return; } // Open the source report var sourceSpreadsheet = SpreadsheetApp.openByUrl(spreadsheetUrl); var sourceSheet = sourceSpreadsheet.getSheets()[0]; var headerInfo = parseHeaderRow(sourceSheet); if (!headerInfo) { Logger.log('ERROR: Could not parse header row from source spreadsheet'); return; } var sourceData = readSourceData(sourceSheet, headerInfo); Logger.log('Read ' + sourceData.length + ' rows from source report'); // Filter data to only include specified competitors (if configured) if (competitorsToTrack.length > 0) { sourceData = filterCompetitors(sourceData, competitorsToTrack); } // Write to HISTORICAL_DATA accumulateHistoricalData(configSpreadsheet, sourceData, headerInfo); Logger.log('Historical data updated'); // Read directly from HISTORICAL_DATA sheet and create metric tabs generateChartsFromHistoricalDataSheet(configSpreadsheet, headerInfo.metrics, competitorsToTrack); Logger.log('Charts created successfully'); // ============================================================================ // ALERT LOGIC // ============================================================================ if (ENABLE_ALERTS) { Logger.log('Checking for alerts...'); checkAndSendAlerts(configSpreadsheet, { emails: ALERT_EMAILS, metric: ALERT_METRIC, threshold: ALERT_THRESHOLD, datePeriod: ALERT_DATE_PERIOD, comparisonPeriod: ALERT_COMPARISON_PERIOD }); } } function parseHeaderRow(sheet) { var headerRowIndex = 3; var rowData = sheet.getRange(headerRowIndex, 1, 1, sheet.getLastColumn()).getValues()[0]; var metrics = []; var dateColumn = -1; var displayUrlColumn = -1; var metricNames = [ 'Impression share', 'Overlap rate', 'Position above rate', 'Top of page rate', 'Abs. Top of page rate', 'Outranking share' ]; for (var i = 0; i < rowData.length; i++) { var cellValue = String(rowData[i]).toLowerCase().trim(); // Check for day/date column if (cellValue === 'day' || cellValue === 'date') { dateColumn = i; } // Check for Display URL domain column (Search) or Store display name (Shopping) if (cellValue === 'display url domain' || cellValue === 'store display name') { displayUrlColumn = i; } // Check for metrics for (var j = 0; j < metricNames.length; j++) { if (cellValue === metricNames[j].toLowerCase()) { metrics.push({ name: metricNames[j], columnIndex: i }); break; } } } if (dateColumn === -1 || displayUrlColumn === -1 || metrics.length === 0) { return null; } return { headerRowIndex: headerRowIndex, dateColumn: dateColumn, displayUrlColumn: displayUrlColumn, metrics: metrics }; } function readSourceData(sheet, headerInfo) { // Data starts at row 4 (row 3 is header) var dataStartRow = 4; var lastRow = sheet.getLastRow(); if (lastRow < dataStartRow) { return []; } var rawData = sheet.getRange(dataStartRow, 1, lastRow - dataStartRow + 1, sheet.getLastColumn()).getValues(); var processedData = []; for (var i = 0; i < rawData.length; i++) { var row = rawData[i]; var dateValue = row[headerInfo.dateColumn]; var displayUrl = String(row[headerInfo.displayUrlColumn]).trim(); // Skip empty rows if (!dateValue || !displayUrl) { continue; } var dataRow = { date: parseDateValue(dateValue), displayUrl: displayUrl, metrics: {} }; // Read all metric values for (var j = 0; j < headerInfo.metrics.length; j++) { var metric = headerInfo.metrics[j]; var rawValue = row[metric.columnIndex]; var value; if (typeof rawValue === 'string') { var trimmedValue = rawValue.trim(); // Explicit handling for Auction Insights specific formats if (trimmedValue === '< 10%' || trimmedValue === '<10%') { value = 0.099; } // Handle dashes or empty strings as 0 else if (trimmedValue === '-' || trimmedValue === '' || trimmedValue === '--') { value = 0; } else { value = parseFloat(rawValue) || 0; } } else { value = parseFloat(rawValue) || 0; } dataRow.metrics[metric.name] = value; } processedData.push(dataRow); } return processedData; } function parseDateValue(dateValue) { // Use UTC methods if (dateValue instanceof Date) { var year = dateValue.getUTCFullYear(); var month = dateValue.getUTCMonth(); var day = dateValue.getUTCDate(); return new Date(Date.UTC(year, month, day)); } var dateString = String(dateValue).trim(); var parts = dateString.split('-'); if (parts.length === 3) { var year = parseInt(parts[0], 10); var month = parseInt(parts[1], 10) - 1; var day = parseInt(parts[2], 10); return new Date(Date.UTC(year, month, day)); } return new Date(dateString); } function filterCompetitors(data, competitorsToTrack) { // Always include "You" (case-insensitive) var allowedCompetitors = competitorsToTrack.map(function(c) { return c.toLowerCase(); }); return data.filter(function(row) { var domain = row.displayUrl.toLowerCase(); if (domain === 'you') { return true; } return allowedCompetitors.indexOf(domain) !== -1; }); } function accumulateHistoricalData(configSpreadsheet, newData, headerInfo) { var dataSheet = configSpreadsheet.getSheetByName('HISTORICAL_DATA'); if (!dataSheet) { dataSheet = configSpreadsheet.insertSheet('HISTORICAL_DATA', 0); // Create header row var headers = ['Date', 'Domain']; for (var i = 0; i < headerInfo.metrics.length; i++) { headers.push(headerInfo.metrics[i].name); } dataSheet.appendRow(headers); } // Read all existing data as display values to build a map var existingDataMap = {}; var lastRow = dataSheet.getLastRow(); if (lastRow > 1) { var existingData = dataSheet.getRange(2, 1, lastRow - 1, 2).getDisplayValues(); for (var i = 0; i < existingData.length; i++) { var dateStr = existingData[i][0]; var domainStr = existingData[i][1]; var key = dateStr + '|' + domainStr; existingDataMap[key] = true; } } // Add new data (avoid duplicates) for (var i = 0; i < newData.length; i++) { var dataRow = newData[i]; var dateStr = formatDate(dataRow.date); var domainStr = dataRow.displayUrl; var key = dateStr + '|' + domainStr; if (existingDataMap[key]) { continue; } var rowToAdd = [dateStr, domainStr]; for (var j = 0; j < headerInfo.metrics.length; j++) { rowToAdd.push(dataRow.metrics[headerInfo.metrics[j].name] || 0); } dataSheet.appendRow(rowToAdd); existingDataMap[key] = true; } } function formatDate(date) { if (!(date instanceof Date)) { // If it's a string, check if it's already in YYYY-MM-DD format var dateStr = String(date).trim(); if (dateStr.match(/^\d{4}-\d{2}-\d{2}$/)) { return dateStr; } date = new Date(dateStr); } var year = date.getUTCFullYear(); var month = String(date.getUTCMonth() + 1).padStart(2, '0'); var day = String(date.getUTCDate()).padStart(2, '0'); return year + '-' + month + '-' + day; } function generateChartsFromHistoricalDataSheet(spreadsheet, metrics, competitorsToTrack) { var dataSheet = spreadsheet.getSheetByName('HISTORICAL_DATA'); if (!dataSheet || dataSheet.getLastRow() <= 1) { return; } // Read ALL data from HISTORICAL_DATA as strings var allData = dataSheet.getRange(1, 1, dataSheet.getLastRow(), dataSheet.getLastColumn()).getDisplayValues(); var headers = allData[0]; var metricColumnMap = {}; for (var i = 0; i < headers.length; i++) { metricColumnMap[headers[i]] = i; } var summarySheet = createSummaryTabPlaceholder(spreadsheet); var createdCharts = []; var tabPosition = 2; for (var i = 0; i < metrics.length; i++) { var metric = metrics[i]; var chart = createMetricTabFromSheet(spreadsheet, allData, metricColumnMap, metric, tabPosition, competitorsToTrack); if (chart) { createdCharts.push({ chart: chart, metricName: metric.name }); tabPosition++; } } populateSummaryTab(spreadsheet, summarySheet, createdCharts); } function createMetricTabFromSheet(spreadsheet, allData, metricColumnMap, metric, tabPosition, competitorsToTrack) { var sheetName = metric.name.replace(/\s+/g, '_').toUpperCase().replace('ABS.', 'ABSOLUTE'); var metricSheet = spreadsheet.getSheetByName(sheetName); if (metricSheet) { spreadsheet.deleteSheet(metricSheet); } metricSheet = spreadsheet.insertSheet(sheetName, tabPosition); var dateColumn = metricColumnMap['Date']; var domainColumn = metricColumnMap['Domain']; var metricColumn = metricColumnMap[metric.name]; // Build dateMap using display values var dateMap = {}; var displayUrls = new Set(); // Skip header row for (var i = 1; i < allData.length; i++) { var row = allData[i]; var dateStr = String(row[dateColumn]).trim(); var displayUrl = String(row[domainColumn]).trim(); var metricValue = parseFloat(row[metricColumn]) || 0; if (competitorsToTrack.length > 0) { var domainLower = displayUrl.toLowerCase(); if (domainLower !== 'you') { var allowed = false; for (var j = 0; j < competitorsToTrack.length; j++) { if (competitorsToTrack[j].toLowerCase() === domainLower) { allowed = true; break; } } if (!allowed) continue; } } if (!dateMap[dateStr]) { dateMap[dateStr] = {}; } dateMap[dateStr][displayUrl] = metricValue; displayUrls.add(displayUrl); } var displayUrlArray = Array.from(displayUrls).sort(); displayUrlArray = displayUrlArray.filter(function(url) { var hasNonZero = false; for (var date in dateMap) { if (dateMap[date][url] && dateMap[date][url] !== 0) { hasNonZero = true; break; } } return hasNonZero; }); if (displayUrlArray.length === 0) { return null; } var headerRow = ['Date'].concat(displayUrlArray.map(function(url) { return metric.name + ' ' + url; })); metricSheet.appendRow(headerRow); var sortedDates = Object.keys(dateMap).sort(); for (var j = 0; j < sortedDates.length; j++) { var dateStr = sortedDates[j]; var rowData = [dateStr]; for (var k = 0; k < displayUrlArray.length; k++) { rowData.push(dateMap[dateStr][displayUrlArray[k]] || 0); } metricSheet.appendRow(rowData); } var chart = createChart(metricSheet, metric.name, sortedDates.length, displayUrlArray); if (chart) { metricSheet.insertChart(chart); } return chart; } function createSummaryTabPlaceholder(spreadsheet) { var summarySheet = spreadsheet.getSheetByName('SUMMARY'); if (summarySheet) { spreadsheet.deleteSheet(summarySheet); } summarySheet = spreadsheet.insertSheet('SUMMARY', 1); summarySheet.getRange(1, 1).setValue('AUCTION INSIGHTS SUMMARY - ALL METRICS'); summarySheet.getRange(1, 1).setFontSize(20).setFontWeight('bold'); return summarySheet; } function populateSummaryTab(spreadsheet, summarySheet, createdCharts) { var chartHeight = 25; var verticalSpacing = 5; for (var i = 0; i < createdCharts.length; i++) { var chartData = createdCharts[i]; var startRow = 3 + (i * (chartHeight + verticalSpacing)); var startCol = 1; var originalSheetName = chartData.metricName.replace(/\s+/g, '_').toUpperCase().replace('ABS.', 'ABSOLUTE'); var originalSheet = spreadsheet.getSheetByName(originalSheetName); if (originalSheet) { var summaryChart = createSummaryChart(originalSheet, chartData.metricName, startRow, startCol); if (summaryChart) { summarySheet.insertChart(summaryChart); } } } } function createSummaryChart(sourceSheet, metricName, startRow, startCol) { var dataRange = sourceSheet.getDataRange(); var numRows = dataRange.getNumRows() - 1; var numCols = dataRange.getNumColumns(); if (numRows <= 0 || numCols <= 1) { return null; } var chart = sourceSheet.newChart() .setChartType(Charts.ChartType.LINE) .setOption('title', metricName) .setOption('titleTextStyle', {fontSize: 14, bold: true}) .setOption('hAxis.title', 'Date') .setOption('legend.position', 'bottom') .setOption('width', 1200) .setOption('height', 600) .setPosition(startRow, startCol, 0, 0); var dateRange = sourceSheet.getRange(2, 1, numRows); chart.addRange(dateRange); for (var col = 2; col <= numCols; col++) { var seriesRange = sourceSheet.getRange(2, col, numRows); var displayUrlName = sourceSheet.getRange(1, col).getValue().replace(metricName + ' ', ''); chart.addRange(seriesRange); chart.setOption('series.' + (col - 2) + '.labelInLegend', displayUrlName); } return chart.build(); } function createChart(sheet, metricName, rowCount, displayUrlArray) { var row = rowCount + 3; var chart = sheet.newChart() .setChartType(Charts.ChartType.LINE) .setOption('title', metricName) .setOption('hAxis.title', 'Date') .setOption('vAxis.title', metricName) .setOption('legend.position', 'bottom') .setOption('width', 600) .setOption('height', 400) .setPosition(row, 1, 0, 0); var dateRange = sheet.getRange(2, 1, rowCount); chart.addRange(dateRange); for (var col = 2; col <= displayUrlArray.length + 1; col++) { var seriesRange = sheet.getRange(2, col, rowCount); var displayUrlName = sheet.getRange(1, col).getValue().replace(metricName + ' ', ''); chart.addRange(seriesRange); chart.setOption('series.' + (col - 2) + '.labelInLegend', displayUrlName); } return chart.build(); } // ============================================================================ // ALERTING FUNCTIONS // ============================================================================ function checkAndSendAlerts(spreadsheet, config) { var dataSheet = spreadsheet.getSheetByName('HISTORICAL_DATA'); if (!dataSheet || dataSheet.getLastRow() <= 1) { Logger.log('No data available for alerts.'); return; } // 1. Fetch All Data var data = dataSheet.getDataRange().getDisplayValues(); var headers = data[0]; // Map headers to indices var colMap = {}; for (var i = 0; i < headers.length; i++) colMap[headers[i]] = i; var metricIdx = colMap[config.metric]; var dateIdx = colMap['Date']; var domainIdx = colMap['Domain']; if (metricIdx === undefined) { Logger.log('ERROR: Alert metric "' + config.metric + '" not found in headers.'); return; } // 2. Parse Data into Object { Domain: { Date: Value } } // Track max date in the dataset var domainData = {}; var allDates = []; for (var i = 1; i < data.length; i++) { var row = data[i]; var dateStr = row[dateIdx]; var domain = row[domainIdx]; var val = parseFloat(row[metricIdx]) || 0; if (!domainData[domain]) domainData[domain] = {}; domainData[domain][dateStr] = val; if (allDates.indexOf(dateStr) === -1) allDates.push(dateStr); } if (allDates.length === 0) return; allDates.sort(); // Ascending string sort (YYYY-MM-DD) var lastDateStr = allDates[allDates.length - 1]; var lastDate = new Date(lastDateStr); Logger.log('Latest date in data: ' + lastDateStr); // 3. Determine Comparison Dates var currentPeriodDates = []; var comparisonPeriodDates = []; var oneDayMs = 24 * 60 * 60 * 1000; if (config.datePeriod === 'LAST_AVAILABLE_DAY') { currentPeriodDates.push(lastDateStr); if (config.comparisonPeriod === 'PREVIOUS_DAY') { var prevDate = new Date(lastDate.getTime() - oneDayMs); comparisonPeriodDates.push(formatDate(prevDate)); } else if (config.comparisonPeriod === 'SAME_DAY_LAST_WEEK') { var prevDate = new Date(lastDate.getTime() - (7 * oneDayMs)); comparisonPeriodDates.push(formatDate(prevDate)); } else if (config.comparisonPeriod === 'AVG_LAST_7_DAYS') { // Comparison against previous history (T-1 to T-7) for (var k = 1; k <= 7; k++) { var prevDate = new Date(lastDate.getTime() - (k * oneDayMs)); comparisonPeriodDates.push(formatDate(prevDate)); } } } else if (config.datePeriod === 'LAST_7_DAYS') { // Current period: T to T-6 for (var k = 0; k < 7; k++) { var d = new Date(lastDate.getTime() - (k * oneDayMs)); currentPeriodDates.push(formatDate(d)); } if (config.comparisonPeriod === 'PREVIOUS_7_DAYS') { // Comparison: T-7 to T-13 for (var k = 7; k < 14; k++) { var d = new Date(lastDate.getTime() - (k * oneDayMs)); comparisonPeriodDates.push(formatDate(d)); } } } // 4. Construct Readable Period Strings // Sort dates first to ensure ranges are readable (Start to End) currentPeriodDates.sort(); comparisonPeriodDates.sort(); var currentRangeStr = getReadableDateRange(config.datePeriod, currentPeriodDates); var pastRangeStr = getReadableDateRange(config.comparisonPeriod, comparisonPeriodDates); var fullPeriodStr = currentRangeStr + " vs " + pastRangeStr; // 5. Calculate Changes var alerts = []; for (var domain in domainData) { var currentAvg = calculateAverage(domainData[domain], currentPeriodDates); var pastAvg = calculateAverage(domainData[domain], comparisonPeriodDates); // If both results are purely 0 (e.g. domain inactive in both periods), skip. if (currentAvg === 0 && pastAvg === 0) continue; var absChange = currentAvg - pastAvg; var pctChange = 0; var status = ''; if (pastAvg !== 0) { pctChange = (absChange / pastAvg) * 100; if (currentAvg === 0) status = 'LOST'; } else if (currentAvg !== 0) { // Handle 0 to non-zero growth pctChange = 100; status = 'NEW'; } var shouldAlert = false; // Trigger if threshold met if (Math.abs(pctChange) >= config.threshold) { shouldAlert = true; Logger.log('Alert Triggered [Threshold]: ' + domain + ' ' + pctChange.toFixed(2) + '%'); } // Or trigger if Status indicates NEW/LOST (Always check this) if (status !== '') { shouldAlert = true; Logger.log('Alert Triggered [Status]: ' + domain + ' ' + status); } if (shouldAlert) { alerts.push({ domain: domain, current: currentAvg, past: pastAvg, absChange: absChange, pctChange: pctChange, status: status }); } } // 6. Log and Send if (alerts.length > 0) { Logger.log('Triggering ' + alerts.length + ' alerts.'); // Log to Spreadsheet logAlertsToSheet(spreadsheet, config, alerts, fullPeriodStr); // Send Email sendAlertEmail(config, alerts, fullPeriodStr); } else { Logger.log('No alerts triggered. Max change was below ' + config.threshold + '% and no New/Lost found.'); } } function getReadableDateRange(periodName, dates) { if (!dates || dates.length === 0) return periodName; if (dates.length === 1) { return periodName + ' (' + dates[0] + ')'; } // Assuming dates are sorted return periodName + ' (' + dates[0] + ' to ' + dates[dates.length - 1] + ')'; } function calculateAverage(dateValueMap, dates) { var sum = 0; var count = 0; for (var i = 0; i < dates.length; i++) { var date = dates[i]; var val = dateValueMap[date]; // If data exists for date, use it. // If undefined (missing row), treat as 0. if (val !== undefined) { sum += val; } else { sum += 0; } count++; } if (count === 0) return 0; return sum / count; } function logAlertsToSheet(spreadsheet, config, alerts, fullPeriodStr) { var sheet = spreadsheet.getSheetByName('ALERTS'); if (!sheet) { sheet = spreadsheet.insertSheet('ALERTS', 1); // Status Column is Permanent sheet.appendRow(['Timestamp', 'Metric', 'Comparison Period', 'Domain', 'Status', 'Current Value', 'Previous Value', 'Abs Change', '% Change']); sheet.setFrozenRows(1); sheet.getRange(1, 1, 1, 9).setFontWeight('bold'); } var timestamp = new Date(); // Sort matches email sort (Absolute % Change Descending) alerts.sort(function(a, b) { return Math.abs(b.pctChange) - Math.abs(a.pctChange); }); for (var i = 0; i < alerts.length; i++) { var alert = alerts[i]; var statusText = alert.status || ''; // Convert 0-1 scale to 0-100 and Fix to 2 decimals for numeric columns sheet.appendRow([ timestamp, config.metric, fullPeriodStr, alert.domain, statusText, parseFloat((alert.current * 100).toFixed(2)), parseFloat((alert.past * 100).toFixed(2)), parseFloat((alert.absChange * 100).toFixed(2)), parseFloat(alert.pctChange.toFixed(2)) ]); } Logger.log('Logged ' + alerts.length + ' alerts to ALERTS tab'); } function sendAlertEmail(config, alerts, fullPeriodStr) { Logger.log('Attempting to send email to: ' + config.emails.join(',')); var subject = 'Auction insights alert'; var metricText = config.metric; var htmlBody = '<h3>Auction Insights Alert</h3>'; htmlBody += '<p><strong>Metric:</strong> ' + metricText + '<br>'; htmlBody += '<strong>Comparison:</strong> ' + fullPeriodStr + '<br>'; htmlBody += '<strong>Threshold:</strong> +/- ' + config.threshold + '%</p>'; htmlBody += '<table border="1" cellpadding="5" cellspacing="0" style="border-collapse: collapse; font-family: Arial, sans-serif;">'; htmlBody += '<tr style="background-color: #f2f2f2;">'; htmlBody += '<th>Domain</th>'; // Status Column is Permanent htmlBody += '<th>Status</th>'; htmlBody += '<th>Comparison Period</th>'; htmlBody += '<th>Current</th>'; htmlBody += '<th>Previous</th>'; htmlBody += '<th>Abs Change</th>'; htmlBody += '<th>% Change</th>'; htmlBody += '</tr>'; // Sort by absolute % change descending alerts.sort(function(a, b) { return Math.abs(b.pctChange) - Math.abs(a.pctChange); }); for (var i = 0; i < alerts.length; i++) { var item = alerts[i]; // Visualize increase as Green and decrease as Red var color = item.pctChange > 0 ? 'green' : 'red'; var statusHtml = ''; if (item.status === 'NEW') { statusHtml = '<span style="color:blue; font-weight:bold;">NEW</span>'; } else if (item.status === 'LOST') { statusHtml = '<span style="color:gray; font-weight:bold;">LOST</span>'; } else { statusHtml = ''; } htmlBody += '<tr>'; htmlBody += '<td>' + item.domain + '</td>'; htmlBody += '<td>' + statusHtml + '</td>'; htmlBody += '<td>' + fullPeriodStr + '</td>'; htmlBody += '<td>' + (item.current * 100).toFixed(2) + '%</td>'; htmlBody += '<td>' + (item.past * 100).toFixed(2) + '%</td>'; htmlBody += '<td>' + (item.absChange * 100).toFixed(2) + '%</td>'; htmlBody += '<td style="color:' + color + '; font-weight:bold;">' + item.pctChange.toFixed(2) + '%</td>'; htmlBody += '</tr>'; } htmlBody += '</table>'; try { MailApp.sendEmail({ to: config.emails.join(','), subject: subject, htmlBody: htmlBody, name: 'Scripts Alerts' }); Logger.log('Email sent successfully.'); } catch (e) { Logger.log('ERROR SENDING EMAIL: ' + e.toString()); } }
