UNPKG

ionic

Version:

A tool for creating and developing Ionic Framework mobile apps.

751 lines (611 loc) • 20.2 kB
/* eslint-disable camelcase, no-underscore-dangle */ var Buffer = require('buffer').Buffer; var http = require('http'); var IonicConfig = require('./config'); var path = require('path'); var util = require('util'); var Q = require('q'); var querystring = require('querystring'); /* Heavily inspired by the original js library copyright Mixpanel, Inc. (http://mixpanel.com/) Copyright (c) 2012 Carl Sverre Released under the MIT license. */ var Stats = module.exports; // Old code - previously used exclusively by CLI. This is changing // Now we want a create client for a token passed. // No longer do we want to assume a default client. // Instead, we initialize one with a token passed in. function create_client(token, config) { var metrics = {}; if (!token) { throw new Error('The Mixpanel Client needs a Mixpanel token: `init(token)`'); } metrics.config = { test: false, debug: false, verbose: false }; metrics.token = token; /** send_request(data) --- this function sends an async GET request to mixpanel data:object the data to send in the request callback:function(err:Error) callback is called when the request is finished or an error occurs */ metrics.send_request = function(endpoint, data, callback) { callback = callback || function() {}; 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 (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: ... })`'); } request_data.api_key = key; } var request_options = { host: 'api.mixpanel.com', port: 80, headers: {} }; if (metrics.config.test) { request_data.test = 1; } var query = querystring.stringify(request_data); request_options.path = [endpoint, '?', query].join(''); http.get(request_options, function(res) { var data = ''; res.on('data', function(chunk) { data += chunk; }); res.on('end', function() { var e; if (metrics.config.verbose) { try { var result = JSON.parse(data); if (result.status !== 1) { e = new Error('Mixpanel Server Error: ' + result.error); } } catch (ex) { e = new Error('Could not parse response from Mixpanel'); } } else { e = (data !== '1') ? new Error('Mixpanel Server Error: ' + data) : undefined; } callback(e); }); }).on('error', function(e) { if (metrics.config.debug) { console.log('Got Error: ' + e.message); } callback(e); }); }; /** track(event, properties, callback) --- this function sends an event to mixpanel. event:string the event name properties:object additional event properties to send callback:function(err:Error) callback is called when the request is finished or an error occurs */ metrics.track = function(event, properties, callback) { if (typeof(properties) === 'function' || !properties) { callback = properties; properties = {}; } // if properties.time exists, use import endpoint var endpoint = (typeof(properties.time) === 'number') ? '/import' : '/track'; properties.token = metrics.token; properties.mp_lib = 'node'; var data = { event: event, properties: properties }; if (metrics.config.debug) { console.log('Sending the following event to Mixpanel:'); console.log(data); } metrics.send_request(endpoint, data, callback); }; /** import(event, properties, callback) --- This function sends an event to mixpanel using the import endpoint. The time argument should be either a Date or Number, and should signify the time the event occurred. It is highly recommended that you specify the distinct_id property for each event you import, otherwise the events will be tied to the IP address of the sending machine. For more information look at: https://mixpanel.com/docs/api-documentation/importing-events-older-than-31-days event:string the event name time:date|number the time of the event properties:object additional event properties to send callback:function(err:Error) callback is called when the request is finished or an error occurs */ metrics.import = function(event, time, properties, callback) { if (typeof(properties) === 'function' || !properties) { callback = properties; 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 = time; metrics.track(event, properties, callback); }; /** alias(distinct_id, alias) --- This function creates an alias for distinct_id For more information look at: https://mixpanel.com/docs/integration-libraries/using-mixpanel-alias distinct_id:string the current identifier alias:string the future alias */ metrics.alias = function(distinct_id, alias, callback) { var properties = { distinct_id: distinct_id, alias: alias }; metrics.track('$create_alias', properties, callback); }; metrics.people = { /** people.set_once(distinct_id, prop, to, callback) --- The same as people.set but in the words of mixpanel: mixpanel.people.set_once " This method allows you to set a user attribute, only if it is not currently set. It can be called multiple times safely, so is perfect for storing things like the first date you saw a user, or the referrer that brought them to your website for the first time. " */ set_once: function(distinct_id, prop, to, callback) { var $set = {}; if (typeof(prop) === 'object') { callback = to; $set = prop; } else { $set[prop] = to; } this._set(distinct_id, $set, callback, { set_once: true }); }, /** people.set(distinct_id, prop, to, callback) --- set properties on an user record in engage usage: mixpanel.people.set('bob', 'gender', 'm'); mixpanel.people.set('joe', { 'company': 'acme', 'plan': 'premium' }); */ set: function(distinct_id, prop, to, callback) { var $set = {}; if (typeof(prop) === 'object') { callback = to; $set = prop; } else { $set[prop] = to; } this._set(distinct_id, $set, callback); }, // used internally by set and set_once _set: function(distinct_id, $set, callback, options) { var set_key = (options && options.set_once) ? '$set_once' : '$set'; var data = { $token: metrics.token, $distinct_id: distinct_id }; data[set_key] = $set; if ('ip' in $set) { data.$ip = $set.ip; delete $set.ip; } if ($set.$ignore_time) { data.$ignore_time = $set.$ignore_time; delete $set.$ignore_time; } if (metrics.config.debug) { console.log('Sending the following data to Mixpanel (Engage):'); console.log(data); } metrics.send_request('/engage', data, callback); }, /** people.increment(distinct_id, prop, to, callback) --- increment/decrement properties on an user record in engage usage: mixpanel.people.increment('bob', 'page_views', 1); // or, for convenience, if you're just incrementing a counter by 1, you can // simply do mixpanel.people.increment('bob', 'page_views'); // to decrement a counter, pass a negative number mixpanel.people.increment('bob', 'credits_left', -1); // like mixpanel.people.set(), you can increment multiple properties at once: mixpanel.people.increment('bob', { counter1: 1, counter2: 3, counter3: -2 }); */ increment: function(distinct_id, prop, by, callback) { var $add = {}; if (typeof(prop) === 'object') { callback = by; Object.keys(prop).forEach(function(key) { var val = prop[key]; if (isNaN(parseFloat(val))) { if (metrics.config.debug) { console.error('Invalid increment value passed to mixpanel.people.increment - must be a number'); console.error('Passed ' + key + ':' + val); } return; } else { $add[key] = val; } }); } else { if (!by) { by = 1; } $add[prop] = by; } var data = { $add: $add, $token: metrics.token, $distinct_id: distinct_id }; if (metrics.config.debug) { console.log('Sending the following data to Mixpanel (Engage):'); console.log(data); } metrics.send_request('/engage', data, callback); }, /** people.track_charge(distinct_id, amount, properties, callback) --- Record that you have charged the current user a certain amount of money. usage: // charge a user $29.99 mixpanel.people.track_charge('bob', 29.99); // 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) { if (!properties) { properties = {}; } if (typeof(amount) !== 'number') { amount = parseFloat(amount); if (isNaN(amount)) { console.error('Invalid value passed to mixpanel.people.track_charge - must be a number'); return; } } properties.$amount = amount; if (properties.hasOwnProperty('$time')) { var time = properties.$time; if (Object.prototype.toString.call(time) === '[object Date]') { properties.$time = time.toISOString(); } } var data = { $append: { $transactions: properties }, $token: metrics.token, $distinct_id: distinct_id }; if (metrics.config.debug) { console.log('Sending the following data to Mixpanel (Engage):'); console.log(data); } metrics.send_request('/engage', data, callback); }, /** people.clear_charges(distinct_id, callback) --- Clear all the current user's transactions. usage: mixpanel.people.clear_charges('bob'); */ clear_charges: function(distinct_id, callback) { var data = { $set: { $transactions: [] }, $token: metrics.token, $distinct_id: distinct_id }; if (metrics.config.debug) { console.log("Clearing this user's charges:", distinct_id); } metrics.send_request('/engage', data, callback); }, /** people.delete_user(distinct_id, callback) --- delete an user record in engage usage: mixpanel.people.delete_user('bob'); */ delete_user: function(distinct_id, callback) { var data = { $delete: distinct_id, $token: metrics.token, $distinct_id: distinct_id }; if (metrics.config.debug) { console.log('Deleting the user from engage:', distinct_id); } metrics.send_request('/engage', data, callback); }, /** people.unset(distinct_id, prop, callback) --- delete a property on an user record in engage usage: mixpanel.people.unset('bob', 'page_views'); mixpanel.people.unset('bob', ['page_views', 'last_login']); */ unset: function(distinct_id, prop, callback) { var $unset = []; if (util.isArray(prop)) { $unset = prop; } else if (typeof(prop) === 'string') { $unset = [prop]; } else { if (metrics.config.debug) { console.error('Invalid argument passed to mixpanel.people.unset - must be a string or array'); console.error('Passed: ' + prop); } return; } var data = { $unset: $unset, $token: metrics.token, $distinct_id: distinct_id }; if (metrics.config.debug) { console.log('Sending the following data to Mixpanel (Engage):'); console.log(data); } metrics.send_request('/engage', data, callback); } }; /** set_config(config) --- Modifies the mixpanel config config:object an object with properties to override in the mixpanel client config */ metrics.set_config = function(config) { for (var c in config) { if (config.hasOwnProperty(c)) { metrics.config[c] = config[c]; } } }; if (config) { metrics.set_config(config); } return metrics; } // module exporting /* module.exports = { Client: function(token) { console.warn("The function `Client(token)` is deprecated. It is now called `init(token)`."); return create_client(token); }, init: create_client }; */ var mixpanel = create_client('69f7271aa8f3d43f2e1b6baf698159b7'); var ionicConfig = IonicConfig.load(); exports.IonicStats = { t: function(additionalData) { try { if (process.argv.length < 3) return; if (ionicConfig.get('statsOptOut') === true) { return; } var cmdName = process.argv[2].toLowerCase(); var cmdArgs = (process.argv.length > 3 ? process.argv.slice(3) : []); // skip the cmdName var statsData = additionalData || {}; var platforms = []; var x; var y; var cmd; // update any aliases with the full cmd so there's a common property var aliasMap = { rm: 'remove', ls: 'list', up: 'update', '-w': '--no-cordova', '-b': '--nobrowser', '-r': '--nolivereload', '-x': '--noproxy', '-l': '--livereload', '-c': '--consolelogs', '-s': '--serverlogs', '-n': '--no-email' }; for (x = 0; x < cmdArgs.length; x += 1) { for (y in aliasMap) { if (cmdArgs[x].toLowerCase() === y) { cmdArgs[x] = aliasMap[y]; } } } var platformWhitelist = [ 'android', 'ios', 'firefoxos', 'wp7', 'wp8', 'amazon-fireos', 'blackberry10', 'tizen' ]; var argsWhitelist = [ 'add', 'remove', 'list', 'update', 'check', 'debug', 'release', 'search', '--livereload', '--consolelogs', '--serverlogs', '--no-cordova', '--nobrowser', '--nolivereload', '--noproxy', '--no-email', '--debug', '--release', '--device', '--emulator', '--sass', '--splash', '--icon' ]; // collect only certain args, skip over everything else for (x = 0; x < cmdArgs.length; x += 1) { cmd = cmdArgs[x].toLowerCase(); // gather what platforms this is targeting for (y = 0; y < platformWhitelist.length; y += 1) { if (cmd === platformWhitelist[y]) { platforms.push(cmd); // group them together statsData[cmd] = true; // also give them their own property break; } } // gather only args that are in our list of valid stat args for (y = 0; y < argsWhitelist.length; y += 1) { if (cmd === argsWhitelist[y]) { statsData[cmd] = true; break; } } } // create a platform property only when there is 1 or more if (platforms.length) { statsData.platform = platforms.sort().join(','); } // add which ionic lib version they're using try { statsData.ionic_version = require(path.resolve('www/lib/ionic/version.json')).version; } catch (e2) {} // eslint-disable-line no-empty // add which cli version is being used try { statsData.cli_version = require('../../package.json').version; if (statsData.cli_version.indexOf('beta') > -1) return; } catch (e2) {} // eslint-disable-line no-empty this.mp(cmdName, statsData); } catch (e) { console.log(('Error stats: ' + e)); } }, mp: function(e, d) { var unique_id = ionicConfig.get('ank'); if (!unique_id) { this.createId(); unique_id = ionicConfig.get('ank'); } d.distinct_id = unique_id; mixpanel.track(e, d, function() {}); // eslint-disable-line no-empty }, createId: function() { var d = new Date().getTime(); var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = (d + Math.random() * 16) % 16 | 0; d = Math.floor(d / 16); return (c === 'x' ? r : (r & 0x7 | 0x8)).toString(16); }); ionicConfig.set('ank', uuid); } }; // Default null client Stats.client = null; // New Stats code Stats.initClient = function initClient(token) { if (!token) { throw new Error('You must pass a token back to initialize a stat client'); } return Stats.client = create_client(token); }; Stats.createId = function createId() { var d = new Date().getTime(); var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = (d + Math.random() * 16) % 16 | 0; d = Math.floor(d / 16); return (c === 'x' ? r : (r & 0x7 | 0x8)).toString(16); }); ionicConfig.set('ank', uuid); return uuid; }; Stats.getUniqueId = function getUniqueId() { var uid = ionicConfig.get('ank'); if (!uid) { uid = Stats.createId(); } return uid; }; Stats.trackAction = function trackAction(appDirectory, command, additionalData) { if (!Stats.client) { throw new Error('No client is available'); } var statsOptOut = ionicConfig.get('statsOptOut'); if (statsOptOut === true) { return Q.resolve('Stats opt out'); } var q = Q.defer(); var statsData = additionalData || {}; statsData = Stats.gatherAdditionalruntimeStats(appDirectory, statsData); Stats.client.track(command, statsData, function(err, data) { if (err) { q.reject(err); } else { q.resolve(data); } }); return q.promise; }; Stats.gatherAdditionalruntimeStats = function gatherAdditionalruntimeStats(appDirectory, additionalData) { var statsData = additionalData || {}; statsData.distinct_id = Stats.getUniqueId(); // add which ionic lib version they're using try { var appJsonPath = path.join(appDirectory, 'www', 'lib', 'ionic', 'version.json'); statsData.ionic_version = require(appJsonPath).version; } catch (e2) { console.log('coulndt get ionic version'); } // add which cli version is being used try { statsData.gui_version = require('../../package.json').version; if (statsData.gui_version.indexOf('beta') > -1) return; } catch (e2) {} // eslint-disable-line no-empty console.log('Additional stats:', statsData); console.log('Additional stats:', statsData); console.log('Additional stats:', statsData); return statsData; };