From f7e979aa2bb5610d3d8a3692d1a9178e8c71b22b Mon Sep 17 00:00:00 2001 From: memyselfandm Date: Wed, 13 Apr 2016 19:52:57 -0700 Subject: [PATCH] merging in new changes from most recent mixpanel-node release --- mixpanel.js | 395 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 327 insertions(+), 68 deletions(-) diff --git a/mixpanel.js b/mixpanel.js index c255935..bbe8e44 100644 --- a/mixpanel.js +++ b/mixpanel.js @@ -5,6 +5,8 @@ Copyright (c) 2012 Carl Sverre Adapted for use in Parse Cloud Code in 2015 by Brennan Stehling + Adaptation updated in 2016 by Mike Gresham baed on commit + https://github.com/mixpanel/mixpanel-node/commit/0a0450ccc77322bb3924685d0273f4a1b04ca51a Released under the MIT license. */ @@ -14,14 +16,16 @@ var Buffer = require('buffer').Buffer; var create_client = function(token, config) { var metrics = {}; - if (!token) { + if(!token) { throw new Error("The Mixpanel Client needs a Mixpanel token: `init(token)`"); } + // Default config metrics.config = { - test: true, - debug: true, - verbose: true + test: false, + debug: false, + verbose: false, + host: 'api.mixpanel.com' }; metrics.token = token; @@ -43,30 +47,26 @@ var create_client = function(token, config) { }); callback = callback || function() {}; - - var url = "https://api.mixpanel.com" + endpoint; - var buffer = new Buffer(JSON.stringify(data)); - var params = { - 'data': buffer.toString('base64'), + var event_data = new Buffer(JSON.stringify(data)); + var request_data = { + 'data': event_data.toString('base64'), 'ip': 0, 'verbose': metrics.config.verbose ? 1 : 0 }; - if (metrics.config.test) { - params.test = 1; - } - if (endpoint === '/import') { var key = metrics.config.key; if (!key) { throw new Error("The Mixpanel Client needs a Mixpanel api key when importing old events: `init(token, { key: ... })`"); } - params.api_key = key; + request_data.api_key = key; } + if (metrics.config.test) { request_data.test = 1; } + Parse.Cloud.httpRequest({ - url: url, - params: params + url: metrics.config.port ? [metrics.config.host,":",metrics.config.port].join("") : metrics.config.host, + params: request_data }).then(function(response) { if (metrics.config.verbose) { console.log(response); @@ -115,8 +115,8 @@ var create_client = function(token, config) { properties.mp_lib = "node"; var data = { - 'event': event, - 'properties': properties + 'event' : event, + 'properties' : properties }; if (metrics.config.debug) { @@ -127,6 +127,15 @@ var create_client = function(token, config) { return metrics.send_request(endpoint, data, callback); }; + var parse_time = function(time) { + if (time === void 0) { + throw new Error("Import methods require you to specify the time of the event"); + } else if (Object.prototype.toString.call(time) === '[object Date]') { + time = Math.floor(time.getTime() / 1000); + } + return time; + }; + /** import(event, properties, callback) --- @@ -153,15 +162,110 @@ var create_client = function(token, config) { properties = {}; } - if (time === void 0) { - throw new Error("The import method requires you to specify the time of the event"); - } else if (Object.prototype.toString.call(time) === '[object Date]') { - time = Math.floor(time.getTime() / 1000); + properties.time = parse_time(time); + + metrics.track(event, properties, callback); + }; + + /** + import_batch(event_list, options, callback) + --- + This function sends a list of events to mixpanel using the import + endpoint. The format of the event array should be: + + [ + { + "event": "event name", + "properties": { + "time": new Date(), // Number or Date; required for each event + "key": "val", + ... + } + }, + { + "event": "event name", + "properties": { + "time": new Date() // Number or Date; required for each event + } + }, + ... + ] + + See import() for further information about the import endpoint. + + Options: + max_batch_size: the maximum number of events to be transmitted over + the network simultaneously. useful for capping bandwidth + usage. + + N.B.: the Mixpanel API only accepts 50 events per request, so regardless + of max_batch_size, larger lists of events will be chunked further into + groups of 50. + + event_list:array list of event names and properties + options:object optional batch configuration + callback:function(error_list:array) callback is called when the request is + finished or an error occurs + */ + metrics.import_batch = function(event_list, options, callback) { + var batch_size = 50, // default: Mixpanel API permits 50 events per request + total_events = event_list.length, + max_simultaneous_events = total_events, + completed_events = 0, + event_group_idx = 0, + request_errors = []; + + if (typeof(options) === 'function' || !options) { + callback = options; + options = {}; + } + if (options.max_batch_size) { + max_simultaneous_events = options.max_batch_size; + if (options.max_batch_size < batch_size) { + batch_size = options.max_batch_size; + } } - properties.time = time; + var send_next_batch = function() { + var properties, + event_batch = []; - metrics.track(event, properties, callback); + // prepare batch with required props + for (var ei = event_group_idx; ei < total_events && ei < event_group_idx + batch_size; ei++) { + properties = event_list[ei].properties; + properties.time = parse_time(properties.time); + if (!properties.token) { + properties.token = metrics.token; + } + event_batch.push(event_list[ei]); + } + + if (event_batch.length > 0) { + metrics.send_request('/import', event_batch, function(e) { + completed_events += event_batch.length; + if (e) { + request_errors.push(e); + } + if (completed_events < total_events) { + send_next_batch(); + } else if (callback) { + callback(request_errors); + } + }); + event_group_idx += batch_size; + } + }; + + if (metrics.config.debug) { + console.log( + "Sending " + event_list.length + " events to Mixpanel in " + + Math.ceil(total_events / batch_size) + " requests" + ); + } + + for (var i = 0; i < max_simultaneous_events; i += batch_size) { + send_next_batch(); + } }; /** @@ -185,7 +289,7 @@ var create_client = function(token, config) { }; metrics.people = { - /** people.set_once(distinct_id, prop, to, callback) + /** people.set_once(distinct_id, prop, to, modifiers, callback) --- The same as people.set but in the words of mixpanel: mixpanel.people.set_once @@ -197,24 +301,32 @@ var create_client = function(token, config) { website for the first time. " */ - set_once: function(distinct_id, prop, to, callback) { - var $set = {}, - data = {}; + set_once: function(distinct_id, prop, to, modifiers, callback) { + var $set = {}; if (typeof(prop) === 'object') { - callback = to; + if (typeof(to) === 'object') { + callback = modifiers; + modifiers = to; + } else { + callback = to; + } $set = prop; } else { $set[prop] = to; + if (typeof(modifiers) === 'function' || !modifiers) { + callback = modifiers; + } } - return this._set(distinct_id, $set, callback, { - set_once: true - }); + modifiers = modifiers || {}; + modifiers.set_once = true; + + return this._set(distinct_id, $set, callback, modifiers); }, /** - people.set(distinct_id, prop, to, callback) + people.set(distinct_id, prop, to, modifiers, callback) --- set properties on an user record in engage @@ -227,22 +339,30 @@ var create_client = function(token, config) { 'plan': 'premium' }); */ - set: function(distinct_id, prop, to, callback) { - var $set = {}, - data = {}; + set: function(distinct_id, prop, to, modifiers, callback) { + var $set = {}; if (typeof(prop) === 'object') { - callback = to; + if (typeof(to) === 'object') { + callback = modifiers; + modifiers = to; + } else { + callback = to; + } $set = prop; } else { $set[prop] = to; + if (typeof(modifiers) === 'function' || !modifiers) { + callback = modifiers; + } } - return this._set(distinct_id, $set, callback); + return this._set(distinct_id, $set, callback, modifiers); }, // used internally by set and set_once _set: function(distinct_id, $set, callback, options) { + options = options || {}; var set_key = (options && options.set_once) ? "$set_once" : "$set"; var data = { @@ -261,6 +381,8 @@ var create_client = function(token, config) { delete $set.$ignore_time; } + data = merge_modifiers(data, options); + if (metrics.config.debug) { console.log("Sending the following data to Mixpanel (Engage):"); console.log(data); @@ -270,7 +392,7 @@ var create_client = function(token, config) { }, /** - people.increment(distinct_id, prop, to, callback) + people.increment(distinct_id, prop, by, modifiers, callback) --- increment/decrement properties on an user record in engage @@ -292,11 +414,16 @@ var create_client = function(token, config) { counter3: -2 }); */ - increment: function(distinct_id, prop, by, callback) { + increment: function(distinct_id, prop, by, modifiers, callback) { var $add = {}; if (typeof(prop) === 'object') { - callback = by; + if (typeof(by) === 'object') { + callback = modifiers; + modifiers = by; + } else { + callback = by; + } Object.keys(prop).forEach(function(key) { var val = prop[key]; @@ -311,10 +438,20 @@ var create_client = function(token, config) { } }); } else { - if (!by) { - by = 1; + if (typeof(by) === 'number' || !by) { + by = by || 1; + $add[prop] = by; + if (typeof(modifiers) === 'function') { + callback = modifiers; + } + } else if (typeof(by) === 'function') { + callback = by; + $add[prop] = 1; + } else { + callback = modifiers; + modifiers = (typeof(by) === 'object') ? by : {}; + $add[prop] = 1; } - $add[prop] = by; } var data = { @@ -323,6 +460,8 @@ var create_client = function(token, config) { '$distinct_id': distinct_id }; + data = merge_modifiers(data, modifiers); + if (metrics.config.debug) { console.log("Sending the following data to Mixpanel (Engage):"); console.log(data); @@ -332,7 +471,7 @@ var create_client = function(token, config) { }, /** - people.append(distinct_id, prop, value, callback) + people.append(distinct_id, prop, value, modifiers, callback) --- Append a value to a list-valued people analytics property. @@ -347,16 +486,24 @@ var create_client = function(token, config) { list2: 123 }); */ - append: function(distinct_id, prop, value, callback) { + append: function(distinct_id, prop, value, modifiers, callback) { var $append = {}; if (typeof(prop) === 'object') { - callback = value; + if (typeof(value) === 'object') { + callback = modifiers; + modifiers = value; + } else { + callback = value; + } Object.keys(prop).forEach(function(key) { $append[key] = prop[key]; }); } else { $append[prop] = value; + if (typeof(modifiers) === 'function') { + callback = modifiers; + } } var data = { @@ -365,6 +512,8 @@ var create_client = function(token, config) { '$distinct_id': distinct_id }; + data = merge_modifiers(data, modifiers); + if (metrics.config.debug) { console.log("Sending the following data to Mixpanel (Engage):"); console.log(data); @@ -374,7 +523,7 @@ var create_client = function(token, config) { }, /** - people.track_charge(distinct_id, amount, properties, callback) + people.track_charge(distinct_id, amount, properties, modifiers, callback) --- Record that you have charged the current user a certain amount of money. @@ -387,11 +536,21 @@ var create_client = function(token, config) { // charge a user $19 on the 1st of february mixpanel.people.track_charge('bob', 19, { '$time': new Date('feb 1 2012') }); */ - track_charge: function(distinct_id, amount, properties, callback) { - var $append = {}; - - if (!properties) { + track_charge: function(distinct_id, amount, properties, modifiers, callback) { + if (typeof(properties) === 'function' || !properties) { + callback = properties || function() {}; properties = {}; + } else { + if (typeof(modifiers) === 'function' || !modifiers) { + callback = modifiers || function() {}; + if (properties.$ignore_time || properties.hasOwnProperty("$ip")) { + modifiers = {}; + Object.keys(properties).forEach(function(key) { + modifiers[key] = properties[key]; + delete properties[key]; + }); + } + } } if (typeof(amount) !== 'number') { @@ -412,13 +571,13 @@ var create_client = function(token, config) { } var data = { - '$append': { - '$transactions': properties - }, + '$append': { '$transactions': properties }, '$token': metrics.token, '$distinct_id': distinct_id }; + data = merge_modifiers(data, modifiers); + if (metrics.config.debug) { console.log("Sending the following data to Mixpanel (Engage):"); console.log(data); @@ -428,7 +587,7 @@ var create_client = function(token, config) { }, /** - people.clear_charges(distinct_id, callback) + people.clear_charges(distinct_id, modifiers, callback) --- Clear all the current user's transactions. @@ -436,15 +595,17 @@ var create_client = function(token, config) { mixpanel.people.clear_charges('bob'); */ - clear_charges: function(distinct_id, callback) { + clear_charges: function(distinct_id, modifiers, callback) { var data = { - '$set': { - '$transactions': [] - }, + '$set': { '$transactions': [] }, '$token': metrics.token, '$distinct_id': distinct_id }; + if (typeof(modifiers) === 'function') { callback = modifiers; } + + data = merge_modifiers(data, modifiers); + if (metrics.config.debug) { console.log("Clearing this user's charges:", distinct_id); } @@ -453,7 +614,7 @@ var create_client = function(token, config) { }, /** - people.delete_user(distinct_id, callback) + people.delete_user(distinct_id, modifiers, callback) --- delete an user record in engage @@ -461,13 +622,17 @@ var create_client = function(token, config) { mixpanel.people.delete_user('bob'); */ - delete_user: function(distinct_id, callback) { + delete_user: function(distinct_id, modifiers, callback) { var data = { - '$delete': distinct_id, + '$delete': '', '$token': metrics.token, '$distinct_id': distinct_id }; + if (typeof(modifiers) === 'function') { callback = modifiers; } + + data = merge_modifiers(data, modifiers); + if (metrics.config.debug) { console.log("Deleting the user from engage:", distinct_id); } @@ -476,7 +641,72 @@ var create_client = function(token, config) { }, /** - people.unset(distinct_id, prop, callback) + people.union(distinct_id, data, modifiers, callback) + --- + merge value(s) into a list-valued people analytics property. + + usage: + + mixpanel.people.union('bob', {'browsers': 'firefox'}); + + mixpanel.people.union('bob', {'browsers', ['chrome'], os: ['linux']}); + */ + union: function(distinct_id, data, modifiers, callback) { + var $union = {}; + + if (typeof(data) !== 'object' || util.isArray(data)) { + if (metrics.config.debug) { + console.error("Invalid value passed to mixpanel.people.union - data must be an object with array values"); + } + return; + } + + Object.keys(data).forEach(function(key) { + var val = data[key]; + if (util.isArray(val)) { + var merge_values = val.filter(function(v) { + return typeof(v) === 'string' || typeof(v) === 'number'; + }); + if (merge_values.length > 0) { + $union[key] = merge_values; + } + } else if (typeof(val) === 'string' || typeof(val) === 'number') { + $union[key] = [val]; + } else { + if (metrics.config.debug) { + console.error("Invalid argument passed to mixpanel.people.union - values must be a scalar value or array"); + console.error("Passed " + key + ':', val); + } + return; + } + }); + + if (Object.keys($union).length === 0) { + return; + } + + data = { + '$union': $union, + '$token': metrics.token, + '$distinct_id': distinct_id + }; + + if (typeof(modifiers) === 'function') { + callback = modifiers; + } + + data = merge_modifiers(data, modifiers); + + if (metrics.config.debug) { + console.log("Sending the following data to Mixpanel (Engage):"); + console.log(data); + } + + return metrics.send_request('/engage', data, callback); + }, + + /** + people.unset(distinct_id, prop, modifiers, callback) --- delete a property on an user record in engage @@ -486,7 +716,7 @@ var create_client = function(token, config) { mixpanel.people.unset('bob', ['page_views', 'last_login']); */ - unset: function(distinct_id, prop, callback) { + unset: function(distinct_id, prop, modifiers, callback) { var $unset = []; if (util.isArray(prop)) { @@ -501,12 +731,18 @@ var create_client = function(token, config) { return; } - data = { + var data = { '$unset': $unset, '$token': metrics.token, '$distinct_id': distinct_id }; + if (typeof(modifiers) === 'function') { + callback = modifiers; + } + + data = merge_modifiers(data, modifiers); + if (metrics.config.debug) { console.log("Sending the following data to Mixpanel (Engage):"); console.log(data); @@ -516,6 +752,21 @@ var create_client = function(token, config) { } }; + var merge_modifiers = function(data, modifiers) { + if (modifiers) { + if (modifiers.$ignore_time) { + data.$ignore_time = modifiers.$ignore_time; + } + if (modifiers.hasOwnProperty("$ip")) { + data.$ip = modifiers.$ip; + } + if (modifiers.hasOwnProperty("$time")) { + data.$time = parse_time(modifiers.$time); + } + } + return data; + }; + /** set_config(config) --- @@ -527,7 +778,15 @@ var create_client = function(token, config) { metrics.set_config = function(config) { for (var c in config) { if (config.hasOwnProperty(c)) { - metrics.config[c] = config[c]; + if (c == "host") { // Split host, into host and port. + metrics.config.host = config[c].split(':')[0]; + var port = config[c].split(':')[1]; + if (port) { + metrics.config.port = Number(port); + } + } else { + metrics.config[c] = config[c]; + } } } }; @@ -546,4 +805,4 @@ module.exports = { return create_client(token); }, init: create_client -}; +}; \ No newline at end of file