Comparing version 0.1.1 to 0.2.0
@@ -74,1 +74,21 @@ // grab the Mixpanel factory | ||
}); | ||
// import multiple events at once | ||
mixpanel_importer.import_batch([ | ||
{ | ||
event: 'old event', | ||
properties: { | ||
time: new Date(2012, 4, 20, 12, 34, 56), | ||
distinct_id: 'billybob', | ||
gender: 'male' | ||
} | ||
}, | ||
{ | ||
event: 'another old event', | ||
properties: { | ||
time: new Date(2012, 4, 21, 11, 33, 55), | ||
distinct_id: 'billybob', | ||
color: 'red' | ||
} | ||
} | ||
]); |
@@ -135,2 +135,11 @@ /* | ||
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; | ||
}; | ||
/** | ||
@@ -162,11 +171,106 @@ import(event, properties, callback) | ||
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(); | ||
} | ||
}; | ||
@@ -173,0 +277,0 @@ |
@@ -10,3 +10,3 @@ { | ||
], | ||
"version": "0.1.1", | ||
"version": "0.2.0", | ||
"homepage": "https://github.com/mixpanel/mixpanel-node", | ||
@@ -13,0 +13,0 @@ "author": "Carl Sverre", |
@@ -87,2 +87,22 @@ Mixpanel-node | ||
}); | ||
// import multiple events at once | ||
mixpanel_importer.import_batch([ | ||
{ | ||
event: 'old event', | ||
properties: { | ||
time: new Date(2012, 4, 20, 12, 34, 56), | ||
distinct_id: 'billybob', | ||
gender: 'male' | ||
} | ||
}, | ||
{ | ||
event: 'another old event', | ||
properties: { | ||
time: new Date(2012, 4, 21, 11, 33, 55), | ||
distinct_id: 'billybob', | ||
color: 'red' | ||
} | ||
} | ||
]); | ||
``` | ||
@@ -89,0 +109,0 @@ |
@@ -1,3 +0,5 @@ | ||
var Mixpanel = require('../lib/mixpanel-node'), | ||
Sinon = require('sinon'); | ||
var Mixpanel = require('../lib/mixpanel-node'), | ||
Sinon = require('sinon'), | ||
http = require('http'), | ||
events = require('events'); | ||
@@ -72,3 +74,3 @@ exports.import = { | ||
function() { this.mixpanel.import('test'); }, | ||
"The import method requires you to specify the time of the event", | ||
"Import methods require you to specify the time of the event", | ||
"import didn't throw an error when time wasn't specified" | ||
@@ -80,1 +82,195 @@ ); | ||
}; | ||
exports.import_batch = { | ||
setUp: function(next) { | ||
this.mixpanel = Mixpanel.init('token', { key: 'key' }); | ||
this.clock = Sinon.useFakeTimers(); | ||
Sinon.stub(this.mixpanel, 'send_request'); | ||
next(); | ||
}, | ||
tearDown: function(next) { | ||
this.mixpanel.send_request.restore(); | ||
this.clock.restore(); | ||
next(); | ||
}, | ||
"calls send_request with correct endpoint and data": function(test) { | ||
var expected_endpoint = "/import", | ||
event_list = [ | ||
{event: 'test', properties: {key1: 'val1', time: 500 }}, | ||
{event: 'test', properties: {key2: 'val2', time: 1000}}, | ||
{event: 'test2', properties: {key2: 'val2', time: 1500}} | ||
], | ||
expected_data = [ | ||
{event: 'test', properties: {key1: 'val1', time: 500, token: 'token'}}, | ||
{event: 'test', properties: {key2: 'val2', time: 1000, token: 'token'}}, | ||
{event: 'test2', properties: {key2: 'val2', time: 1500, token: 'token'}} | ||
]; | ||
this.mixpanel.import_batch(event_list); | ||
test.ok( | ||
this.mixpanel.send_request.calledWithMatch(expected_endpoint, expected_data), | ||
"import_batch didn't call send_request with correct arguments" | ||
); | ||
test.done(); | ||
}, | ||
"requires the time argument for every event": function(test) { | ||
var event_list = [ | ||
{event: 'test', properties: {key1: 'val1', time: 500 }}, | ||
{event: 'test', properties: {key2: 'val2', time: 1000}}, | ||
{event: 'test2', properties: {key2: 'val2' }} | ||
]; | ||
test.throws( | ||
function() { this.mixpanel.import_batch(event_list); }, | ||
"Import methods require you to specify the time of the event", | ||
"import didn't throw an error when time wasn't specified" | ||
); | ||
test.done(); | ||
}, | ||
"batches 50 events at a time": function(test) { | ||
var event_list = []; | ||
for (var ei = 0; ei < 130; ei++) { // 3 batches: 50 + 50 + 30 | ||
event_list.push({event: 'test', properties: {key1: 'val1', time: 500 + ei }}); | ||
} | ||
this.mixpanel.import_batch(event_list); | ||
test.equals( | ||
3, this.mixpanel.send_request.callCount, | ||
"import_batch didn't call send_request correct number of times" | ||
); | ||
test.done(); | ||
} | ||
}; | ||
exports.import_batch_integration = { | ||
setUp: function(next) { | ||
this.mixpanel = Mixpanel.init('token', { key: 'key' }); | ||
this.clock = Sinon.useFakeTimers(); | ||
Sinon.stub(http, 'get'); | ||
this.http_emitter = new events.EventEmitter(); | ||
// stub sequence of http responses | ||
this.res = []; | ||
for (var ri = 0; ri < 5; ri++) { | ||
this.res.push(new events.EventEmitter()); | ||
http.get | ||
.onCall(ri) | ||
.callsArgWith(1, this.res[ri]) | ||
.returns(this.http_emitter); | ||
} | ||
this.event_list = []; | ||
for (var ei = 0; ei < 130; ei++) { // 3 batches: 50 + 50 + 30 | ||
this.event_list.push({event: 'test', properties: {key1: 'val1', time: 500 + ei }}); | ||
} | ||
next(); | ||
}, | ||
tearDown: function(next) { | ||
http.get.restore(); | ||
this.clock.restore(); | ||
next(); | ||
}, | ||
"calls provided callback after all requests finish": function(test) { | ||
test.expect(2); | ||
this.mixpanel.import_batch(this.event_list, function(error_list) { | ||
test.equals( | ||
3, http.get.callCount, | ||
"import_batch didn't call send_request correct number of times before callback" | ||
); | ||
test.equals( | ||
0, error_list.length, | ||
"import_batch returned errors in callback unexpectedly" | ||
); | ||
test.done(); | ||
}); | ||
for (var ri = 0; ri < 3; ri++) { | ||
this.res[ri].emit('data', '1'); | ||
this.res[ri].emit('end'); | ||
} | ||
}, | ||
"passes error list to callback": function(test) { | ||
test.expect(1); | ||
this.mixpanel.import_batch(this.event_list, function(error_list) { | ||
test.equals( | ||
3, error_list.length, | ||
"import_batch didn't return errors in callback" | ||
); | ||
test.done(); | ||
}); | ||
for (var ri = 0; ri < 3; ri++) { | ||
this.res[ri].emit('data', '0'); | ||
this.res[ri].emit('end'); | ||
} | ||
}, | ||
"calls provided callback when options are passed": function(test) { | ||
test.expect(2); | ||
this.mixpanel.import_batch(this.event_list, {max_batch_size: 100}, function(error_list) { | ||
test.equals( | ||
3, http.get.callCount, | ||
"import_batch didn't call send_request correct number of times before callback" | ||
); | ||
test.equals( | ||
0, error_list.length, | ||
"import_batch returned errors in callback unexpectedly" | ||
); | ||
test.done(); | ||
}); | ||
for (var ri = 0; ri < 3; ri++) { | ||
this.res[ri].emit('data', '1'); | ||
this.res[ri].emit('end'); | ||
} | ||
}, | ||
"sends more requests when max_batch_size < 50": function(test) { | ||
test.expect(2); | ||
this.mixpanel.import_batch(this.event_list, {max_batch_size: 30}, function(error_list) { | ||
test.equals( | ||
5, http.get.callCount, // 30 + 30 + 30 + 30 + 10 | ||
"import_batch didn't call send_request correct number of times before callback" | ||
); | ||
test.equals( | ||
0, error_list.length, | ||
"import_batch returned errors in callback unexpectedly" | ||
); | ||
test.done(); | ||
}); | ||
for (var ri = 0; ri < 5; ri++) { | ||
this.res[ri].emit('data', '1'); | ||
this.res[ri].emit('end'); | ||
} | ||
}, | ||
"behaves well without a callback": function(test) { | ||
test.expect(2); | ||
this.mixpanel.import_batch(this.event_list); | ||
test.equals( | ||
3, http.get.callCount, | ||
"import_batch didn't call send_request correct number of times" | ||
); | ||
this.mixpanel.import_batch(this.event_list, {max_batch_size: 100}); | ||
test.equals( | ||
5, http.get.callCount, // 3 + 100 / 50; last request starts async | ||
"import_batch didn't call send_request correct number of times" | ||
); | ||
test.done(); | ||
} | ||
}; |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
60884
1328
144
3