Skip to content

Instantly share code, notes, and snippets.

@ak--47
Created March 27, 2023 18:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ak--47/55f6062c3f103ff0c754275f3990882a to your computer and use it in GitHub Desktop.
Save ak--47/55f6062c3f103ff0c754275f3990882a to your computer and use it in GitHub Desktop.
refresh mixpanel dropdowns
#!/usr/bin/env node
/*
THE WONDERFUL EVENT/PROP DROPDOWN RESURFACER
--------------------------------------------
purpose:
mixpanel's dropdowns for events and props are great; but they only cache data that is from the last 30 days
this sucks if you are importing data that's older than 30 days
this script grabs an example of each unique event, in the time window you specify
and then send a copy of each event to mixpanel with a time of NOW
this effectively repopulates all the dropdowns with the events/props on your historical data
usage:
1) modify the config to include your project secret (line 35); change to 'api-eu.mixpanel.com' (line 220) for EU residency
2) run this script as:
node refreshMpDropdowns.js
then see that all your dropdowns work again!
lovingly made for you by
___ _ __
/ _ \ | | / /
/ /_\ \| |/ /
| _ || \
| | | || |\ \
\_| |_/\_| \_/
ak@mixpanel.com
*/
const config = {
secret: `YOUR-PROJECT-SECRET`,
projectId: `YOUR-PROJECT-ID`,
//the range of dates to look for
from: `2021-01-01`,
to: `2021-10-01`
}
const http = require("https");
const qs = require("querystring");
global.RIGHT_NOW = Math.round(Date.now() / 1000);
//make a 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();
});
}
async function main(config) {
const auth = Buffer.from(config.secret + '::').toString('base64')
//JQL FOR EVENT NAMES
log(`calling /jql from ${config.from} to ${config.to}`)
const jqlRes = await jqlQueryForEvents(config, auth);
const allEvents = jqlRes.map(event => event.value);
log(` found ${allEvents.length} unique events`)
//MOVE $TIME; KILL DISTINCT_ID
log(`\nmodifying ${allEvents.length} events to update $time, $distinct_id, and wiping properties`)
const processedEvents = makeCurrentAndWipeUID(allEvents);
//get ride of $identify, $create_alias, and $merge
const uniqueEvents = processedEvents.filter(ev => !['$identify', '$create_alias', '$merge'].includes(ev.event));
log(` removing $identify, $create_alias, and $merge events (${processedEvents.length - uniqueEvents.length} events)`)
//BATCH THEM UP
log(`\nwrapping ${uniqueEvents.length} events into ${Math.round(uniqueEvents.length/50 + 1)} batches`)
const batchesOfEventsToSend = chunkArray(uniqueEvents, 150);
//TRACK IT
log(`\ncalling /import for project: ${config.projectId}`)
for await (batch of batchesOfEventsToSend) {
let sendToMp = await flushEventsToMixpanel(batch, auth)
console.log(` ${JSON.stringify(sendToMp)}`)
}
console.log(`\nALL FINISHED!\nin a few moments, all your project's dropdowns will be fixed! yay!`)
process.exit()
}
main(config)
//UTILITIES
async function jqlQueryForEvents(config, auth) {
const query = `function main() {
return Events({
from_date: '${config.from}',
to_date: '${config.to}'
})
.groupBy(["name"], mixpanel.reducer.any());
}`
const options = {
hostname: 'mixpanel.com', // or 'eu.mixpanel.com'
path: '/api/2.0/jql',
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: `Basic ${auth}`,
'Content-Type': 'application/x-www-form-urlencoded'
}
};
const response = await fetch(options, qs.stringify({ script: query }))
return response;
}
async function mpRawExport(config, eventName, auth) {
const options = {
hostname: 'data.mixpanel.com',
path: `/api/2.0/export?from_date=${config.from}&to_date=${config.to}&limit=1&event=%5B%22${encodeURIComponent(eventName)}%22%5D`,
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Basic ${auth}`,
'Content-Type': 'application/x-www-form-urlencoded'
}
};
const response = await fetch(options)
return response;
}
function chunkArray(arr, size) {
const arr2 = arr.slice(0),
arrays = [];
while (arr2.length > 0) {
arrays.push(arr2.splice(0, size));
}
return arrays;
}
function makeCurrentAndWipeUID(arrayOfEvents) {
for (mpEvent of arrayOfEvents) {
//fix event name
mpEvent.event = mpEvent.name
//remove top level keys
delete mpEvent.name;
delete mpEvent.dataset;
delete mpEvent.distinct_id;
delete mpEvent.labels;
delete mpEvent.sampling_factor;
delete mpEvent.time;
delete mpEvent.properties.$import
delete mpEvent.properties.token
delete mpEvent.properties.$mp_api_endpoint
delete mpEvent.properties.$mp_api_timestamp_ms
delete mpEvent.properties.mp_processing_time_ms
delete mpEvent.properties.$insert_id
delete mpEvent.properties.$distinct_id_before_identify
delete mpEvent.properties.$is_reshuffled
delete mpEvent.properties.mp_lib
delete mpEvent.properties.$mp_api_timestamp_ms
//delete all prop values so as not to pollute analysis
for (let key in mpEvent.properties) {
let emptyValue = getEmptyType(typeof mpEvent.properties[key])
mpEvent.properties[key] = emptyValue
}
mpEvent.properties.distinct_id = `EVENT RESURFACER!`
mpEvent.properties.$source = `EVENT RESURFACER!`
mpEvent.properties.$insert_id = makeid(12)
mpEvent.properties.time = RIGHT_NOW;
}
return arrayOfEvents
}
async function flushEventsToMixpanel(batch, auth) {
const options = {
hostname: 'api.mixpanel.com', //or 'api-eu.mixpanel.com'
path: `/import?strict=1`,
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: `Basic ${auth}`,
'Content-Type': 'application/json'
}
};
const response = await fetch(options, JSON.stringify(batch))
return response
}
function log(message) {
console.log(message)
}
function sleep(milliseconds) {
return new Promise((resolve) => setTimeout(resolve, milliseconds))
}
function getEmptyType(type = "") {
switch (type) {
case "undefined":
return ``;
case "boolean":
return false;
case "number":
return 0;
case "bigint":
return 0;
case "string":
return ``;
case "symbol":
return ``;
case "function":
return {};
case "object":
if (Array.isArray(type)) {
return []
}
else {
return {};
}
default:
return ``;
}
}
function makeid(length) {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() *
charactersLength));
}
return result;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment