mixpanel-browser
Version:
The official Mixpanel JavaScript browser client library
481 lines (447 loc) • 17.4 kB
JavaScript
/* eslint camelcase: "off" */
import { addOptOutCheckMixpanelPeople } from './gdpr-utils';
import {
SET_ACTION,
SET_ONCE_ACTION,
UNSET_ACTION,
ADD_ACTION,
APPEND_ACTION,
REMOVE_ACTION,
UNION_ACTION,
apiActions
} from './api-actions';
import { _, console } from './utils';
/**
* Mixpanel People Object
* @constructor
*/
var MixpanelPeople = function() {};
_.extend(MixpanelPeople.prototype, apiActions);
MixpanelPeople.prototype._init = function(mixpanel_instance) {
this._mixpanel = mixpanel_instance;
};
/*
* Set properties on a user record.
*
* ### Usage:
*
* mixpanel.people.set('gender', 'm');
*
* // or set multiple properties at once
* mixpanel.people.set({
* 'Company': 'Acme',
* 'Plan': 'Premium',
* 'Upgrade date': new Date()
* });
* // properties can be strings, integers, dates, or lists
*
* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values.
* @param {*} [to] A value to set on the given property name
* @param {Function} [callback] If provided, the callback will be called after tracking the event.
*/
MixpanelPeople.prototype.set = addOptOutCheckMixpanelPeople(function(prop, to, callback) {
var data = this.set_action(prop, to);
if (_.isObject(prop)) {
callback = to;
}
// make sure that the referrer info has been updated and saved
if (this._get_config('save_referrer')) {
this._mixpanel['persistence'].update_referrer_info(document.referrer);
}
// update $set object with default people properties
data[SET_ACTION] = _.extend(
{},
_.info.people_properties(),
this._mixpanel['persistence'].get_referrer_info(),
data[SET_ACTION]
);
return this._send_request(data, callback);
});
/*
* Set properties on a user record, only if they do not yet exist.
* This will not overwrite previous people property values, unlike
* people.set().
*
* ### Usage:
*
* mixpanel.people.set_once('First Login Date', new Date());
*
* // or set multiple properties at once
* mixpanel.people.set_once({
* 'First Login Date': new Date(),
* 'Starting Plan': 'Premium'
* });
*
* // properties can be strings, integers or dates
*
* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values.
* @param {*} [to] A value to set on the given property name
* @param {Function} [callback] If provided, the callback will be called after tracking the event.
*/
MixpanelPeople.prototype.set_once = addOptOutCheckMixpanelPeople(function(prop, to, callback) {
var data = this.set_once_action(prop, to);
if (_.isObject(prop)) {
callback = to;
}
return this._send_request(data, callback);
});
/*
* Unset properties on a user record (permanently removes the properties and their values from a profile).
*
* ### Usage:
*
* mixpanel.people.unset('gender');
*
* // or unset multiple properties at once
* mixpanel.people.unset(['gender', 'Company']);
*
* @param {Array|String} prop If a string, this is the name of the property. If an array, this is a list of property names.
* @param {Function} [callback] If provided, the callback will be called after tracking the event.
*/
MixpanelPeople.prototype.unset = addOptOutCheckMixpanelPeople(function(prop, callback) {
var data = this.unset_action(prop);
return this._send_request(data, callback);
});
/*
* Increment/decrement numeric people analytics properties.
*
* ### Usage:
*
* mixpanel.people.increment('page_views', 1);
*
* // or, for convenience, if you're just incrementing a counter by
* // 1, you can simply do
* mixpanel.people.increment('page_views');
*
* // to decrement a counter, pass a negative number
* mixpanel.people.increment('credits_left', -1);
*
* // like mixpanel.people.set(), you can increment multiple
* // properties at once:
* mixpanel.people.increment({
* counter1: 1,
* counter2: 6
* });
*
* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and numeric values.
* @param {Number} [by] An amount to increment the given property
* @param {Function} [callback] If provided, the callback will be called after tracking the event.
*/
MixpanelPeople.prototype.increment = addOptOutCheckMixpanelPeople(function(prop, by, callback) {
var data = {};
var $add = {};
if (_.isObject(prop)) {
_.each(prop, function(v, k) {
if (!this._is_reserved_property(k)) {
if (isNaN(parseFloat(v))) {
console.error('Invalid increment value passed to mixpanel.people.increment - must be a number');
return;
} else {
$add[k] = v;
}
}
}, this);
callback = by;
} else {
// convenience: mixpanel.people.increment('property'); will
// increment 'property' by 1
if (_.isUndefined(by)) {
by = 1;
}
$add[prop] = by;
}
data[ADD_ACTION] = $add;
return this._send_request(data, callback);
});
/*
* Append a value to a list-valued people analytics property.
*
* ### Usage:
*
* // append a value to a list, creating it if needed
* mixpanel.people.append('pages_visited', 'homepage');
*
* // like mixpanel.people.set(), you can append multiple
* // properties at once:
* mixpanel.people.append({
* list1: 'bob',
* list2: 123
* });
*
* @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values.
* @param {*} [value] value An item to append to the list
* @param {Function} [callback] If provided, the callback will be called after tracking the event.
*/
MixpanelPeople.prototype.append = addOptOutCheckMixpanelPeople(function(list_name, value, callback) {
if (_.isObject(list_name)) {
callback = value;
}
var data = this.append_action(list_name, value);
return this._send_request(data, callback);
});
/*
* Remove a value from a list-valued people analytics property.
*
* ### Usage:
*
* mixpanel.people.remove('School', 'UCB');
*
* @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values.
* @param {*} [value] value Item to remove from the list
* @param {Function} [callback] If provided, the callback will be called after tracking the event.
*/
MixpanelPeople.prototype.remove = addOptOutCheckMixpanelPeople(function(list_name, value, callback) {
if (_.isObject(list_name)) {
callback = value;
}
var data = this.remove_action(list_name, value);
return this._send_request(data, callback);
});
/*
* Merge a given list with a list-valued people analytics property,
* excluding duplicate values.
*
* ### Usage:
*
* // merge a value to a list, creating it if needed
* mixpanel.people.union('pages_visited', 'homepage');
*
* // like mixpanel.people.set(), you can append multiple
* // properties at once:
* mixpanel.people.union({
* list1: 'bob',
* list2: 123
* });
*
* // like mixpanel.people.append(), you can append multiple
* // values to the same list:
* mixpanel.people.union({
* list1: ['bob', 'billy']
* });
*
* @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values.
* @param {*} [value] Value / values to merge with the given property
* @param {Function} [callback] If provided, the callback will be called after tracking the event.
*/
MixpanelPeople.prototype.union = addOptOutCheckMixpanelPeople(function(list_name, values, callback) {
if (_.isObject(list_name)) {
callback = values;
}
var data = this.union_action(list_name, values);
return this._send_request(data, callback);
});
/*
* Record that you have charged the current user a certain amount
* of money. Charges recorded with track_charge() will appear in the
* Mixpanel revenue report.
*
* ### Usage:
*
* // charge a user $50
* mixpanel.people.track_charge(50);
*
* // charge a user $30.50 on the 2nd of january
* mixpanel.people.track_charge(30.50, {
* '$time': new Date('jan 1 2012')
* });
*
* @param {Number} amount The amount of money charged to the current user
* @param {Object} [properties] An associative array of properties associated with the charge
* @param {Function} [callback] If provided, the callback will be called when the server responds
*/
MixpanelPeople.prototype.track_charge = addOptOutCheckMixpanelPeople(function(amount, properties, callback) {
if (!_.isNumber(amount)) {
amount = parseFloat(amount);
if (isNaN(amount)) {
console.error('Invalid value passed to mixpanel.people.track_charge - must be a number');
return;
}
}
return this.append('$transactions', _.extend({
'$amount': amount
}, properties), callback);
});
/*
* Permanently clear all revenue report transactions from the
* current user's people analytics profile.
*
* ### Usage:
*
* mixpanel.people.clear_charges();
*
* @param {Function} [callback] If provided, the callback will be called after tracking the event.
*/
MixpanelPeople.prototype.clear_charges = function(callback) {
return this.set('$transactions', [], callback);
};
/*
* Permanently deletes the current people analytics profile from
* Mixpanel (using the current distinct_id).
*
* ### Usage:
*
* // remove the all data you have stored about the current user
* mixpanel.people.delete_user();
*
*/
MixpanelPeople.prototype.delete_user = function() {
if (!this._identify_called()) {
console.error('mixpanel.people.delete_user() requires you to call identify() first');
return;
}
var data = {'$delete': this._mixpanel.get_distinct_id()};
return this._send_request(data);
};
MixpanelPeople.prototype.toString = function() {
return this._mixpanel.toString() + '.people';
};
MixpanelPeople.prototype._send_request = function(data, callback) {
data['$token'] = this._get_config('token');
data['$distinct_id'] = this._mixpanel.get_distinct_id();
var device_id = this._mixpanel.get_property('$device_id');
var user_id = this._mixpanel.get_property('$user_id');
var had_persisted_distinct_id = this._mixpanel.get_property('$had_persisted_distinct_id');
if (device_id) {
data['$device_id'] = device_id;
}
if (user_id) {
data['$user_id'] = user_id;
}
if (had_persisted_distinct_id) {
data['$had_persisted_distinct_id'] = had_persisted_distinct_id;
}
var date_encoded_data = _.encodeDates(data);
if (!this._identify_called()) {
this._enqueue(data);
if (!_.isUndefined(callback)) {
if (this._get_config('verbose')) {
callback({status: -1, error: null});
} else {
callback(-1);
}
}
return _.truncate(date_encoded_data, 255);
}
return this._mixpanel._track_or_batch({
type: 'people',
data: date_encoded_data,
endpoint: this._get_config('api_host') + '/engage/',
batcher: this._mixpanel.request_batchers.people
}, callback);
};
MixpanelPeople.prototype._get_config = function(conf_var) {
return this._mixpanel.get_config(conf_var);
};
MixpanelPeople.prototype._identify_called = function() {
return this._mixpanel._flags.identify_called === true;
};
// Queue up engage operations if identify hasn't been called yet.
MixpanelPeople.prototype._enqueue = function(data) {
if (SET_ACTION in data) {
this._mixpanel['persistence']._add_to_people_queue(SET_ACTION, data);
} else if (SET_ONCE_ACTION in data) {
this._mixpanel['persistence']._add_to_people_queue(SET_ONCE_ACTION, data);
} else if (UNSET_ACTION in data) {
this._mixpanel['persistence']._add_to_people_queue(UNSET_ACTION, data);
} else if (ADD_ACTION in data) {
this._mixpanel['persistence']._add_to_people_queue(ADD_ACTION, data);
} else if (APPEND_ACTION in data) {
this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, data);
} else if (REMOVE_ACTION in data) {
this._mixpanel['persistence']._add_to_people_queue(REMOVE_ACTION, data);
} else if (UNION_ACTION in data) {
this._mixpanel['persistence']._add_to_people_queue(UNION_ACTION, data);
} else {
console.error('Invalid call to _enqueue():', data);
}
};
MixpanelPeople.prototype._flush_one_queue = function(action, action_method, callback, queue_to_params_fn) {
var _this = this;
var queued_data = _.extend({}, this._mixpanel['persistence']._get_queue(action));
var action_params = queued_data;
if (!_.isUndefined(queued_data) && _.isObject(queued_data) && !_.isEmptyObject(queued_data)) {
_this._mixpanel['persistence']._pop_from_people_queue(action, queued_data);
if (queue_to_params_fn) {
action_params = queue_to_params_fn(queued_data);
}
action_method.call(_this, action_params, function(response, data) {
// on bad response, we want to add it back to the queue
if (response === 0) {
_this._mixpanel['persistence']._add_to_people_queue(action, queued_data);
}
if (!_.isUndefined(callback)) {
callback(response, data);
}
});
}
};
// Flush queued engage operations - order does not matter,
// and there are network level race conditions anyway
MixpanelPeople.prototype._flush = function(
_set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback
) {
var _this = this;
var $append_queue = this._mixpanel['persistence']._get_queue(APPEND_ACTION);
var $remove_queue = this._mixpanel['persistence']._get_queue(REMOVE_ACTION);
this._flush_one_queue(SET_ACTION, this.set, _set_callback);
this._flush_one_queue(SET_ONCE_ACTION, this.set_once, _set_once_callback);
this._flush_one_queue(UNSET_ACTION, this.unset, _unset_callback, function(queue) { return _.keys(queue); });
this._flush_one_queue(ADD_ACTION, this.increment, _add_callback);
this._flush_one_queue(UNION_ACTION, this.union, _union_callback);
// we have to fire off each $append individually since there is
// no concat method server side
if (!_.isUndefined($append_queue) && _.isArray($append_queue) && $append_queue.length) {
var $append_item;
var append_callback = function(response, data) {
if (response === 0) {
_this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, $append_item);
}
if (!_.isUndefined(_append_callback)) {
_append_callback(response, data);
}
};
for (var i = $append_queue.length - 1; i >= 0; i--) {
$append_item = $append_queue.pop();
if (!_.isEmptyObject($append_item)) {
_this.append($append_item, append_callback);
}
}
// Save the shortened append queue
_this._mixpanel['persistence'].save();
}
// same for $remove
if (!_.isUndefined($remove_queue) && _.isArray($remove_queue) && $remove_queue.length) {
var $remove_item;
var remove_callback = function(response, data) {
if (response === 0) {
_this._mixpanel['persistence']._add_to_people_queue(REMOVE_ACTION, $remove_item);
}
if (!_.isUndefined(_remove_callback)) {
_remove_callback(response, data);
}
};
for (var j = $remove_queue.length - 1; j >= 0; j--) {
$remove_item = $remove_queue.pop();
if (!_.isEmptyObject($remove_item)) {
_this.remove($remove_item, remove_callback);
}
}
_this._mixpanel['persistence'].save();
}
};
MixpanelPeople.prototype._is_reserved_property = function(prop) {
return prop === '$distinct_id' || prop === '$token' || prop === '$device_id' || prop === '$user_id' || prop === '$had_persisted_distinct_id';
};
// MixpanelPeople Exports
MixpanelPeople.prototype['set'] = MixpanelPeople.prototype.set;
MixpanelPeople.prototype['set_once'] = MixpanelPeople.prototype.set_once;
MixpanelPeople.prototype['unset'] = MixpanelPeople.prototype.unset;
MixpanelPeople.prototype['increment'] = MixpanelPeople.prototype.increment;
MixpanelPeople.prototype['append'] = MixpanelPeople.prototype.append;
MixpanelPeople.prototype['remove'] = MixpanelPeople.prototype.remove;
MixpanelPeople.prototype['union'] = MixpanelPeople.prototype.union;
MixpanelPeople.prototype['track_charge'] = MixpanelPeople.prototype.track_charge;
MixpanelPeople.prototype['clear_charges'] = MixpanelPeople.prototype.clear_charges;
MixpanelPeople.prototype['delete_user'] = MixpanelPeople.prototype.delete_user;
MixpanelPeople.prototype['toString'] = MixpanelPeople.prototype.toString;
export { MixpanelPeople };