Automação Google Ads: Texto do Anúncio

Nessa série especial, reunimos alguns scripts úteis para você automatizar e aumentar o retorno de suas campanhas no Google Ads. Confira abaixo como automatizar o Google Ads para “Trabalhar com Textos dos anúncios”.

 

1. Corrija os erros de letras maiúsculas em seus anúncios – por Russell Savage. Caso seus anúncios contenham muitas letras maiúsculas, o Google pode não aprová-los. Esse script substitui quaisquer sequências de 3 letras maiúsculas, criando um anúncio novo e pausando o antigo.

//———————————–
// Fix Ads with EXCESSIVE CAPITALIZATION
// Created By: Russ Savage
// FreeAdWordsScripts.com
//———————————–
function main() {
var find_caps = /[A-Z]{3,}/g;
var SEP = ‘~~@~~’; // this needs to be something you would never put in your ads.
var ad_iter = AdWordsApp.ads().withCondition(“ApprovalStatus = ‘DISAPPROVED'”).get();

while(ad_iter.hasNext()) {
var ad = ad_iter.next();
var old_ad_cnt = get_ad_count(ad.getAdGroup());
var old_ad_str = [ad.getHeadline(),ad.getDescription1(),ad.getDescription2(),ad.getDisplayUrl()].join(SEP);
var new_ad_str = old_ad_str;
Logger.log(“Before:”+old_ad_str);
var m = “”;
while((m = find_caps.exec(new_ad_str)) != null) {
new_ad_str = replace_all(new_ad_str,m[0],init_cap(m[0]),false);
}
Logger.log(“After:”+new_ad_str);
if(old_ad_str != new_ad_str) {
var [new_headline,new_desc1,new_desc2,new_disp_url] = new_ad_str.split(SEP);
ad.getAdGroup().createTextAd(new_headline, new_desc1, new_desc2, new_disp_url, ad.getDestinationUrl());
var new_ad_cnt = get_ad_count(ad.getAdGroup());
if(new_ad_cnt == (old_ad_cnt+1)) {
ad.remove();
}
} else {
Logger.log(“Skipping because no changes were made.”);
}
}

function init_cap(s) {
return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
}

// This function was adapted from: http://dumpsite.com/forum/index.php?topic=4.msg8#msg8
function replace_all(original,str1, str2, ignore) {
return original.replace(new RegExp(str1.replace(/([\/\,\!\\\^\$\{\}\[\]\(\)\.\*\+\?\|\<\>\-\&])/g,”\\$&”),(ignore?”gi”:”g”)),(typeof(str2)==”string”)?str2.replace(/\$/g,”$$$$”):str2);
}

function get_ad_count(ad_group) {
var ad_iter = ad_group.ads().get();
var new_ad_cnt = 0;
while(ad_iter.hasNext()) {
ad_iter.next();
new_ad_cnt++;
}
return new_ad_cnt;
}
}

 

2. Automação de testes de anúncios – Por Russell Savage. O Google costuma analisar em 1 dia anúncios para aprovação, porém nem sempre este é o caso. Esse script detecta quando há uma alteração no anúncio e grava em uma planilha do Google com a data em que ele começou a rodar.

/************************************
* Ad Creative Test Automation Script
* Version: 1.3
* Changelog v1.3 – Data is written to the spreadsheet a little faster
* Changelog v1.2 – Added additional threshold options
* Changelog v1.1 – Fixed issue with dates in email
* Created By: Russ Savage
* FreeAdWordsScripts.com
************************************/
// You can use any of the same values here as you can for METRIC below
var THRESHOLD = { metric : ‘Clicks’, value : 100 };
var TO = [‘[email protected]’];

//Try any of these values for METRIC:
//AverageCpc, AverageCpm, AveragePageviews, AveragePosition,
//AverageTimeOnSite, BounceRate, Clicks, ConversionRate,
//Conversions, Cost, Ctr, Impressions
var METRIC = ‘Ctr’;
var ASC_OR_DESC = ‘ASC’; //ASC – pause ad with lowest value, DESC – pause ad with highest value

function main() {
//Start by finding what has changed in the account
var ad_map = buildCurrentAdMap();
var prev_ad_map = readMapFromSpreadsheet();
prev_ad_map = updatePreviousAdMap(prev_ad_map,ad_map);
writeMapToSpreadsheet(prev_ad_map);

//Now run through the adgroups to find creative tests
var ag_iter = AdWordsApp.adGroups().get();
var paused_ads = [];
while(ag_iter.hasNext()) {
var ag = ag_iter.next();
if(!prev_ad_map[ag.getId()]) { continue; }

//Here is the date range for the test metrics
var last_touched_str = _getDateString(prev_ad_map[ag.getId()].last_touched,’yyyyMMdd’);
var get_today_str = _getDateString(new Date(),’yyyyMMdd’);

var ag_stats = ag.getStatsFor(last_touched_str, get_today_str);
if(ag_stats[‘get’+THRESHOLD.metric]() >= THRESHOLD.value) {
var ad_iter = ag.ads().withCondition(‘Status = ENABLED’)
.forDateRange(last_touched_str, get_today_str)
.orderBy(METRIC+” “+ASC_OR_DESC).get();
var ad = ad_iter.next();
var metric = ad.getStatsFor(last_touched_str, get_today_str)[‘get’+METRIC]();
ad.pause();
paused_ads.push({a : ad, m : metric});
}
}
sendEmailForPausedAds(paused_ads);
}

// A function to send an email with an attached report of ads it has paused
function sendEmailForPausedAds(ads) {
if(ads.length == 0) { return; } //No ads paused, no email
var email_body = ‘”‘ + [‘CampaignName’,’AdGroupName’,’Headline’,’Desc1′,’Desc2′,’DisplayUrl’,METRIC].join(‘”,”‘) + ‘”\n’;
for(var i in ads) {
var ad = ads[i].a;
var metric = ads[i].m;
email_body += ‘”‘ + [ad.getCampaign().getName(),
ad.getAdGroup().getName(),
ad.getHeadline(),
ad.getDescription1(),
ad.getDescription2(),
ad.getDisplayUrl(),
metric
].join(‘”,”‘) +
‘”\n’;
}
var date_str = _getDateString(new Date(),’yyyy-MM-dd’);
var options = { attachments: [Utilities.newBlob(email_body, ‘text/csv’, “FinishedTests_”+date_str+’.csv’)] };
var subject = ‘Finished Tests – ‘ + date_str;
for(var i in TO) {
MailApp.sendEmail(TO[i], subject, ‘See attached.’, options);
}
}

//Given two lists of ads, this checks to make sure they are the same.
function sameAds(ads1,ads2) {
for(var i in ads1) {
if(ads1[i] != ads2[i]) { return false; }
}
return true;
}

//This reads the stored data from the spreadsheet
function readMapFromSpreadsheet() {
var ad_map = {};
var sheet = SpreadsheetApp.openById(findSpreadsheetId()).getActiveSheet();
var data = sheet.getRange(‘A:C’).getValues();
for(var i in data) {
if(data[i][0] == ”) { break; }
var [ag_id,last_touched,ad_ids] = data[i];
ad_map[ag_id] = { ad_ids : (”+ad_ids).split(‘,’), last_touched : new Date(last_touched) };
}
return ad_map;
}

//This will search for a label containing the spreadsheet id
//If one isn’t found, it will create a new one and the label as well
function findSpreadsheetId() {
var spreadsheet_id = “”;
var label_iter = AdWordsApp.labels().withCondition(“Name STARTS_WITH ‘history_script:'”).get();
if(label_iter.hasNext()) {
var label = label_iter.next();
return label.getName().split(‘:’)[1];
} else {
var sheet = SpreadsheetApp.create(‘AdGroups History’);
var sheet_id = sheet.getId();
AdWordsApp.createLabel(‘history_script:’+sheet_id, ‘stores sheet id for adgroup changes script.’);
return sheet_id;
}
}

//This will store the data from the account into a spreadsheet
function writeMapToSpreadsheet(ad_map) {
var toWrite = [];
for(var ag_id in ad_map) {
var ad_ids = ad_map[ag_id].ad_ids;
var last_touched = ad_map[ag_id].last_touched;
toWrite.push([ag_id,last_touched,ad_ids.join(‘,’)]);
}
writeToSpreadsheet(toWrite);
}

// Write the keyword data to the spreadsheet
function writeToSpreadsheet(toWrite) {
var sheet = SpreadsheetApp.openById(findSpreadsheetId()).getActiveSheet();
sheet.clear();

var numRows = sheet.getMaxRows();
if(numRows < toWrite.length) {
sheet.insertRows(1,toWrite.length-numRows);
}
var range = sheet.getRange(1,1,toWrite.length,toWrite[0].length);
range.setValues(toWrite);
}

//This builds a map of the ads in the account so that it is easy to compare
function buildCurrentAdMap() {
var ad_map = {}; // { ag_id : { ad_ids : [ ad_id, … ], last_touched : date } }
var ad_iter = AdWordsApp.ads().withCondition(‘Status = ENABLED’).get();
while(ad_iter.hasNext()) {
var ad = ad_iter.next();
var ag_id = ad.getAdGroup().getId();
if(ad_map[ag_id]) {
ad_map[ag_id].ad_ids.push(ad.getId());
ad_map[ag_id].ad_ids.sort();
} else {
ad_map[ag_id] = { ad_ids : [ad.getId()], last_touched : new Date() };
}
}
return ad_map;
}

//This takes the old ad map and the current ad map and returns an
//updated map with all changes.
function updatePreviousAdMap(prev_ad_map,ad_map) {
for(var ag_id in ad_map) {
var current_ads = ad_map[ag_id].ad_ids;
var previous_ads = (prev_ad_map[ag_id]) ? prev_ad_map[ag_id].ad_ids : [];
if(!sameAds(current_ads,previous_ads)) {
prev_ad_map[ag_id] = ad_map[ag_id];
}
}
return prev_ad_map;
}

//Helper function to format the date
function _getDateString(date,format) {
return Utilities.formatDate(date,AdWordsApp.currentAccount().getTimeZone(),format);
}

 

3. Gerencie seus criativos com Excel – Por Russell Savage. Integre o Google Ads com o Drive e baixe todas as métricas automaticamente no seu computador. Não só isso, mas também adicione palavras-chave, gerencie anúncios, grupo de anúncios, extensões etc. Para utilizar esse recurso é necessário ter o Google Drive em seu computador.

/******************************************
* Manage AdWords Ads Using Excel
* Version 1.0
* Author: Russ Savage
* FreeAdWordsScripts.com
****************************************/
var FOLDER_PATH = ‘AdWordsData’; //The path where the file will be stored on GDrive
var FILE_NAME = ‘creatives.csv’; //The name of the file on GDrive

var INCLUDE_STATS = true; // Set to false to remove stats from the file
var DATE_RANGE = ‘LAST_30_DAYS’; //The date range for the stats

var INCLUDE_PAUSED_ADS = true; //Set to false to only report on active ads
var DELETE_ORIGINAL_ADS = true; //When set to false, the original ads will be paused instead of deleted

function main() {
var file = getFile(FILE_NAME,FOLDER_PATH);
if(!file) {
file = createNewFile(FILE_NAME,FOLDER_PATH,buildCreativesFile());
return;
}

var fileContent = file.getBlob().getDataAsString();
var creatives = {};
if(fileContent) {
creatives = parseFileContent(fileContent);
}

var creativeIter = getAdIterator();
while(creativeIter.hasNext()) {
var creative = creativeIter.next();
var adId = creative.getId();
if(creatives[adId]) {
//ok we found the ad.
//Checking status
var isEnabledFile = (creatives[adId][‘Status’] === ‘Enabled’);
if(creative.isEnabled() != isEnabledFile) {
if(isEnabledFile) {
creative.enable();
} else {
creative.pause();
}
}

if(hadAdChanged(creative,creatives[adId])) {
if(DELETE_ORIGINAL_ADS) {
creative.remove();
} else {
creative.pause();
}
createNewAd(creative.getAdGroup(),creatives[creative.getId()]);
}
}
}
file.setContent(buildCreativesFile());
}

//Helper function to create a new ad
function createNewAd(ag,newAd) {
var optArgs = {
isMobilePreferred: (newAd[‘Device’] === ‘Mobile’) ? true : false
};
ag.createTextAd(newAd[‘Headline’],newAd[‘Desc1’],newAd[‘Desc2’],newAd[‘DisplayUrl’],newAd[‘DestinationUrl’],optArgs);
}

//This checks to see if the ad has been changed
function hadAdChanged(ad,oldAd) {
var newAdText = [ad.getHeadline(),
ad.getDescription1(),
ad.getDescription2(),
ad.getDisplayUrl(),
ad.getDestinationUrl(),
(ad.isMobilePreferred()) ? ‘Mobile’ : ‘Desktop’].join(‘~~!~~’);
var oldAdText = [oldAd[‘Headline’],
oldAd[‘Desc1’],
oldAd[‘Desc2’],
oldAd[‘DisplayUrl’],
oldAd[‘DestinationUrl’],
oldAd[‘Device’]].join(‘~~!~~’);
Logger.log(newAdText);
Logger.log(oldAdText);
return (newAdText !== oldAdText);
}

//This builds the creatives file from all the ads in the account.
function buildCreativesFile() {
var report = getReportColumns();
var creativeIter = getAdIterator();
while(creativeIter.hasNext()) {
var creative = creativeIter.next();
report += getReportRow(creative);
}
return report;
}

//This returns the ad iterator based on options.
function getAdIterator() {
var adSelector = AdWordsApp.ads().withCondition(“Type = ‘TEXT_AD'”);
if(!INCLUDE_PAUSED_ADS) {
adSelector = adSelector.withCondition(‘CampaignStatus = ENABLED’)
.withCondition(‘AdGroupStatus = ENABLED’)
.withCondition(‘Status = ENABLED’);
}
return adSelector.get();
}

//This returns a CSV fow for the report.
function getReportRow(ad) {
var retVal = [
ad.getId(),
ad.getCampaign().getName(),(ad.getCampaign().isPaused()) ? ‘Paused’ : ‘Enabled’,
ad.getAdGroup().getName(),(ad.getAdGroup().isPaused()) ? ‘Paused’ : ‘Enabled’,
ad.getHeadline(),
ad.getDescription1(),
ad.getDescription2(),
ad.getDisplayUrl(),
ad.getDestinationUrl(),
(ad.isPaused()) ? ‘Paused’ : ‘Enabled’,
(ad.isMobilePreferred()) ? ‘Mobile’ : ‘Desktop’,
];
if(INCLUDE_STATS) {
var stats = ad.getStatsFor(DATE_RANGE);
retVal = retVal.concat([
stats.getImpressions(),
stats.getClicks(),
stats.getCtr(),
stats.getCost(),
stats.getAverageCpc(),
stats.getConversions(),
stats.getConversionRate(),
stats.getAveragePageviews(),
stats.getAveragePosition(),
stats.getAverageTimeOnSite(),
stats.getBounceRate()
]);
}
return ‘”‘ + retVal.join(‘”,”‘) + ‘”\n’;
}

//This returns the column headings used for the report.
function getReportColumns() {
var columnHeadings = [
‘AdId’,
‘CampaignName’,’CampaignStatus’,
‘AdGroupName’,’AdGroupStatus’,
‘Headline’,
‘Desc1’,
‘Desc2’,
‘DisplayUrl’,
‘DestinationUrl’,
‘Status’,
‘Device’];
if(INCLUDE_STATS) {
columnHeadings = columnHeadings.concat([
‘Impressions’,
‘Clicks’,
‘Ctr’,
‘Cost’,
‘Cpc’,
‘Conversions’,
‘ConversionRate’,
‘AveragePageviews’,
‘AvgPosition’,
‘AvgTimeOnSite’,
‘BounceRate’
]);
}
return ‘”‘ + columnHeadings.join(‘”,”‘) + ‘”\n’;
}

//This function parses the creatives file into an object for processing
function parseFileContent(fileContent) {
var headers = [];
var idHash = {};
var data = Utilities.parseCsv(fileContent);
for(var i in data) {
var cells = data[i]
if(cells.length == 1) { continue; } //skip any empty rows
if(i == 0) {
headers = cells;
} else {
var rowMap = {};
for(var x in headers) {
headers[x] = headers[x];
cells[x] = cells[x];
rowMap[headers[x]] = cells[x];
}
idHash[rowMap[‘AdId’]] = rowMap;
}
}
return idHash;
}

//This function gets the file from GDrive
function getFile(fileName,folderPath) {
var folder = getFolder(folderPath);
if(folder.getFilesByName(fileName).hasNext()) {
return folder.getFilesByName(fileName).next();
} else {
return null;
}
}

//This function creates a new file on GDrive
function createNewFile(fileName,folderPath,content) {
if(!fileName) { throw ‘createNewFile: Missing filename.’; }
var folder = getFolder(folderPath);

return folder.createFile(fileName, content);
}

//This function finds the folder for the file and creates folders if needed
function getFolder(folderPath) {
var folder = DriveApp.getRootFolder();
if(folderPath) {
var pathArray = folderPath.split(‘/’);
for(var i in pathArray) {
var folderName = pathArray[i];
if(folder.getFoldersByName(folderName).hasNext()) {
folder = folder.getFoldersByName(folderName).next();
} else {
folder = folder.createFolder(folderName);
}
}
}
return folder;
}

 

4. Copie anúncios existentes com uma nova URL – Por Derek Martin. Economize tempo ao realizar testes para uma nova página de destino, o script irá copiar os anúncios da sua conta com a URL desejada.

function main() {
// change the CampaignName condition to whatever suits you
var adIter = AdWordsApp.ads().withCondition(“CampaignName contains WP”).withCondition(“Status = ENABLED”).get();

while(adIter.hasNext()) {
var ad = adIter.next();

var headline = ad.getHeadline();
var d1 = ad.getDescription1()
var d2 = ad.getDescription2();
var displayUrl = ad.getDisplayUrl();
var dest = “http://your-url-here.com/”;

var camp = ad.getCampaign();
var adgroup = ad.getAdGroup();

var newAd = adgroup.newTextAdBuilder()
.withHeadline(headline)
.withDescription1(d1)
.withDescription2(d2)
.withDisplayUrl(displayUrl)
.withDestinationUrl(dest)
.build();

}
}

 

 

5. Sincronize seu estoque com anúncios do Google Ads – Por Nathan Byloff. Esse script é exclusivo para sites que utilizam o WooCommerce. Caso o item de seu e-commerce esteja esgotado, o script pausa os anúncios impedindo que seu orçamento seja gastado.

var CAMPAIGN_NAME = “Your Campaign Name”;
var userKey = “Your API Key”;
var userSecret = “Your API Secret”;
function main() {
var campaign = getCampaign();
var url = ‘https://www.yourstore.com/wc-api/v2/products?consumer_key=’ + userKey + ‘&consumer_secret=’ + userSecret;
var response = UrlFetchApp.fetch(url);
response = response.getContentText();
var data = JSON.parse(response);
for(var i=0; i < data.products.length; i++) {
var product = data.products[i];
if (product.in_stock == true) {
Logger.log(‘enable ad: ‘ + product.sku);
toggleAd(campaign, product.sku, true);
} else {
Logger.log(‘disable ad: ‘ + product.sku);
toggleAd(campaign, product.sku, false);
}
}
}

function toggleAd(campaign, sku, status) {
var adsIterator = campaign.ads().withCondition(‘LabelNames CONTAINS_ANY [“‘ + sku + ‘”]’).get();
if (adsIterator.hasNext()) {
//Find all ad groups and store them in an array
while (adsIterator.hasNext()) {
var ad = adsIterator.next();
if (status) {
if (!ad.isEnabled()) {
ad.enable();
}
} else if (!status) {
//disable ad
if (ad.isEnabled()) {
ad.pause();
}
}
}
}
}

/**
* Get a specified campaign based on a global var we’ve set
*/
function getCampaign() {
return AdWordsApp.campaigns().withCondition(“Name = ‘” + CAMPAIGN_NAME + “‘”).get().next();
}

 

6. Atualizador de contagens regressivas – Por Derek Martin. O script procura por anúncios que estejam com a contagem regressiva expirada e substitui por novos anúncios com datas atualizadas.

/***************************************************************************************
* AdWords Countdown Ad Updater — Find stale countdown ads and replace them with
* Ads that are updated with new dates.
* Version 1.0
* Created By: Derek Martin
* DerekMartinLA.com or MixedMarketingArtist.com
****************************************************************************************/

var DESCRIPTION2_TEXT = “Sale Ends In”

function main() {
// Set your campaign criteria here

var campIter = AdWordsApp.campaigns().withCondition(“Status=ENABLED”).withCondition(“Name does_not_contain Remarketing”).get();

while (campIter.hasNext()) {
var camp = campIter.next();

var agIter = camp.adGroups().withCondition(“Status = ENABLED”).get();

while (agIter.hasNext()) {
var ag = agIter.next();

var adsIter = ag.ads().withCondition(“Status = ENABLED”).get();

while (adsIter.hasNext()) {
ad = adsIter.next();

var headline,d1,d2,displayUrl, destUrl;
headline = ad.getHeadline();
d1 = ad.getDescription1();
d2 = ad.getDescription2();
displayUrl = ad.getDisplayUrl();
destUrl = ad.getDestinationUrl();

var regex = /\{=COUNTDOWN\(\”(\d{4})\/(\d{1,2})\/(\d{1,2})/

if (d2.match(regex)) {
match = d2.match(regex);
info(match);
var the_date = new Date(match[1], match[2]-1,match[3])
var today = new Date;
info(today);
if (daysBetween(today, the_date) < 0) {
ad.pause();
var newDate = new Date();

newDate.setDate(newDate.getDate() + 5); // add 5 days
var formattedDate = Utilities.formatDate(newDate, “PST”, “yyyy/MM/dd”);
var newDest2 = DESCRIPTION2_TEXT + ” {\=COUNTDOWN(\”” + formattedDate +” 23:59:00\”)}.”

status = ag.newTextAdBuilder().withHeadline(headline).withDescription1(d1).withDescription2(newDest2).withDisplayUrl(displayUrl).withDestinationUrl(destUrl).build();
}
}
}

}
}
}

function daysBetween( date1, date2 ) {
//Get 1 day in milliseconds
var one_day=1000*60*60*24;

// Convert both dates to milliseconds
var date1_ms = date1.getTime();
var date2_ms = date2.getTime();

// Calculate the difference in milliseconds
var difference_ms = date2_ms – date1_ms;

// Convert back to days and return
return Math.round(difference_ms/one_day);
}

/* HELPER FUNCTIONS */

function warn(msg) {
Logger.log(‘WARNING: ‘+msg);
}

function info(msg) {
Logger.log(msg);
}

 

7. Automatizador de criativos com estatísticas – Por Russell Savage. O script notifica por e-mail quando seus anúncios de teste adquirem certa significância estatística para você tomar uma ação. É um ótimo script caso não queira tomar decisões apenas pelas métricas do Google Ads.

/*********************************************
* Automated Creative Testing With Statistical Significance
* Version 2.1
* Changelog v2.1
* – Fixed INVALID_PREDICATE_ENUM_VALUE
* Changelog v2.0
* – Fixed bug in setting the correct date
* – Script now uses a minimum visitors threshold
* per Ad instead of AdGroup
* – Added the ability to add the start date as a label to AdGroups
* – Added ability to check mobile and desktop ads separately
* Changelog v1.1.1 – Fixed bug with getDisplayUrl
* Changelog v1.1
* – Added ability to only run on some campaigns
* – Fixed bug in info logging
* Russ Savage
* FreeAdWordsScripts.com
**********************************************/
var EXTRA_LOGS = true;
var TO = [‘[email protected]’];
var CONFIDENCE_LEVEL = 95; // 90%, 95%, or 99% are most common

//If you only want to run on some campaigns, apply a label to them
//and put the name of the label here. Leave blank to run on all campaigns.
var CAMPAIGN_LABEL = ”;

//These two metrics are the components that make up the metric you
//want to compare. For example, this measures CTR = Clicks/Impressions
//Other examples might be:
// Cost Per Conv = Cost/Conversions
// Conversion Rate = Conversions/Clicks
// Cost Per Click = Cost/Clicks
var VISITORS_METRIC = ‘Impressions’;
var CONVERSIONS_METRIC = ‘Clicks’;
//This is the number of impressions the Ad needs to have in order
//to start measuring the results of a test.
var VISITORS_THRESHOLD = 100;

//Setting this to true to enable the script to check mobile ads
//against other mobile ads only. Enabling this will start new tests
//in all your AdGroups so only enable this after you have completed
//a testing cycle.
var ENABLE_MOBILE_AD_TESTING = false;

//Set this on the first run which should be the approximate last time
//you started a new creative test. After the first run, this setting
//will be ignored.
var OVERRIDE_LAST_TOUCHED_DATE = ‘Jan 1, 2014’;

var LOSER_LABEL = ‘Loser ‘+CONFIDENCE_LEVEL+’% Confidence’;
var CHAMPION_LABEL = ‘Current Champion’;

// Set this to true and the script will apply a label to
// each AdGroup to let you know the date the test started
// This helps you validate the results of the script.
var APPLY_TEST_START_DATE_LABELS = true;

//These come from the url when you are logged into AdWords
//Set these if you want your emails to link directly to the AdGroup
var __c = ”;
var __u = ”;

function main() {
createLabelIfNeeded(LOSER_LABEL,”#FF00FF”); //Set the colors of the labels here
createLabelIfNeeded(CHAMPION_LABEL,”#0000FF”); //Set the colors of the labels here

//Let’s find all the AdGroups that have new tests starting
var currentAdMap = getCurrentAdsSnapshot();
var previousAdMap = getPreviousAdsSnapshot();
if(previousAdMap) {
currentAdMap = updateCurrentAdMap(currentAdMap,previousAdMap);
}
storeAdsSnapshot(currentAdMap);
previousAdMap = null;

//Now run through the AdGroups to find tests
var agSelector = AdWordsApp.adGroups()
.withCondition(‘CampaignStatus = ENABLED’)
.withCondition(‘AdGroupStatus = ENABLED’)
.withCondition(‘Status = ENABLED’);
if(CAMPAIGN_LABEL !== ”) {
var campNames = getCampaignNames();
agSelector = agSelector.withCondition(“CampaignName IN [‘”+campNames.join(“‘,'”)+”‘]”);
}
var agIter = agSelector.get();
var todayDate = getDateString(new Date(),’yyyyMMdd’);
var touchedAdGroups = [];
var finishedEarly = false;
while(agIter.hasNext()) {
var ag = agIter.next();

var numLoops = (ENABLE_MOBILE_AD_TESTING) ? 2 : 1;
for(var loopNum = 0; loopNum < numLoops; loopNum++) {
var isMobile = (loopNum == 1);
var rowKey;
if(isMobile) {
info(‘Checking Mobile Ads in AdGroup: “‘+ag.getName()+'”‘);
rowKey = [ag.getCampaign().getId(),ag.getId(),’Mobile’].join(‘-‘);
} else {
info(‘Checking Ads in AdGroup: “‘+ag.getName()+'”‘);
rowKey = [ag.getCampaign().getId(),ag.getId()].join(‘-‘);
}

if(!currentAdMap[rowKey]) { //This shouldn’t happen
warn(‘Could not find AdGroup: ‘+ag.getName()+’ in current ad map.’);
continue;
}

if(APPLY_TEST_START_DATE_LABELS) {
var dateLabel;
if(isMobile) {
dateLabel = ‘Mobile Tests Started: ‘+getDateString(currentAdMap[rowKey].lastTouched,’yyyy-MM-dd’);
} else {
dateLabel = ‘Tests Started: ‘+getDateString(currentAdMap[rowKey].lastTouched,’yyyy-MM-dd’);
}

createLabelIfNeeded(dateLabel,”#8A2BE2”);
//remove old start date
var labelIter = ag.labels().withCondition(“Name STARTS_WITH ‘”+dateLabel.split(‘:’)[0]+”‘”)
.withCondition(“Name != ‘”+dateLabel+”‘”).get();
while(labelIter.hasNext()) {
var label = labelIter.next();
ag.removeLabel(label.getName());
if(!label.adGroups().get().hasNext()) {
//if there are no more entities with that label, delete it.
label.remove();
}
}
applyLabel(ag,dateLabel);
}

//Here is the date range for the test metrics
var lastTouchedDate = getDateString(currentAdMap[rowKey].lastTouched,’yyyyMMdd’);
info(‘Last Touched Date: ‘+lastTouchedDate+’ Todays Date: ‘+ todayDate);
if(lastTouchedDate === todayDate) {
//Special case where the AdGroup was updated today which means a new test has started.
//Remove the old labels, but keep the champion as the control for the next test
info(‘New test is starting in AdGroup: ‘+ag.getName());
removeLoserLabelsFromAds(ag,isMobile);
continue;
}

//Is there a previous winner? if so we should use it as the control.
var controlAd = checkForPreviousWinner(ag,isMobile);

//Here we order by the Visitors metric and use that as a control if we don’t have one
var adSelector = ag.ads().withCondition(‘Status = ENABLED’).withCondition(‘AdType = TEXT_AD’);
if(!AdWordsApp.getExecutionInfo().isPreview()) {
adSelector = adSelector.withCondition(“LabelNames CONTAINS_NONE [‘”+[LOSER_LABEL,CHAMPION_LABEL].join(“‘,'”)+”‘]”);
}
var adIter = adSelector.forDateRange(lastTouchedDate, todayDate)
.orderBy(VISITORS_METRIC+” DESC”)
.get();
if( (controlAd == null && adIter.totalNumEntities() < 2) ||
(controlAd != null && adIter.totalNumEntities() < 1) )
{
info(‘AdGroup did not have enough eligible Ads. Had: ‘+adIter.totalNumEntities()+’, Needed at least 2′);
continue;
}

if(!controlAd) {
info(‘No control set for AdGroup. Setting one.’);
while(adIter.hasNext()) {
var ad = adIter.next();
if(shouldSkip(isMobile,ad)) { continue; }
controlAd = ad;
break;
}
if(!controlAd) {
continue;
}
applyLabel(controlAd,CHAMPION_LABEL);
}

while(adIter.hasNext()) {
var testAd = adIter.next();
if(shouldSkip(isMobile,testAd)) { continue; }
//The Test object does all the heavy lifting for us.
var test = new Test(controlAd,testAd,
CONFIDENCE_LEVEL,
lastTouchedDate,todayDate,
VISITORS_METRIC,CONVERSIONS_METRIC);
info(‘Control – Visitors: ‘+test.getControlVisitors()+’ Conversions: ‘+test.getControlConversions());
info(‘Test – Visitors: ‘+test.getTestVisitors()+’ Conversions: ‘+test.getTestConversions());
info(‘P-Value: ‘+test.getPValue());

if(test.getControlVisitors() < VISITORS_THRESHOLD ||
test.getTestVisitors() < VISITORS_THRESHOLD)
{
info(‘Not enough visitors in the control or test ad. Skipping.’);
continue;
}

//Check for significance
if(test.isSignificant()) {
var loser = test.getLoser();
removeLabel(loser,CHAMPION_LABEL); //Champion has been dethroned
applyLabel(loser,LOSER_LABEL);

//The winner is the new control. Could be the same as the old one.
controlAd = test.getWinner();
applyLabel(controlAd,CHAMPION_LABEL);

//We store some metrics for a nice email later
if(!ag[‘touchCount’]) {
ag[‘touchCount’] = 0;
touchedAdGroups.push(ag);
}
ag[‘touchCount’]++;
}
}

//Let’s bail if we run out of time so we can send the emails.
if((!AdWordsApp.getExecutionInfo().isPreview() && AdWordsApp.getExecutionInfo().getRemainingTime() < 60) ||
( AdWordsApp.getExecutionInfo().isPreview() && AdWordsApp.getExecutionInfo().getRemainingTime() < 10) )
{
finishedEarly = true;
break;
}
}
}
if(touchedAdGroups.length > 0) {
sendMailForTouchedAdGroups(touchedAdGroups,finishedEarly);
}
beacon();
}

// A helper function to return the list of campaign ids with a label for filtering
function getCampaignNames() {
var campNames = [];
var labelIter = AdWordsApp.labels().withCondition(“Name = ‘”+CAMPAIGN_LABEL+”‘”).get();
if(labelIter.hasNext()) {
var label = labelIter.next();
var campIter = label.campaigns().get();
while(campIter.hasNext()) {
campNames.push(campIter.next().getName());
}
}
return campNames;
}

function applyLabel(entity,label) {
if(!AdWordsApp.getExecutionInfo().isPreview()) {
entity.applyLabel(label);
} else {
var adText = (entity.getEntityType() === ‘Ad’) ? [entity.getHeadline(),entity.getDescription1(),
entity.getDescription2(),entity.getDisplayUrl()].join(‘ ‘)
: entity.getName();
Logger.log(‘PREVIEW: Would have applied label: ‘+label+’ to Entity: ‘+ adText);
}
}

function removeLabel(ad,label) {
if(!AdWordsApp.getExecutionInfo().isPreview()) {
ad.removeLabel(label);
} else {
var adText = [ad.getHeadline(),ad.getDescription1(),ad.getDescription2(),ad.getDisplayUrl()].join(‘ ‘);
Logger.log(‘PREVIEW: Would have removed label: ‘+label+’ from Ad: ‘+ adText);
}
}

// This function checks if the AdGroup has an Ad with a Champion Label
// If so, the new test should use that as the control.
function checkForPreviousWinner(ag,isMobile) {
var adSelector = ag.ads().withCondition(‘Status = ENABLED’)
.withCondition(‘AdType = TEXT_AD’);
if(!AdWordsApp.getExecutionInfo().isPreview()) {
adSelector = adSelector.withCondition(“LabelNames CONTAINS_ANY [‘”+CHAMPION_LABEL+”‘]”);
}
var adIter = adSelector.get();
while(adIter.hasNext()) {
var ad = adIter.next();
if(shouldSkip(isMobile,ad)) { continue; }
info(‘Found a previous winner. Using it as the control.’);
return ad;
}
return null;
}

function shouldSkip(isMobile,ad) {
if(isMobile) {
if(!ad.isMobilePreferred()) {
return true;
}
} else {
if(ad.isMobilePreferred()) {
return true;
}
}
return false;
}

// This function sends the email to the people in the TO array.
// If the script finishes early, it adds a notice to the email.
function sendMailForTouchedAdGroups(ags,finishedEarly) {
var htmlBody = ‘<html><head></head><body>’;
if(finishedEarly) {
htmlBody += ‘The script was not able to check all AdGroups. ‘ +
‘It will check additional AdGroups on the next run.<br / >’ ;
}
htmlBody += ‘The following AdGroups have one or more creative tests that have finished.’ ;
htmlBody += buildHtmlTable(ags);
htmlBody += ‘<p><small>Generated by <a href=”http://www.freeadwordsscripts.com”>FreeAdWordsScripts.com</a></small></p>’ ;
htmlBody += ‘</body></html>’;
var options = {
htmlBody : htmlBody,
};
var subject = ags.length + ‘ Creative Test(s) Completed – ‘ +
Utilities.formatDate(new Date(), AdWordsApp.currentAccount().getTimeZone(), ‘yyyy-MM-dd’);
for(var i in TO) {
MailApp.sendEmail(TO[i], subject, ags.length+’ AdGroup(s) have creative tests that have finished.’, options);
}
}

// This function uses my HTMLTable object to build the styled html table for the email.
function buildHtmlTable(ags) {
var table = new HTMLTable();
//CSS from: http://coding.smashingmagazine.com/2008/08/13/top-10-css-table-designs/
//Inlined using: http://inlinestyler.torchboxapps.com/
table.setTableStyle([‘font-family: “Lucida Sans Unicode”,”Lucida Grande”,Sans-Serif;’,
‘font-size: 12px;’,
‘background: #fff;’,
‘margin: 45px;’,
‘width: 480px;’,
‘border-collapse: collapse;’,
‘text-align: left’].join(”));
table.setHeaderStyle([‘font-size: 14px;’,
‘font-weight: normal;’,
‘color: #039;’,
‘padding: 10px 8px;’,
‘border-bottom: 2px solid #6678b1’].join(”));
table.setCellStyle([‘border-bottom: 1px solid #ccc;’,
‘padding: 4px 6px’].join(”));
table.addHeaderColumn(‘#’);
table.addHeaderColumn(‘Campaign Name’);
table.addHeaderColumn(‘AdGroup Name’);
table.addHeaderColumn(‘Tests Completed’);
for(var i in ags) {
table.newRow();
table.addCell(table.getRowCount());
var campName = ags[i].getCampaign().getName();
var name = ags[i].getName();
var touchCount = ags[i][‘touchCount’];
var campLink, agLink;
if(__c !== ” && __u !== ”) { // You should really set these.
campLink = getUrl(ags[i].getCampaign(),’Ad groups’);
agLink = getUrl(ags[i],’Ads’);
table.addCell(a(campLink,campName));
table.addCell(a(agLink,name));
} else {
table.addCell(campName);
table.addCell(name);
}
table.addCell(touchCount,’text-align: right’);
}
return table.toString();
}

// Just a helper to build the html for a link.
function a(link,val) {
return ‘<a href=”‘+link+'”>’+val+'</a>’;
}

// This function finds all the previous losers and removes their label.
// It is used when the script detects a change in the AdGroup and needs to
// start a new test.
function removeLoserLabelsFromAds(ag,isMobile) {
var adSelector = ag.ads().withCondition(‘Status = ENABLED’);
if(!AdWordsApp.getExecutionInfo().isPreview()) {
adSelector = adSelector.withCondition(“LabelNames CONTAINS_ANY [‘”+LOSER_LABEL+”‘]”);
}
var adIter = adSelector.get();
while(adIter.hasNext()) {
var ad = adIter.next();
if(shouldSkip(isMobile,ad)) { continue; }
removeLabel(ad,LOSER_LABEL);
}
}

// A helper function to create a new label if it doesn’t exist in the account.
function createLabelIfNeeded(name,color) {
if(!AdWordsApp.labels().withCondition(“Name = ‘”+name+”‘”).get().hasNext()) {
info(‘Creating label: “‘+name+'”‘);
AdWordsApp.createLabel(name,””,color);
} else {
info(‘Label: “‘+name+'” already exists.’);
}
}

// This function compares the previous and current Ad maps and
// updates the current map with the date that the AdGroup was last touched.
// If OVERRIDE_LAST_TOUCHED_DATE is set and there is no previous data for the
// AdGroup, it uses that as the last touched date.
function updateCurrentAdMap(current,previous) {
info(‘Updating the current Ads map using historical snapshot.’);
for(var rowKey in current) {
var currentAds = current[rowKey].adIds;
var previousAds = (previous[rowKey]) ? previous[rowKey].adIds : [];
if(currentAds.join(‘-‘) === previousAds.join(‘-‘)) {
current[rowKey].lastTouched = previous[rowKey].lastTouched;
}
if(previousAds.length === 0 && OVERRIDE_LAST_TOUCHED_DATE !== ”) {
current[rowKey].lastTouched = new Date(OVERRIDE_LAST_TOUCHED_DATE);
}
//if we make it here without going into the above if statements
//then the adgroup has changed and we should keep the new date
}
info(‘Finished updating the current Ad map.’);
return current;
}

// This stores the Ad map snapshot to a file so it can be used for the next run.
// The data is stored as a JSON string for easy reading later.
function storeAdsSnapshot(data) {
info(‘Storing the Ads snapshot to Google Drive.’);
var fileName = getSnapshotFilename();
var file = DriveApp.getFilesByName(fileName).next();
file.setContent(Utilities.jsonStringify(data));
info(‘Finished.’);
}

// This reads the JSON formatted previous snapshot from a file on GDrive
// If the file doesn’t exist, it creates a new one and returns an empty map.
function getPreviousAdsSnapshot() {
info(‘Loading the previous Ads snapshot from Google Drive.’);
var fileName = getSnapshotFilename();
var fileIter = DriveApp.getFilesByName(fileName);
if(fileIter.hasNext()) {
return Utilities.jsonParse(fileIter.next().getBlob().getDataAsString());
} else {
DriveApp.createFile(fileName, ”);
return {};
}
}

// A helper function to build the filename for the snapshot.
function getSnapshotFilename() {
var accountId = AdWordsApp.currentAccount().getCustomerId();
return (accountId + ‘ Ad Testing Script Snapshot.json’);
}

// This function pulls the Ad Performance Report which is the fastest
// way to build a snapshot of the current ads in the account.
// This only pulls in active text ads.
function getCurrentAdsSnapshot() {
info(‘Running Ad Performance Report to get current Ads snapshot.’);
var OPTIONS = { includeZeroImpressions : true };
var cols = [‘CampaignId’,’AdGroupId’,’Id’,’DevicePreference’,’Impressions’];
var report = ‘AD_PERFORMANCE_REPORT’;
var query = [‘select’,cols.join(‘,’),’from’,report,
‘where AdType = TEXT_AD’,
‘and AdNetworkType1 = SEARCH’,
‘and CampaignStatus = ENABLED’,
‘and AdGroupStatus = ENABLED’,
‘and Status = ENABLED’,
‘during’,’TODAY’].join(‘ ‘);
var results = {}; // { campId-agId : row, … }
var reportIter = AdWordsApp.report(query, OPTIONS).rows();
while(reportIter.hasNext()) {
var row = reportIter.next();
var rowKey = [row.CampaignId,row.AdGroupId].join(‘-‘);
if(ENABLE_MOBILE_AD_TESTING && row.DevicePreference == 30001) {
rowKey += ‘-Mobile’;
}
if(!results[rowKey]) {
results[rowKey] = { adIds : [], lastTouched : new Date() };
}
results[rowKey].adIds.push(row.Id);
}
for(var i in results) {
results[i].adIds.sort();
}
info(‘Finished building the current Ad map.’);
return results;
}

//Helper function to format the date
function getDateString(date,format) {
return Utilities.formatDate(new Date(date),AdWordsApp.currentAccount().getTimeZone(),format);
}

// Function to build out the urls for deeplinking into the AdWords account.
// For this to work, you need to have __c and __u filled in.
// Taken from: http://www.freeadwordsscripts.com/2013/11/building-entity-deep-links-with-adwords.html
function getUrl(entity,tab) {
var customerId = __c;
var effectiveUserId = __u;
var decodedTab = getTab(tab);

var base = ‘https://adwords.google.com/cm/CampaignMgmt?’;
var url = base+’__c=’+customerId+’&__u=’+effectiveUserId+’#’;

if(typeof entity[‘getEntityType’] === ‘undefined’) {
return url+’r.ONLINE.di&app=cm’;
}

var type = entity.getEntityType()
if(type === ‘Campaign’) {
return url+’c.’+entity.getId()+’.’+decodedTab+’&app=cm’;
}
if(type === ‘AdGroup’) {
return url+’a.’+entity.getId()+’_’+entity.getCampaign().getId()+’.’+decodedTab+’&app=cm’;
}
if(type === ‘Keyword’) {
return url+’a.’+entity.getAdGroup().getId()+’_’+entity.getCampaign().getId()+’.key&app=cm’;
}
if(type === ‘Ad’) {
return url+’a.’+entity.getAdGroup().getId()+’_’+entity.getCampaign().getId()+’.create&app=cm’;
}
return url+’r.ONLINE.di&app=cm’;

function getTab(tab) {
var mapping = {
‘Ad groups’:’ag’,’Settings:All settings’:’st_sum’,
‘Settings:Locations’:’st_loc’,’Settings:Ad schedule’:’st_as’,
‘Settings:Devices’:’st_p’,’Ads’:’create’,
‘Keywords’:’key’,’Audiences’:’au’,’Ad extensions’:’ae’,
‘Auto targets’:’at’,’Dimensions’ : ‘di’
};
if(mapping[tab]) { return mapping[tab]; }
return ‘key’; //default to keyword tab
}
}

// Helper function to print info logs
function info(msg) {
if(EXTRA_LOGS) {
Logger.log(‘INFO: ‘+msg);
}
}

// Helper function to print more serious warnings
function warn(msg) {
Logger.log(‘WARNING: ‘+msg);
}

/********************************
* Track Script Runs in Google Analytics
* Created By: Russ Savage
* FreeAdWordsScripts.com
********************************/
function beacon() {
var TAG_ID = ‘UA-40187672-2’;
var CAMPAIGN_SOURCE = ‘adwords’;
var CAMPAIGN_MEDIUM = ‘scripts’;
var CAMPAIGN_NAME = ‘AdTestingScriptV2_1’;
var HOSTNAME = ‘www.freeadwordsscripts.com’;
var PAGE = ‘/Ad_Testing_Script_v2_1’;
if(AdWordsApp.getExecutionInfo().isPreview()) {
PAGE += ‘/preview’;
}
var DOMAIN_LINK = ‘http://’+HOSTNAME+PAGE;

//Pulled from: http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
var uuid = ‘xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx’.replace(/[xy]/g,
function(c) {var r = Math.random()*16|0,v=c==’x’?r:r&0x3|0x8;return v.toString(16);});

var url = ‘http://www.google-analytics.com/collect?’;
var payload = {
‘v’:1,’tid’:TAG_ID,’cid’:uuid,
‘t’:’pageview’,’cs’:CAMPAIGN_SOURCE,’cm’:CAMPAIGN_MEDIUM,’cn’:CAMPAIGN_NAME,
‘dl’:DOMAIN_LINK
};
var qs = ”;
for(var key in payload) {
qs += key + ‘=’ + encodeURIComponent(payload[key]) + ‘&’;
}
url += qs.substring(0,qs.length-1);
UrlFetchApp.fetch(url);
}

/*********************************************
* Test: A class for runnning A/B Tests for Ads
* Version 1.0
* Based on VisualWebsiteOptimizer logic: http://goo.gl/jiImn
* Russ Savage
* FreeAdWordsScripts.com
**********************************************/
// A description of the parameters:
// control – the control Ad, test – the test Ad
// startDate, endDate – the start and end dates for the test
// visitorMetric, conversionMetric – the components of the metric to use for the test
function Test(control,test,desiredConf,startDate,endDate,visitorMetric,conversionMetric) {
this.desiredConfidence = desiredConf/100;
this.verMetric = visitorMetric;
this.conMetric = conversionMetric;
this.startDate = startDate;
this.endDate = endDate;
this.winner;

this.controlAd = control;
this.controlStats = (this.controlAd[‘stats’]) ? this.controlAd[‘stats’] : this.controlAd.getStatsFor(this.startDate, this.endDate);
this.controlAd[‘stats’] = this.controlStats;
this.controlVisitors = this.controlStats[‘get’+this.verMetric]();
this.controlConversions = this.controlStats[‘get’+this.conMetric]();
this.controlCR = getConversionRate(this.controlVisitors,this.controlConversions);

this.testAd = test;
this.testStats = (this.testAd[‘stats’]) ? this.testAd[‘stats’] : this.testAd.getStatsFor(this.startDate, this.endDate);
this.testAd[‘stats’] = this.testStats;
this.testVisitors = this.testStats[‘get’+this.verMetric]();
this.testConversions = this.testStats[‘get’+this.conMetric]();
this.testCR = getConversionRate(this.testVisitors,this.testConversions);

this.pValue;

this.getControlVisitors = function() { return this.controlVisitors; }
this.getControlConversions = function() { return this.controlConversions; }
this.getTestVisitors = function() { return this.testVisitors; }
this.getTestConversions = function() { return this.testConversions; }

// Returns the P-Value for the two Ads
this.getPValue = function() {
if(!this.pValue) {
this.pValue = calculatePValue(this);
}
return this.pValue;
};

// Determines if the test has hit significance
this.isSignificant = function() {
var pValue = this.getPValue();
if(pValue && pValue !== ‘N/A’ && (pValue >= this.desiredConfidence || pValue <= (1 – this.desiredConfidence))) {
return true;
}
return false;
}

// Returns the winning Ad
this.getWinner = function() {
if(this.decideWinner() === ‘control’) {
return this.controlAd;
}
if(this.decideWinner() === ‘challenger’) {
return this.testAd;
}
return null;
};

// Returns the losing Ad
this.getLoser = function() {
if(this.decideWinner() === ‘control’) {
return this.testAd;
}
if(this.decideWinner() === ‘challenger’) {
return this.controlAd;
}
return null;
};

// Determines if the control or the challenger won
this.decideWinner = function () {
if(this.winner) {
return this.winner;
}
if(this.isSignificant()) {
if(this.controlCR >= this.testCR) {
this.winner = ‘control’;
} else {
this.winner = ‘challenger’;
}
} else {
this.winner = ‘no winner’;
}
return this.winner;
}

// This function returns the confidence level for the test
function calculatePValue(instance) {
var control = {
visitors: instance.controlVisitors,
conversions: instance.controlConversions,
cr: instance.controlCR
};
var challenger = {
visitors: instance.testVisitors,
conversions: instance.testConversions,
cr: instance.testCR
};
var z = getZScore(control,challenger);
if(z == -1) { return ‘N/A’; }
var norm = normSDist(z);
return norm;
}

// A helper function to make rounding a little easier
function round(value) {
var decimals = Math.pow(10,5);
return Math.round(value*decimals)/decimals;
}

// Return the conversion rate for the test
function getConversionRate(visitors,conversions) {
if(visitors == 0) {
return -1;
}
return conversions/visitors;
}

function getStandardError(cr,visitors) {
if(visitors == 0) {
throw ‘Visitors cannot be 0.’;
}
return Math.sqrt((cr*(1-cr)/visitors));
}

function getZScore(c,t) {
try {
if(!c[‘se’]) { c[‘se’] = getStandardError(c.cr,c.visitors); }
if(!t[‘se’]) { t[‘se’] = getStandardError(t.cr,t.visitors); }
} catch(e) {
Logger.log(e);
return -1;
}

if((Math.sqrt(Math.pow(c.se,2)+Math.pow(t.se,2))) == 0) {
Logger.log(‘WARNING: Somehow the denominator in the Z-Score calulator was 0.’);
return -1;
}
return ((c.cr-t.cr)/Math.sqrt(Math.pow(c.se,2)+Math.pow(t.se,2)));
}

//From: http://www.codeproject.com/Articles/408214/Excel-Function-NORMSDIST-z
function normSDist(z) {
var sign = 1.0;
if (z < 0) { sign = -1; }
return round(0.5 * (1.0 + sign * erf(Math.abs(z)/Math.sqrt(2))));
}

// From: http://picomath.org/javascript/erf.js.html
function erf(x) {
// constants
var a1 = 0.254829592;
var a2 = -0.284496736;
var a3 = 1.421413741;
var a4 = -1.453152027;
var a5 = 1.061405429;
var p = 0.3275911;

// Save the sign of x
var sign = 1;
if (x < 0) {
sign = -1;
}
x = Math.abs(x);

// A&S formula 7.1.26
var t = 1.0/(1.0 + p*x);
var y = 1.0 – (((((a5*t + a4)*t) + a3)*t + a2)*t + a1)*t*Math.exp(-x*x);

return sign*y;
}
}

/*********************************************
* HTMLTable: A class for building HTML Tables
* Version 1.0
* Russ Savage
* FreeAdWordsScripts.com
**********************************************/
function HTMLTable() {
this.headers = [];
this.columnStyle = {};
this.body = [];
this.currentRow = 0;
this.tableStyle;
this.headerStyle;
this.cellStyle;

this.addHeaderColumn = function(text) {
this.headers.push(text);
};

this.addCell = function(text,style) {
if(!this.body[this.currentRow]) {
this.body[this.currentRow] = [];
}
this.body[this.currentRow].push({ val:text, style:(style) ? style : ” });
};

this.newRow = function() {
if(this.body != []) {
this.currentRow++;
}
};

this.getRowCount = function() {
return this.currentRow;
};

this.setTableStyle = function(css) {
this.tableStyle = css;
};

this.setHeaderStyle = function(css) {
this.headerStyle = css;
};

this.setCellStyle = function(css) {
this.cellStyle = css;
if(css[css.length-1] !== ‘;’) {
this.cellStyle += ‘;’;
}
};

this.toString = function() {
var retVal = ‘<table ‘;
if(this.tableStyle) {
retVal += ‘style=”‘+this.tableStyle+'”‘;
}
retVal += ‘>’+_getTableHead(this)+_getTableBody(this)+'</table>’;
return retVal;
};

function _getTableHead(instance) {
var headerRow = ”;
for(var i in instance.headers) {
headerRow += _th(instance,instance.headers[i]);
}
return ‘<thead><tr>’+headerRow+'</tr></thead>’;
};

function _getTableBody(instance) {
var retVal = ‘<tbody>’;
for(var r in instance.body) {
var rowHtml = ‘<tr>’;
for(var c in instance.body[r]) {
rowHtml += _td(instance,instance.body[r][c]);
}
rowHtml += ‘</tr>’;
retVal += rowHtml;
}
retVal += ‘</tbody>’;
return retVal;
};

function _th(instance,val) {
var retVal = ‘<th scope=”col” ‘;
if(instance.headerStyle) {
retVal += ‘style=”‘+instance.headerStyle+'”‘;
}
retVal += ‘>’+val+'</th>’;
return retVal;
};

function _td(instance,cell) {
var retVal = ‘<td ‘;
if(instance.cellStyle || cell.style) {
retVal += ‘style=”‘;
if(instance.cellStyle) {
retVal += instance.cellStyle;
}
if(cell.style) {
retVal += cell.style;
}
retVal += ‘”‘;
}
retVal += ‘>’+cell.val+'</td>’;
return retVal;
};
}