Created
July 30, 2018 21:14
-
-
Save thomaszurkan-optimizely/a55813fdad3eade5d45b0bc426b5ee17 to your computer and use it in GitHub Desktop.
Using AWS Lambda with Optimizely Web hooks for Project Management
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
'use strict'; | |
console.log('Loading function'); | |
const http = require('https'); | |
const doc = require('dynamodb-doc'); | |
const dynamo = new doc.DynamoDB(); | |
const crypto = require('crypto'); | |
const TABLENAME = "OptimizelyProfile"; | |
const KEY1 = "version"; | |
const KEY2 = "projectId"; | |
const FIELD = "projectJson" | |
var diff = []; | |
var topLevelJSON = null; | |
/** | |
* Demonstrates a simple HTTP endpoint using API Gateway. You have full | |
* access to the request and response payload, including headers and | |
* status code. | |
* | |
* This will accept a HTTPS webhook from optimizely, check against the secret key, and compare | |
* the datafile copy in a local dynamodb instance with the newer version associated with the webhook. | |
* It then publishes those diffences to a slack channel webhook. | |
*/ | |
exports.handler = (event, context, callback) => { | |
const hmac = crypto.createHmac('sha1', process.env.SECRET_KEY); | |
//console.log('Received event:', JSON.stringify(event, null, 2)); | |
const done = (err, res) => callback(null, { | |
statusCode: err ? '400' : '200', | |
body: err ? err.message : JSON.stringify(res), | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
}); | |
switch (event.httpMethod) { | |
case 'POST': | |
let body = JSON.parse(event.body); | |
let projectId = body['project_id']; | |
let url = body['data']['cdn_url']; | |
let hubHeader = event.headers['X-Hub-Signature']; | |
hmac.update(event.body); | |
let secretCompare = 'sha1=' + hmac.digest('hex'); | |
if (secretCompare != hubHeader) { | |
done(new Error(`Not trusted`)); | |
break; | |
} | |
var text = 'Fullstack e2e project file has been updated by someone on your team'; | |
profilefileExists((val)=> { | |
loadDatafile(url, (err, datafile) => { | |
if (err) { | |
console.log('problem with datafile'); | |
console.log(err.messge); | |
} | |
else { | |
loadProfilefile(projectId, (err, profile) => { | |
if (err){ | |
console.log('error loading profile file') | |
console.log(err.message); | |
console.log(err.stack); | |
saveProfilefile(projectId, datafile, (err, data) => { | |
if (err) { | |
console.log('error saving profile'); | |
console.log(err.message); | |
} | |
else { | |
console.log('profile was saved'); | |
} | |
var postData = JSON.stringify({ | |
"text": text | |
}); | |
sendWebhook(postData, done); | |
}); | |
} | |
else { | |
var title = 'Fullstack e2e project file has been updated by someone on your team\n'; | |
var szDiff = '' | |
if (Object.getOwnPropertyNames(profile).length > 0) { | |
diff = []; | |
topLevelJSON = datafile; | |
diffDic(datafile, profile); | |
if (diff.lenth == 0) { | |
szDiff = title + 'No difference'; | |
} | |
else { | |
szDiff = title; | |
var next = diff.pop(); | |
while (next != null) { | |
szDiff += next + '\n'; | |
next = diff.pop(); | |
} | |
} | |
} | |
else { | |
szDiff = title + 'No difference'; | |
} | |
updateProfilefile(projectId, datafile, (err, data) => { | |
if (err) { | |
console.log('error updating profile'); | |
console.log(err.message); | |
} | |
else { | |
console.log('profile was updated'); | |
} | |
var postData = JSON.stringify({ | |
"text": szDiff | |
}); | |
sendWebhook(postData, done); | |
}); | |
} | |
}); | |
} | |
}); | |
}); | |
break; | |
default: | |
done(new Error(`Unsupported method "${event.httpMethod}"`)); | |
} | |
}; | |
/** | |
* find if two dictionaries are different | |
* | |
**/ | |
function diffDic(current, past) { | |
var isDifferent = false; | |
for (let key in current) { | |
const value = current[key]; | |
if (Array.isArray(value) && (past.hasOwnProperty(key) && Array.isArray(past[key]))) { | |
if (diffArray(value, past[key])) { | |
isDifferent = true; | |
switch (key) { | |
case 'trafficAllocation': | |
var ids = current['audienceIds']; | |
ids.forEach((n) => { | |
topLevelJSON['audiences'].forEach((a) => { | |
if (a["id"] == n) { | |
diff.push( key + ' for ' + a['name'] + ' has changed'); | |
} | |
}); | |
}); | |
break; | |
case 'experiments': | |
var id = current['id']; | |
var found = false; | |
topLevelJSON['featureFlags'].forEach((n) => { | |
if (n['rolloutId'] == id) { | |
diff.push('rollout' + ' for ' + n['key'] + ' has changed'); | |
found = true; | |
} | |
}); | |
if (!found) { | |
diff.push(key + ' has changed'); | |
} | |
break; | |
default: | |
diff.push(key + ' has changed'); | |
break; | |
} | |
} | |
} | |
else if (typeof(value) == 'object' && !Array.isArray(value) && | |
typeof(past[key]) == 'object' && !Array.isArray(past[key])) { | |
if (diffDic(value, past[key])) { | |
diff.push(key + ' has changed'); | |
isDifferent = true; | |
} | |
} | |
else { | |
if (value != past[key]) { | |
diff.push(key + ' has changed from ' + past[key] + ' to ' + value); | |
isDifferent = true; | |
} | |
} | |
} | |
return isDifferent; | |
} | |
/** | |
* find if two json arrays are different | |
*/ | |
function diffArray(current, past) { | |
var isDifferent = false; | |
for (let i = 0; i < current.length; i++) { | |
if (i < past.length) { | |
if (typeof(current[i]) == 'object' && Array.isArray(current[i]) && | |
typeof(past) == 'object' && Array.isArray(past[i])) { | |
if (diffArray(current[i], past[i])) { | |
isDifferent = true; | |
} | |
} | |
else if (typeof(current[i]) == 'object' && !Array.isArray(current[i]) && | |
typeof(past) == 'object' && !Array.isArray(past[i])) { | |
if (diffDic(current[i], past[i])) { | |
if (current[i].hasOwnProperty('name')) { | |
diff.push(current[i]['name'] + ' has changed.'); | |
} | |
else if (current[i].hasOwnProperty('key')) { | |
diff.push(current[i]['key'] + ' has changed.'); | |
} | |
isDifferent = true; | |
} | |
} | |
else { | |
if (current[i] != past[i]) { | |
diff.push('changed value from ' + past[i] +' to ' + current[i]); | |
isDifferent = true; | |
} | |
} | |
} | |
else { | |
isDifferent = true; | |
} | |
} | |
return isDifferent; | |
} | |
/** | |
* sendWebhook - this sends the webhook to slack. | |
* @param data - to send | |
* @param done - callback when done. | |
* */ | |
function sendWebhook(data, done) { | |
const options = { | |
hostname: 'hooks.slack.com', | |
protocol: 'https:', | |
//port: 80, | |
path: '/services/YOUR_KEY_HERE', | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json' | |
} | |
}; | |
const req = http.request(options, (res) => { | |
res.setEncoding('utf8'); | |
res.on('data', (chunk) => { | |
console.log(`BODY: ${chunk}`); | |
}); | |
res.on('end', () => { | |
console.log('No more data in response.'); | |
}); | |
}); | |
req.on('error', (e) => { | |
console.error(`problem with request: ${e.message}`); | |
}); | |
req.write(data); | |
req.end(); | |
done(null, {'result' :'200'}) | |
} | |
/** | |
* Create the DynamoDB table with the partition and sort keys. | |
* | |
**/ | |
function createTable(done) { | |
var params = { | |
TableName : TABLENAME, | |
KeySchema: [ | |
{ AttributeName: KEY1, KeyType: "HASH"}, | |
{ AttributeName: KEY2, KeyType: "RANGE"} | |
], | |
AttributeDefinitions: [ | |
{ AttributeName: KEY1, AttributeType: "N" }, | |
{ AttributeName: KEY2, AttributeType: "S" } | |
], | |
ProvisionedThroughput: { | |
ReadCapacityUnits: 5, | |
WriteCapacityUnits: 5 | |
} | |
}; | |
dynamo.createTable(params, done); | |
} | |
/** | |
* profilefileExists - check to see if the table exists or not. Creates it if it | |
* doesn't exist. | |
* @param done - callback with either true or false if it existed or not | |
* */ | |
function profilefileExists(done) { | |
dynamo.scan({TableName : TABLENAME }, (err, data) => { | |
if (err) { | |
console.log(err.message); | |
createTable((err, data) => { | |
if (err) { | |
console.log(err.message); | |
} | |
done(false); | |
}); | |
} | |
else { | |
done(true); | |
} | |
}); | |
} | |
/** | |
* loadProfilefile - loads the project file for the associated project id. It calls the callback when complete with | |
* (error, data) | |
* @param projectId - project id number. | |
* @param done - callback to call when complete. | |
* | |
* */ | |
function loadProfilefile(projectId, done) { | |
console.log('load profile') | |
var params = {}; | |
params.TableName = TABLENAME; | |
const strProjectId = '' + projectId; | |
params.Key = {version : projectId, projectId: strProjectId }; | |
dynamo.getItem(params, (err, data) => { | |
if (err) { | |
console.log(err.message); | |
done(err, {}); | |
} | |
else { | |
if (data == null || !data.hasOwnProperty('Item')) { | |
done(new Error("No Entry"), null); | |
} | |
else { | |
var item = data['Item'][FIELD]; | |
done(null, JSON.parse(item)); | |
} | |
} | |
}); | |
} | |
/** | |
* saveProfilefile - save the project for the first time. If we get an error, we assume the data has not | |
* been added and add it to the db. It would probably be safer to test the error. :) | |
* @param projectId - The projectId number. | |
* @param data - The json data to save for this project. | |
* @param done - the callback to call after putItem. | |
* | |
* */ | |
function saveProfilefile(projectId, data, done) { | |
const strProjectId = '' + projectId; | |
var params = { | |
Item: { | |
version: projectId, | |
projectId: strProjectId, | |
projectJson:JSON.stringify(data) | |
}, | |
TableName: TABLENAME | |
}; | |
dynamo.putItem(params, done); | |
} | |
/** | |
* updateProfilefile - updates the project json with the provided json by project id. | |
* @param projectId - number that is the project id | |
* @param data - the json data for that project | |
* @param done - callback to call after updateItem has completed. | |
* | |
* */ | |
function updateProfilefile(projectId, data, done) { | |
const strProjectId = '' + projectId; | |
const params = { | |
TableName: TABLENAME, | |
Key: { | |
version: projectId, | |
projectId: strProjectId | |
}, | |
UpdateExpression: "set #MyVariable = :x", | |
ExpressionAttributeNames: { | |
"#MyVariable": FIELD | |
}, | |
ExpressionAttributeValues: { | |
":x": JSON.stringify(data) | |
} | |
}; | |
dynamo.updateItem(params, done); | |
} | |
/** | |
* loadDatafile - loads the datafile from the url provided. This is the CDN location from the webhook payload | |
* @param datafileUrl - string to url that contains valid project file | |
* @param done - callback with error or json project file | |
* | |
* */ | |
function loadDatafile(datafileUrl, done) { | |
http.get(datafileUrl, (resp) => { | |
let data = ''; | |
// A chunk of data has been recieved. | |
resp.on('data', (chunk) => { | |
data += chunk; | |
}); | |
// The whole response has been received. Print out the result. | |
resp.on('end', () => { | |
done(null, JSON.parse(data)); | |
}); | |
}).on("error", (err) => { | |
done(err, JSON.parse('{}')); | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment