Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Github Webhooks => Mixpanel Annotations
// GitHub to Mixpanel Annotations
// by AK
// ak@mixpanel.com
// purpose: ingest github webhooks; create Mixpanel annotations
//deps
const crypto = require('crypto');
const http = require("https");
/*
! IMPORTANT NOTE !
it is NOT a good practice to store secrets in code as we have done here
creds are just here for ease of illustration
the best way to store secrets in a cloud function is to use Google's secret manager
https://cloud.google.com/functions/docs/configuring/secrets
*/
const creds = {
github: `{{ your github secret }}`,
mpUser: `{{ your mixpanel service account user }}`,
mpSecret: `{{ your mixpanel service account secret }}`,
workspace_id: `{{ your mixpanel workspace_id }}`
}
//entry point for cloud function
exports.start = async (req, res) => {
console.log('RUNNING!')
//grab body + sig
const body = req.body
const sig = req.headers['x-hub-signature-256']
//run our program
await main(body, sig).then((response) => {
res.status(200).send(response);
console.log('FINISHED!')
})
};
async function main(body = {}, sig = ``) {
//verify github signature!
const isVerified = verifyPostData(sig, body, creds.github)
if (isVerified) {
const annotationData = {
date: ``,
description: `${body.release.tag_name} - ${body.release.name}`
}
//make the date match mixpanel's format and add time
const published_at = new Date(body.release.published_at);
const UTCOffset = 5 //EST
published_at.setHours(published_at.getHours() + UTCOffset);
const dateTimeString = `${published_at.getFullYear().toString().padStart(4, '0')}-${(published_at.getMonth()+1).toString().padStart(2, '0')}-${published_at.getDate().toString().padStart(2, '0')} ${published_at.getHours().toString().padStart(2, '0')}:${published_at.getMinutes().toString().padStart(2, '0')}:${published_at.getSeconds().toString().padStart(2, '0')}`;
annotationData.date = dateTimeString;
//create the annotation
const response = await makeAnnotation(annotationData, creds)
if (response.statusCode === 200) {
console.log(`created annotation!`)
return `👍`
} else {
console.log(`failed to create annotation!`)
return `👎`
}
} else {
console.log(`message was not signed correctly!`)
return `👎`
}
}
//UTILITIES
//verify github actually sent the message
function verifyPostData(signature, body, secret) {
const sigHeaderName = 'X-Hub-Signature-256'
const sigHashAlg = 'sha256'
const sig = Buffer.from(signature || '', 'utf8')
const hmac = crypto.createHmac(sigHashAlg, secret)
const digest = Buffer.from(sigHashAlg + '=' + hmac.update(JSON.stringify(body)).digest('hex'), 'utf8')
if (sig.length !== digest.length || !crypto.timingSafeEqual(digest, sig)) {
return false;
} else {
return true;
}
}
//call the annotations API
async function makeAnnotation(data, creds) {
const auth = Buffer.from(`${creds.mpUser}:${creds.mpSecret}`).toString('base64')
const options = {
hostname: 'mixpanel.com',
path: `/api/app/workspaces/${creds.workspace_id}/annotations`,
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: `Basic ${auth}`
}
};
const response = await fetch(options, data)
return response;
}
//promise-aware version of fetch() using http.request
function fetch(params, postData) {
return new Promise(function(resolve, reject) {
const req = http.request(params, function(res) {
// reject on bad status
if (res.statusCode < 200 || res.statusCode >= 300) {
return reject(new Error('statusCode=' + res.statusCode));
}
// cumulate data
let body = [];
res.on('data', function(chunk) {
body.push(chunk);
});
// resolve on end
res.on('end', function() {
try {
body = JSON.parse(Buffer.concat(body).toString());
} catch (e) {
reject(e);
}
resolve(body);
});
});
// reject on request error
req.on('error', function(err) {
// This is not a "Second reject", just a different sort of failure
reject(err);
});
if (postData) {
req.write(postData);
}
// IMPORTANT
req.end();
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment