Created
October 1, 2018 07:01
-
-
Save dustinrecko/3f356dfcf17ba12c4cd306779387d370 to your computer and use it in GitHub Desktop.
OMR | Performance Monitoring in Slack via Labels
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* AdWords Performance Monitoring in Slack via Labels | |
* @author: Dustin Recko | |
* | |
*/ | |
// Config Section //> | |
var DB_URL = 'https://...'; // The Firebase Database URL | |
var DB_AUTH = 'xxx'; // The Firebase Database Secret | |
var SLACK_HOOK = 'https://...'; // The Slack Hook URL | |
var SLACK_CHANNEL = '#adwords'; // The Slack Channel | |
var SLACK_EMOJI = ':smile:'; // The Slack Emoji | |
var TRIGGER = { | |
clicks: 30, //Stats as a trigger with a specified threshold | |
weeks: [1,4] //Time as a trigger with a start and end threshold | |
}; | |
var LABEL_NAME = "CONTROL"; // The name of the label used in AdWords to activate monitoring for Keywords, AdGroups, and Ads | |
var NOW = new Date(); | |
// End of Config <// | |
function main() { | |
init(); | |
var ACC = AdWordsApp.currentAccount().getName().split(" ")[0]; | |
var myDb = new firebase(DB_URL,DB_AUTH); | |
var myDbJson = myDb.getJson(LABEL_NAME+'/'+ACC) || nest({},[LABEL_NAME,ACC]); | |
var mySlack = new slack(SLACK_HOOK,SLACK_CHANNEL,SLACK_EMOJI); | |
var myLabel = AdWordsApp.labels().withCondition("Name = '"+LABEL_NAME+"'").get().next(); | |
var process = { | |
"keywords": myLabel.keywords().get(), | |
"adGroups": myLabel.adGroups().get(), | |
"ads": myLabel.ads().get() | |
}; | |
/// Main process | |
for(var handler in process) { | |
/// Check items with the label | |
while(process[handler].hasNext()) { | |
var h = process[handler].next(); | |
if(!myDbJson[handler] || !myDbJson[handler][h.getId()]) { | |
var obj = { | |
"name" : (h.getText instanceof Function) ? h.getText() : ((h.getDescription1 instanceof Function) ? h.getDescription1() : h.getName()), | |
"campaign": h.getCampaign().getName(), | |
"qsStart" : (h.getQualityScore instanceof Function) ? h.getQualityScore() : 0, | |
"started" : NOW.getTime(), | |
"trigger": initTrigger(TRIGGER) | |
}; | |
myDb.patch(obj,LABEL_NAME+'/'+ACC+'/'+handler+'/'+h.getId()); | |
myDbJson = nest(myDbJson,[handler,h.getId()]); | |
} else { | |
for(var i in TRIGGER) { | |
switch(typeof(myDbJson[handler][h.getId()].trigger[i])) { | |
case "boolean": | |
if(!myDbJson[handler][h.getId()].trigger[i]) { | |
if(statsBasedCheck(handler,h,i,myDbJson[handler][h.getId()])) { | |
var status = {}; | |
status[i] = true; | |
myDb.patch(status,LABEL_NAME+'/'+ACC+'/'+handler+'/'+h.getId()+'/trigger'); | |
} | |
} | |
break; | |
case "number": | |
var weeksPassed = Math.round((NOW.getTime() - myDbJson[handler][h.getId()].started)/(1000*60*60*24)) / 7; | |
if(weeksPassed%1 === 0 && TRIGGER[i][0] <= weeksPassed && weeksPassed <= TRIGGER[i][1] && weeksPassed > myDbJson[handler][h.getId()].trigger[i]) { | |
timeBasedCheck(handler,h,i,myDbJson[handler][h.getId()]); | |
var status = {}; | |
status[i] = weeksPassed; | |
myDb.patch(status,LABEL_NAME+'/'+ACC+'/'+handler+'/'+h.getId()+'/trigger'); | |
} | |
break; | |
} | |
} | |
} | |
/// Flag processed items | |
myDbJson[handler][h.getId()].flag = true; | |
} | |
/// Cleanup no longer labelled items, i.e., non-flagged | |
for(var i in myDbJson[handler]) { | |
if(myDbJson[handler][i].flag == undefined) | |
myDb.purge(LABEL_NAME+'/'+ACC+'/'+handler+'/'+i); | |
} | |
} | |
/// Some functions | |
function init() { | |
Date.prototype.yyyymmdd = function(days) { | |
if(days) { | |
this.setDate(this.getDate() + days); | |
} | |
return Utilities.formatDate(this, AdWordsApp.currentAccount().getTimeZone(),'yyyyMMdd'); | |
} | |
} | |
function firebase(db,auth) { | |
this.db = db; | |
this.auth = auth; | |
this.patch = function(payload,path) { | |
path = path+'/.json?auth='; | |
var options = { | |
"method" : "patch", | |
"payload" : JSON.stringify( payload ) | |
}; | |
UrlFetchApp.fetch(this.db+path+this.auth,options); | |
} | |
this.purge = function(path) { | |
path = path+'/.json?auth='; | |
var options = { | |
"method": "delete" | |
}; | |
UrlFetchApp.fetch(this.db+path+this.auth,options); | |
} | |
this.getJson = function(path) { | |
path = path+'/.json?auth='; | |
return JSON.parse( | |
UrlFetchApp | |
.fetch(this.db+path+this.auth) | |
.getContentText() | |
); | |
} | |
} | |
function slack(hook,channel,emoji) { | |
this.hook = hook; | |
this.channel = channel; | |
this.emoji = emoji; | |
this.msg = function(payload) { | |
payload.channel = this.channel; | |
payload.icon_emoji = this.emoji; | |
var options = { | |
method: "POST", | |
contentType: 'application/json', | |
payload: JSON.stringify(payload) | |
}; | |
UrlFetchApp.fetch(this.hook,options); | |
} | |
} | |
function initTrigger() { | |
var obj = {}; | |
for(var i in TRIGGER) { | |
if(typeof(TRIGGER[i]) == "number") | |
obj[i] = false | |
else | |
obj[i] = 0 | |
} | |
return obj; | |
} | |
function statsBasedCheck(type,handler,trigger,dbData) { | |
var sh = handler.getStatsFor(new Date(dbData.started).yyyymmdd(),NOW.yyyymmdd()); | |
var stats = { | |
avgCpc: sh.getAverageCpc().toFixed(2), | |
avgPos: sh.getAveragePosition(), | |
clicks: sh.getClicks(), | |
conversions: sh.getConversions(), | |
cost: sh.getCost(), | |
qs: (type == 'keywords') ? handler.getQualityScore() : 0 | |
}; | |
if(stats[trigger] >= TRIGGER[trigger]) { | |
var daysPassed = Math.round((NOW.getTime() - dbData.started)/(1000*60*60*24)); | |
var sh = handler.getStatsFor(new Date(dbData.started).yyyymmdd(daysPassed*-1),new Date(dbData.started).yyyymmdd()); | |
var beforeStats = { | |
avgCpc: sh.getAverageCpc().toFixed(2), | |
avgPos: sh.getAveragePosition(), | |
clicks: sh.getClicks(), | |
conversions: sh.getConversions(), | |
cost: sh.getCost(), | |
qs: dbData.qsStart | |
}; | |
var attachments = []; | |
for(var s in stats) { | |
attachments.push({ | |
title: s, | |
text: 'is '+stats[s]+ (stats[s]>=beforeStats[s] ? ' (up' : '(down') +' from '+beforeStats[s]+')' | |
}); | |
} | |
mySlack.msg({ | |
text: LABEL_NAME+" > "+ACC+" > "+type+" > "+dbData.name+" in "+dbData.campaign+" passed "+stats[trigger]+" "+trigger+" in "+daysPassed+" days (was "+beforeStats[trigger]+" in the previous period)", | |
attachments: attachments | |
}); | |
return true; | |
} | |
return false; | |
} | |
function timeBasedCheck(type,handler,trigger,dbData) { | |
var sh = handler.getStatsFor(new Date(dbData.started).yyyymmdd(),NOW.yyyymmdd()); | |
var stats = { | |
avgCpc: sh.getAverageCpc().toFixed(2), | |
avgPos: sh.getAveragePosition(), | |
clicks: sh.getClicks(), | |
conversions: sh.getConversions(), | |
cost: sh.getCost(), | |
qs: (type == 'keywords') ? handler.getQualityScore() : 0 | |
}; | |
var sh = handler.getStatsFor(new Date(dbData.started).yyyymmdd((dbData.trigger[trigger]+1)*-7),new Date(dbData.started).yyyymmdd()); | |
var beforeStats = { | |
avgCpc: sh.getAverageCpc().toFixed(2), | |
avgPos: sh.getAveragePosition(), | |
clicks: sh.getClicks(), | |
conversions: sh.getConversions(), | |
cost: sh.getCost(), | |
qs: dbData.qsStart | |
}; | |
var attachments = []; | |
for(var s in stats) { | |
attachments.push({ | |
title: s, | |
text: 'is '+stats[s]+ (stats[s]>=beforeStats[s] ? ' (up' : '(down') +' from '+beforeStats[s]+')' | |
}); | |
} | |
mySlack.msg({ | |
text: LABEL_NAME+" > "+ACC+" > "+type+" > "+dbData.name+" in "+dbData.campaign+" passed "+(dbData.trigger[trigger]+1)+" "+trigger, | |
attachments: attachments | |
}); | |
} | |
// by Jason Knight | |
function nest(base,arr) { | |
for (var obj = base, ptr = obj, i = 0, j = arr.length; i < j; i++) | |
ptr = (ptr[arr[i]] = {}); | |
return obj; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment