Skip to content

Instantly share code, notes, and snippets.

@thomaszurkan-optimizely
Created July 30, 2018 21:14
Show Gist options
  • Save thomaszurkan-optimizely/a55813fdad3eade5d45b0bc426b5ee17 to your computer and use it in GitHub Desktop.
Save thomaszurkan-optimizely/a55813fdad3eade5d45b0bc426b5ee17 to your computer and use it in GitHub Desktop.
Using AWS Lambda with Optimizely Web hooks for Project Management
'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