UNPKG

33.7 kBJavaScriptView Raw
1/*
2 Heavily inspired by the original js library copyright Mixpanel, Inc.
3 (http://mixpanel.com/)
4
5 Copyright (c) 2012 Carl Sverre
6
7 Released under the MIT license.
8*/
9
10var http = require('http'),
11 https = require('https'),
12 querystring = require('querystring'),
13 Buffer = require('buffer').Buffer,
14 util = require('util'),
15 HttpsProxyAgent = require('https-proxy-agent');
16
17var async_all = require('./utils').async_all;
18
19var REQUEST_LIBS = {
20 http: http,
21 https: https
22};
23
24var create_proxy_agent = function() {
25 var proxyPath = process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
26 return proxyPath ? new HttpsProxyAgent(proxyPath) : null;
27}
28
29var create_client = function(token, config) {
30 var metrics = {};
31 var proxyAgent = create_proxy_agent();
32
33 // mixpanel constants
34 var MAX_BATCH_SIZE = 50,
35 TRACK_AGE_LIMIT = 60 * 60 * 24 * 5;
36
37 if(!token) {
38 throw new Error("The Mixpanel Client needs a Mixpanel token: `init(token)`");
39 }
40
41 // Default config
42 metrics.config = {
43 test: false,
44 debug: false,
45 verbose: false,
46 host: 'api.mixpanel.com',
47 protocol: 'https',
48 path: '',
49 };
50
51 metrics.token = token;
52
53 /**
54 * sends an async GET or POST request to mixpanel
55 * for batch processes data must be send in the body of a POST
56 * @param {object} options
57 * @param {string} options.endpoint
58 * @param {object} options.data the data to send in the request
59 * @param {string} [options.method] e.g. `get` or `post`, defaults to `get`
60 * @param {function} callback called on request completion or error
61 */
62 metrics.send_request = function(options, callback) {
63 callback = callback || function() {};
64
65 var content = Buffer.from(JSON.stringify(options.data)).toString('base64'),
66 endpoint = options.endpoint,
67 method = (options.method || 'GET').toUpperCase(),
68 query_params = {
69 'ip': 0,
70 'verbose': metrics.config.verbose ? 1 : 0
71 },
72 key = metrics.config.key,
73 request_lib = REQUEST_LIBS[metrics.config.protocol],
74 request_options = {
75 host: metrics.config.host,
76 port: metrics.config.port,
77 headers: {},
78 method: method
79 },
80 request;
81
82 if (!request_lib) {
83 throw new Error(
84 "Mixpanel Initialization Error: Unsupported protocol " + metrics.config.protocol + ". " +
85 "Supported protocols are: " + Object.keys(REQUEST_LIBS)
86 );
87 }
88
89
90 if (method === 'POST') {
91 content = 'data=' + content;
92 request_options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
93 request_options.headers['Content-Length'] = Buffer.byteLength(content);
94 } else if (method === 'GET') {
95 query_params.data = content;
96 }
97
98
99 // add `key` query params
100 if (key) {
101 query_params.api_key = key;
102 } else if (endpoint === '/import') {
103 throw new Error("The Mixpanel Client needs a Mixpanel api key when importing old events: `init(token, { key: ... })`");
104 }
105
106 if (proxyAgent) {
107 request_options.agent = proxyAgent;
108 }
109
110 if (metrics.config.test) {
111 query_params.test = 1;
112 }
113
114 request_options.path = metrics.config.path + endpoint + "?" + querystring.stringify(query_params);
115
116 request = request_lib.request(request_options, function(res) {
117 var data = "";
118 res.on('data', function(chunk) {
119 data += chunk;
120 });
121
122 res.on('end', function() {
123 var e;
124 if (metrics.config.verbose) {
125 try {
126 var result = JSON.parse(data);
127 if(result.status != 1) {
128 e = new Error("Mixpanel Server Error: " + result.error);
129 }
130 }
131 catch(ex) {
132 e = new Error("Could not parse response from Mixpanel");
133 }
134 }
135 else {
136 e = (data !== '1') ? new Error("Mixpanel Server Error: " + data) : undefined;
137 }
138
139 callback(e);
140 });
141 });
142
143 request.on('error', function(e) {
144 if (metrics.config.debug) {
145 console.log("Got Error: " + e.message);
146 }
147 callback(e);
148 });
149
150 if (method === 'POST') {
151 request.write(content);
152 }
153 request.end();
154 };
155
156 /**
157 * Send an event to Mixpanel, using the specified endpoint (e.g., track/import)
158 * @param {string} endpoint - API endpoint name
159 * @param {string} event - event name
160 * @param {object} properties - event properties
161 * @param {Function} [callback] - callback for request completion/error
162 */
163 metrics.send_event_request = function(endpoint, event, properties, callback) {
164 properties.token = metrics.token;
165 properties.mp_lib = "node";
166
167 var data = {
168 event: event,
169 properties: properties
170 };
171
172 if (metrics.config.debug) {
173 console.log("Sending the following event to Mixpanel:\n", data);
174 }
175
176 metrics.send_request({ method: "GET", endpoint: endpoint, data: data }, callback);
177 };
178
179 /**
180 * Validate type of time property, and convert to Unix timestamp if necessary
181 * @param {Date|number} time - value to check
182 * @returns {number} Unix timestamp
183 */
184 var ensure_timestamp = function(time) {
185 if (!(time instanceof Date || typeof time === "number")) {
186 throw new Error("`time` property must be a Date or Unix timestamp and is only required for `import` endpoint");
187 }
188 return time instanceof Date ? Math.floor(time.getTime() / 1000) : time;
189 };
190
191 /**
192 * breaks array into equal-sized chunks, with the last chunk being the remainder
193 * @param {Array} arr
194 * @param {number} size
195 * @returns {Array}
196 */
197 var chunk = function(arr, size) {
198 var chunks = [],
199 i = 0,
200 total = arr.length;
201
202 while (i < total) {
203 chunks.push(arr.slice(i, i += size));
204 }
205 return chunks;
206 };
207
208 /**
209 * sends events in batches
210 * @param {object} options
211 * @param {[{}]} options.event_list array of event objects
212 * @param {string} options.endpoint e.g. `/track` or `/import`
213 * @param {number} [options.max_concurrent_requests] limits concurrent async requests over the network
214 * @param {number} [options.max_batch_size] limits number of events sent to mixpanel per request
215 * @param {Function} [callback] callback receives array of errors if any
216 *
217 */
218 var send_batch_requests = function(options, callback) {
219 var event_list = options.event_list,
220 endpoint = options.endpoint,
221 max_batch_size = options.max_batch_size ? Math.min(MAX_BATCH_SIZE, options.max_batch_size) : MAX_BATCH_SIZE,
222 // to maintain original intention of max_batch_size; if max_batch_size is greater than 50, we assume the user is trying to set max_concurrent_requests
223 max_concurrent_requests = options.max_concurrent_requests || (options.max_batch_size > MAX_BATCH_SIZE && Math.ceil(options.max_batch_size / MAX_BATCH_SIZE)),
224 event_batches = chunk(event_list, max_batch_size),
225 request_batches = max_concurrent_requests ? chunk(event_batches, max_concurrent_requests) : [event_batches],
226 total_event_batches = event_batches.length,
227 total_request_batches = request_batches.length;
228
229 /**
230 * sends a batch of events to mixpanel through http api
231 * @param {Array} batch
232 * @param {Function} cb
233 */
234 function send_event_batch(batch, cb) {
235 if (batch.length > 0) {
236 batch = batch.map(function (event) {
237 var properties = event.properties;
238
239 if (endpoint === '/import' || event.properties.time) {
240 // usually there will be a time property, but not required for `/track` endpoint
241 event.properties.time = ensure_timestamp(event.properties.time);
242 }
243 event.properties.token = event.properties.token || metrics.token;
244 return event;
245 });
246
247 // must be a POST
248 metrics.send_request({ method: "POST", endpoint: endpoint, data: batch }, cb);
249 }
250 }
251
252 /**
253 * Asynchronously sends batches of requests
254 * @param {number} index
255 */
256 function send_next_request_batch(index) {
257 var request_batch = request_batches[index],
258 cb = function (errors, results) {
259 index += 1;
260 if (index === total_request_batches) {
261 callback && callback(errors, results);
262 } else {
263 send_next_request_batch(index);
264 }
265 };
266
267 async_all(request_batch, send_event_batch, cb);
268 }
269
270 // init recursive function
271 send_next_request_batch(0);
272
273 if (metrics.config.debug) {
274 console.log(
275 "Sending " + event_list.length + " events to Mixpanel in " +
276 total_event_batches + " batches of events and " +
277 total_request_batches + " batches of requests"
278 );
279 }
280 };
281
282 /**
283 track(event, properties, callback)
284 ---
285 this function sends an event to mixpanel.
286
287 event:string the event name
288 properties:object additional event properties to send
289 callback:function(err:Error) callback is called when the request is
290 finished or an error occurs
291 */
292 metrics.track = function(event, properties, callback) {
293 if (!properties || typeof properties === "function") {
294 callback = properties;
295 properties = {};
296 }
297
298 // time is optional for `track` but must be less than 5 days old if set
299 if (properties.time) {
300 properties.time = ensure_timestamp(properties.time);
301 if (properties.time < Date.now() / 1000 - TRACK_AGE_LIMIT) {
302 throw new Error("`track` not allowed for event more than 5 days old; use `mixpanel.import()`");
303 }
304 }
305
306 metrics.send_event_request("/track", event, properties, callback);
307 };
308
309 /**
310 * send a batch of events to mixpanel `track` endpoint: this should only be used if events are less than 5 days old
311 * @param {Array} event_list array of event objects to track
312 * @param {object} [options]
313 * @param {number} [options.max_concurrent_requests] number of concurrent http requests that can be made to mixpanel
314 * @param {number} [options.max_batch_size] number of events that can be sent to mixpanel per request
315 * @param {Function} [callback] callback receives array of errors if any
316 */
317 metrics.track_batch = function(event_list, options, callback) {
318 options = options || {};
319 if (typeof options === 'function') {
320 callback = options;
321 options = {};
322 }
323 var batch_options = {
324 event_list: event_list,
325 endpoint: "/track",
326 max_concurrent_requests: options.max_concurrent_requests,
327 max_batch_size: options.max_batch_size
328 };
329
330 send_batch_requests(batch_options, callback);
331 };
332
333 /**
334 import(event, time, properties, callback)
335 ---
336 This function sends an event to mixpanel using the import
337 endpoint. The time argument should be either a Date or Number,
338 and should signify the time the event occurred.
339
340 It is highly recommended that you specify the distinct_id
341 property for each event you import, otherwise the events will be
342 tied to the IP address of the sending machine.
343
344 For more information look at:
345 https://mixpanel.com/docs/api-documentation/importing-events-older-than-31-days
346
347 event:string the event name
348 time:date|number the time of the event
349 properties:object additional event properties to send
350 callback:function(err:Error) callback is called when the request is
351 finished or an error occurs
352 */
353 metrics.import = function(event, time, properties, callback) {
354 if (!properties || typeof properties === "function") {
355 callback = properties;
356 properties = {};
357 }
358
359 properties.time = ensure_timestamp(time);
360
361 metrics.send_event_request("/import", event, properties, callback);
362 };
363
364 /**
365 import_batch(event_list, options, callback)
366 ---
367 This function sends a list of events to mixpanel using the import
368 endpoint. The format of the event array should be:
369
370 [
371 {
372 "event": "event name",
373 "properties": {
374 "time": new Date(), // Number or Date; required for each event
375 "key": "val",
376 ...
377 }
378 },
379 {
380 "event": "event name",
381 "properties": {
382 "time": new Date() // Number or Date; required for each event
383 }
384 },
385 ...
386 ]
387
388 See import() for further information about the import endpoint.
389
390 Options:
391 max_batch_size: the maximum number of events to be transmitted over
392 the network simultaneously. useful for capping bandwidth
393 usage.
394 max_concurrent_requests: the maximum number of concurrent http requests that
395 can be made to mixpanel; also useful for capping bandwidth.
396
397 N.B.: the Mixpanel API only accepts 50 events per request, so regardless
398 of max_batch_size, larger lists of events will be chunked further into
399 groups of 50.
400
401 event_list:array list of event names and properties
402 options:object optional batch configuration
403 callback:function(error_list:array) callback is called when the request is
404 finished or an error occurs
405 */
406 metrics.import_batch = function(event_list, options, callback) {
407 var batch_options;
408
409 if (typeof(options) === "function" || !options) {
410 callback = options;
411 options = {};
412 }
413 batch_options = {
414 event_list: event_list,
415 endpoint: "/import",
416 max_concurrent_requests: options.max_concurrent_requests,
417 max_batch_size: options.max_batch_size
418 };
419 send_batch_requests(batch_options, callback);
420 };
421
422 /**
423 alias(distinct_id, alias)
424 ---
425 This function creates an alias for distinct_id
426
427 For more information look at:
428 https://mixpanel.com/docs/integration-libraries/using-mixpanel-alias
429
430 distinct_id:string the current identifier
431 alias:string the future alias
432 */
433 metrics.alias = function(distinct_id, alias, callback) {
434 var properties = {
435 distinct_id: distinct_id,
436 alias: alias
437 };
438
439 metrics.track('$create_alias', properties, callback);
440 };
441
442 metrics.people = {
443 /** people.set_once(distinct_id, prop, to, modifiers, callback)
444 ---
445 The same as people.set but in the words of mixpanel:
446 mixpanel.people.set_once
447
448 " This method allows you to set a user attribute, only if
449 it is not currently set. It can be called multiple times
450 safely, so is perfect for storing things like the first date
451 you saw a user, or the referrer that brought them to your
452 website for the first time. "
453
454 */
455 set_once: function(distinct_id, prop, to, modifiers, callback) {
456 var $set = {};
457
458 if (typeof(prop) === 'object') {
459 if (typeof(to) === 'object') {
460 callback = modifiers;
461 modifiers = to;
462 } else {
463 callback = to;
464 }
465 $set = prop;
466 } else {
467 $set[prop] = to;
468 if (typeof(modifiers) === 'function' || !modifiers) {
469 callback = modifiers;
470 }
471 }
472
473 modifiers = modifiers || {};
474 modifiers.set_once = true;
475
476 this._set(distinct_id, $set, callback, modifiers);
477 },
478
479 /**
480 people.set(distinct_id, prop, to, modifiers, callback)
481 ---
482 set properties on an user record in engage
483
484 usage:
485
486 mixpanel.people.set('bob', 'gender', 'm');
487
488 mixpanel.people.set('joe', {
489 'company': 'acme',
490 'plan': 'premium'
491 });
492 */
493 set: function(distinct_id, prop, to, modifiers, callback) {
494 var $set = {};
495
496 if (typeof(prop) === 'object') {
497 if (typeof(to) === 'object') {
498 callback = modifiers;
499 modifiers = to;
500 } else {
501 callback = to;
502 }
503 $set = prop;
504 } else {
505 $set[prop] = to;
506 if (typeof(modifiers) === 'function' || !modifiers) {
507 callback = modifiers;
508 }
509 }
510
511 this._set(distinct_id, $set, callback, modifiers);
512 },
513
514 // used internally by set and set_once
515 _set: function(distinct_id, $set, callback, options) {
516 options = options || {};
517 var set_key = (options && options.set_once) ? "$set_once" : "$set";
518
519 var data = {
520 '$token': metrics.token,
521 '$distinct_id': distinct_id
522 };
523 data[set_key] = $set;
524
525 if ('ip' in $set) {
526 data.$ip = $set.ip;
527 delete $set.ip;
528 }
529
530 if ($set.$ignore_time) {
531 data.$ignore_time = $set.$ignore_time;
532 delete $set.$ignore_time;
533 }
534
535 data = merge_modifiers(data, options);
536
537 if (metrics.config.debug) {
538 console.log("Sending the following data to Mixpanel (Engage):");
539 console.log(data);
540 }
541
542 metrics.send_request({ method: "GET", endpoint: "/engage", data: data }, callback);
543 },
544
545 /**
546 people.increment(distinct_id, prop, by, modifiers, callback)
547 ---
548 increment/decrement properties on an user record in engage
549
550 usage:
551
552 mixpanel.people.increment('bob', 'page_views', 1);
553
554 // or, for convenience, if you're just incrementing a counter by 1, you can
555 // simply do
556 mixpanel.people.increment('bob', 'page_views');
557
558 // to decrement a counter, pass a negative number
559 mixpanel.people.increment('bob', 'credits_left', -1);
560
561 // like mixpanel.people.set(), you can increment multiple properties at once:
562 mixpanel.people.increment('bob', {
563 counter1: 1,
564 counter2: 3,
565 counter3: -2
566 });
567 */
568 increment: function(distinct_id, prop, by, modifiers, callback) {
569 var $add = {};
570
571 if (typeof(prop) === 'object') {
572 if (typeof(by) === 'object') {
573 callback = modifiers;
574 modifiers = by;
575 } else {
576 callback = by;
577 }
578 Object.keys(prop).forEach(function(key) {
579 var val = prop[key];
580
581 if (isNaN(parseFloat(val))) {
582 if (metrics.config.debug) {
583 console.error("Invalid increment value passed to mixpanel.people.increment - must be a number");
584 console.error("Passed " + key + ":" + val);
585 }
586 return;
587 } else {
588 $add[key] = val;
589 }
590 });
591 } else {
592 if (typeof(by) === 'number' || !by) {
593 by = by || 1;
594 $add[prop] = by;
595 if (typeof(modifiers) === 'function') {
596 callback = modifiers;
597 }
598 } else if (typeof(by) === 'function') {
599 callback = by;
600 $add[prop] = 1;
601 } else {
602 callback = modifiers;
603 modifiers = (typeof(by) === 'object') ? by : {};
604 $add[prop] = 1;
605 }
606 }
607
608 var data = {
609 '$add': $add,
610 '$token': metrics.token,
611 '$distinct_id': distinct_id
612 };
613
614 data = merge_modifiers(data, modifiers);
615
616 if (metrics.config.debug) {
617 console.log("Sending the following data to Mixpanel (Engage):");
618 console.log(data);
619 }
620
621 metrics.send_request({ method: "GET", endpoint: "/engage", data: data }, callback);
622 },
623
624 /**
625 people.append(distinct_id, prop, value, modifiers, callback)
626 ---
627 Append a value to a list-valued people analytics property.
628
629 usage:
630
631 // append a value to a list, creating it if needed
632 mixpanel.people.append('bob', 'pages_visited', 'homepage');
633
634 // like mixpanel.people.set(), you can append multiple properties at once:
635 mixpanel.people.append('bob', {
636 list1: 'bob',
637 list2: 123
638 });
639 */
640 append: function(distinct_id, prop, value, modifiers, callback) {
641 var $append = {};
642
643 if (typeof(prop) === 'object') {
644 if (typeof(value) === 'object') {
645 callback = modifiers;
646 modifiers = value;
647 } else {
648 callback = value;
649 }
650 Object.keys(prop).forEach(function(key) {
651 $append[key] = prop[key];
652 });
653 } else {
654 $append[prop] = value;
655 if (typeof(modifiers) === 'function') {
656 callback = modifiers;
657 }
658 }
659
660 var data = {
661 '$append': $append,
662 '$token': metrics.token,
663 '$distinct_id': distinct_id
664 };
665
666 data = merge_modifiers(data, modifiers);
667
668 if (metrics.config.debug) {
669 console.log("Sending the following data to Mixpanel (Engage):");
670 console.log(data);
671 }
672
673 metrics.send_request({ method: "GET", endpoint: "/engage", data: data }, callback);
674 },
675
676 /**
677 people.track_charge(distinct_id, amount, properties, modifiers, callback)
678 ---
679 Record that you have charged the current user a certain
680 amount of money.
681
682 usage:
683
684 // charge a user $29.99
685 mixpanel.people.track_charge('bob', 29.99);
686
687 // charge a user $19 on the 1st of february
688 mixpanel.people.track_charge('bob', 19, { '$time': new Date('feb 1 2012') });
689 */
690 track_charge: function(distinct_id, amount, properties, modifiers, callback) {
691 if (typeof(properties) === 'function' || !properties) {
692 callback = properties || function() {};
693 properties = {};
694 } else {
695 if (typeof(modifiers) === 'function' || !modifiers) {
696 callback = modifiers || function() {};
697 if (properties.$ignore_time || properties.hasOwnProperty("$ip")) {
698 modifiers = {};
699 Object.keys(properties).forEach(function(key) {
700 modifiers[key] = properties[key];
701 delete properties[key];
702 });
703 }
704 }
705 }
706
707 if (typeof(amount) !== 'number') {
708 amount = parseFloat(amount);
709 if (isNaN(amount)) {
710 console.error("Invalid value passed to mixpanel.people.track_charge - must be a number");
711 return;
712 }
713 }
714
715 properties.$amount = amount;
716
717 if (properties.hasOwnProperty('$time')) {
718 var time = properties.$time;
719 if (Object.prototype.toString.call(time) === '[object Date]') {
720 properties.$time = time.toISOString();
721 }
722 }
723
724 var data = {
725 '$append': { '$transactions': properties },
726 '$token': metrics.token,
727 '$distinct_id': distinct_id
728 };
729
730 data = merge_modifiers(data, modifiers);
731
732 if (metrics.config.debug) {
733 console.log("Sending the following data to Mixpanel (Engage):");
734 console.log(data);
735 }
736
737 metrics.send_request({ method: "GET", endpoint: "/engage", data: data }, callback);
738 },
739
740 /**
741 people.clear_charges(distinct_id, modifiers, callback)
742 ---
743 Clear all the current user's transactions.
744
745 usage:
746
747 mixpanel.people.clear_charges('bob');
748 */
749 clear_charges: function(distinct_id, modifiers, callback) {
750 var data = {
751 '$set': { '$transactions': [] },
752 '$token': metrics.token,
753 '$distinct_id': distinct_id
754 };
755
756 if (typeof(modifiers) === 'function') { callback = modifiers; }
757
758 data = merge_modifiers(data, modifiers);
759
760 if (metrics.config.debug) {
761 console.log("Clearing this user's charges:", distinct_id);
762 }
763
764 metrics.send_request({ method: "GET", endpoint: "/engage", data: data }, callback);
765 },
766
767 /**
768 people.delete_user(distinct_id, modifiers, callback)
769 ---
770 delete an user record in engage
771
772 usage:
773
774 mixpanel.people.delete_user('bob');
775 */
776 delete_user: function(distinct_id, modifiers, callback) {
777 var data = {
778 '$delete': '',
779 '$token': metrics.token,
780 '$distinct_id': distinct_id
781 };
782
783 if (typeof(modifiers) === 'function') { callback = modifiers; }
784
785 data = merge_modifiers(data, modifiers);
786
787 if (metrics.config.debug) {
788 console.log("Deleting the user from engage:", distinct_id);
789 }
790
791 metrics.send_request({ method: "GET", endpoint: "/engage", data: data }, callback);
792 },
793
794 /**
795 people.union(distinct_id, data, modifiers, callback)
796 ---
797 merge value(s) into a list-valued people analytics property.
798
799 usage:
800
801 mixpanel.people.union('bob', {'browsers': 'firefox'});
802
803 mixpanel.people.union('bob', {'browsers': ['chrome'], os: ['linux']});
804 */
805 union: function(distinct_id, data, modifiers, callback) {
806 var $union = {};
807
808 if (typeof(data) !== 'object' || util.isArray(data)) {
809 if (metrics.config.debug) {
810 console.error("Invalid value passed to mixpanel.people.union - data must be an object with scalar or array values");
811 }
812 return;
813 }
814
815 Object.keys(data).forEach(function(key) {
816 var val = data[key];
817 if (util.isArray(val)) {
818 var merge_values = val.filter(function(v) {
819 return typeof(v) === 'string' || typeof(v) === 'number';
820 });
821 if (merge_values.length > 0) {
822 $union[key] = merge_values;
823 }
824 } else if (typeof(val) === 'string' || typeof(val) === 'number') {
825 $union[key] = [val];
826 } else {
827 if (metrics.config.debug) {
828 console.error("Invalid argument passed to mixpanel.people.union - values must be a scalar value or array");
829 console.error("Passed " + key + ':', val);
830 }
831 return;
832 }
833 });
834
835 if (Object.keys($union).length === 0) {
836 return;
837 }
838
839 data = {
840 '$union': $union,
841 '$token': metrics.token,
842 '$distinct_id': distinct_id
843 };
844
845 if (typeof(modifiers) === 'function') {
846 callback = modifiers;
847 }
848
849 data = merge_modifiers(data, modifiers);
850
851 if (metrics.config.debug) {
852 console.log("Sending the following data to Mixpanel (Engage):");
853 console.log(data);
854 }
855
856 metrics.send_request({ method: "GET", endpoint: "/engage", data: data }, callback);
857 },
858
859 /**
860 people.unset(distinct_id, prop, modifiers, callback)
861 ---
862 delete a property on an user record in engage
863
864 usage:
865
866 mixpanel.people.unset('bob', 'page_views');
867
868 mixpanel.people.unset('bob', ['page_views', 'last_login']);
869 */
870 unset: function(distinct_id, prop, modifiers, callback) {
871 var $unset = [];
872
873 if (util.isArray(prop)) {
874 $unset = prop;
875 } else if (typeof(prop) === 'string') {
876 $unset = [prop];
877 } else {
878 if (metrics.config.debug) {
879 console.error("Invalid argument passed to mixpanel.people.unset - must be a string or array");
880 console.error("Passed: " + prop);
881 }
882 return;
883 }
884
885 var data = {
886 '$unset': $unset,
887 '$token': metrics.token,
888 '$distinct_id': distinct_id
889 };
890
891 if (typeof(modifiers) === 'function') {
892 callback = modifiers;
893 }
894
895 data = merge_modifiers(data, modifiers);
896
897 if (metrics.config.debug) {
898 console.log("Sending the following data to Mixpanel (Engage):");
899 console.log(data);
900 }
901
902 metrics.send_request({ method: "GET", endpoint: "/engage", data: data }, callback);
903 }
904 };
905
906 var merge_modifiers = function(data, modifiers) {
907 if (modifiers) {
908 if (modifiers.$ignore_alias) {
909 data.$ignore_alias = modifiers.$ignore_alias;
910 }
911 if (modifiers.$ignore_time) {
912 data.$ignore_time = modifiers.$ignore_time;
913 }
914 if (modifiers.hasOwnProperty("$ip")) {
915 data.$ip = modifiers.$ip;
916 }
917 if (modifiers.hasOwnProperty("$time")) {
918 data.$time = ensure_timestamp(modifiers.$time);
919 }
920 }
921 return data;
922 };
923
924 /**
925 set_config(config)
926 ---
927 Modifies the mixpanel config
928
929 config:object an object with properties to override in the
930 mixpanel client config
931 */
932 metrics.set_config = function(config) {
933 for (var c in config) {
934 if (config.hasOwnProperty(c)) {
935 if (c == "host") { // Split host, into host and port.
936 metrics.config.host = config[c].split(':')[0];
937 var port = config[c].split(':')[1];
938 if (port) {
939 metrics.config.port = Number(port);
940 }
941 } else {
942 metrics.config[c] = config[c];
943 }
944 }
945 }
946 };
947
948 if (config) {
949 metrics.set_config(config);
950 }
951
952 return metrics;
953};
954
955// module exporting
956module.exports = {
957 Client: function(token) {
958 console.warn("The function `Client(token)` is deprecated. It is now called `init(token)`.");
959 return create_client(token);
960 },
961 init: create_client
962};