diff --git a/bower.json b/bower.json index b49bfbc7..ea145423 100644 --- a/bower.json +++ b/bower.json @@ -15,6 +15,7 @@ "tests" ], "dependencies": { - "polymer": "Polymer/polymer#^1.1.0" + "polymer": "Polymer/polymer#^1.1.0", + "angular-mixpanel": "~1.1.2" } } diff --git a/lib/browser/app.js b/lib/browser/app.js index 9142a3f7..5f818443 100644 --- a/lib/browser/app.js +++ b/lib/browser/app.js @@ -29,31 +29,31 @@ const BrowserWindow = electron.remote.BrowserWindow; const currentWindow = BrowserWindow.fromId(1); require('angular-ui-bootstrap'); -require('./browser/modules/track'); require('./browser/modules/selection-state'); require('./browser/modules/drive-scanner'); require('./browser/modules/image-writer'); require('./browser/modules/path'); +require('./browser/modules/analytics'); const app = angular.module('Etcher', [ 'ui.bootstrap', - 'TrackJS', // Etcher modules 'Etcher.path', 'Etcher.selection-state', 'Etcher.drive-scanner', - 'Etcher.image-writer' + 'Etcher.image-writer', + 'Etcher.analytics' ]); -app.controller('AppController', function($q, $log, DriveScannerService, SelectionStateService, ImageWriterService) { +app.controller('AppController', function($q, DriveScannerService, SelectionStateService, ImageWriterService, AnalyticsService) { let self = this; this.selection = SelectionStateService; this.writer = ImageWriterService; this.scanner = DriveScannerService; this.restart = function(options) { - $log.debug('Restarting'); + AnalyticsService.logEvent('Restart'); this.selection.clear(options); self.state = { @@ -75,7 +75,9 @@ app.controller('AppController', function($q, $log, DriveScannerService, Selectio // `angular.equals` is used instead of `_.isEqual` to // cope with `$$hashKey`. if (!angular.equals(self.selection.getDrive(), drive)) { - $log.debug(`Autoselecting drive: ${drive.device}`); + AnalyticsService.logEvent('Auto-select drive', { + device: drive.device + }); self.selectDrive(drive); } @@ -99,13 +101,17 @@ app.controller('AppController', function($q, $log, DriveScannerService, Selectio this.selectImage = function() { return $q.when(dialog.selectImage()).then(function(image) { self.selection.setImage(image); - $log.debug(`Image selected: ${image}`); + AnalyticsService.logEvent('Select image', { + image: image + }); }); }; this.selectDrive = function(drive) { self.selection.setDrive(drive); - $log.debug(`Drive selected: ${drive.device}`); + AnalyticsService.logEvent('Select drive', { + device: drive.device + }); }; this.reselectImage = function() { @@ -119,7 +125,7 @@ app.controller('AppController', function($q, $log, DriveScannerService, Selectio // "returns" to the first step. self.selection.clear(); - $log.debug('Reselecting image'); + AnalyticsService.logEvent('Reselect image'); }; this.reselectDrive = function() { @@ -128,7 +134,7 @@ app.controller('AppController', function($q, $log, DriveScannerService, Selectio } self.selection.removeDrive(); - $log.debug('Reselecting drive'); + AnalyticsService.logEvent('Reselect drive'); }; this.burn = function(image, drive) { @@ -137,16 +143,20 @@ app.controller('AppController', function($q, $log, DriveScannerService, Selectio // otherwise Windows throws EPERM self.scanner.stop(); - $log.debug(`Burning ${image} to ${drive.device}`); + AnalyticsService.logEvent('Burn', { + image: image, + device: drive.device + }); + return self.writer.burn(image, drive, function(state) { self.state = state; - $log.debug(`Progress: ${self.state.progress}% at ${self.state.speed} MB/s`); + AnalyticsService.log(`Progress: ${self.state.progress}% at ${self.state.speed} MB/s`); // Show progress inline in operating system task bar currentWindow.setProgressBar(self.state.progress / 100); }).then(function() { - $log.debug('Done!'); + AnalyticsService.logEvent('Done'); }).catch(dialog.showError).finally(function() { // Remove progress bar from task bar diff --git a/lib/browser/modules/analytics.js b/lib/browser/modules/analytics.js new file mode 100644 index 00000000..b5775670 --- /dev/null +++ b/lib/browser/modules/analytics.js @@ -0,0 +1,130 @@ +/* + * Copyright 2016 Resin.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +/** + * @module Etcher.analytics + */ + +const _ = require('lodash'); +const angular = require('angular'); +const username = require('username'); +const app = require('electron').remote.app; + +// Force Mixpanel snippet to load Mixpanel locally +// instead of using a CDN for performance reasons +window.MIXPANEL_CUSTOM_LIB_URL = '../bower_components/mixpanel/mixpanel.js'; + +require('../../../bower_components/mixpanel/mixpanel-jslib-snippet.js'); +require('../../../bower_components/angular-mixpanel/src/angular-mixpanel'); +const analytics = angular.module('Etcher.analytics', [ + 'analytics.mixpanel' +]); + +analytics.config(function($mixpanelProvider) { + $mixpanelProvider.apiKey('63e5fc4563e00928da67d1226364dd4c'); + + $mixpanelProvider.superProperties({ + + // jscs:disable requireCamelCaseOrUpperCaseIdentifiers + + distinct_id: username.sync(), + + // jscs:enable requireCamelCaseOrUpperCaseIdentifiers + + electron: app.getVersion(), + node: process.version, + arch: process.arch + }); +}); + +// TrackJS integration +// http://docs.trackjs.com/tracker/framework-integrations +analytics.config(function($provide) { + $provide.decorator('$exceptionHandler', function($delegate, $window) { + return function(exception, cause) { + $window.trackJs.track(exception); + $delegate(exception, cause); + }; + }); + + $provide.decorator('$log', function($delegate, $window) { + + // Save the original $log.debug() + let debugFn = $delegate.debug; + + $delegate.debug = function(message) { + message = new Date() + ' ' + message; + $window.trackJs.console.debug(message); + debugFn.call(null, message); + }; + + return $delegate; + }); +}); + +analytics.service('AnalyticsService', function($log, $mixpanel) { + let self = this; + + /** + * @summary Log a debug message + * @function + * @public + * + * @description + * This function sends the debug message to TrackJS only. + * + * @param {String} message - message + * + * @example + * AnalyticsService.log('Hello World'); + */ + this.log = function(message) { + $log.debug(message); + }; + + /** + * @summary Log an event + * @function + * @public + * + * @description + * This function sends the debug message to TrackJS and Mixpanel. + * + * @param {String} message - message + * @param {Object} [data] - event data + * + * @example + * AnalyticsService.logEvent('Select image', { + * image: '/dev/disk2' + * }); + */ + this.logEvent = function(message, data) { + + // Clone data before passing it to `mixpanel.track` + // since this function mutates the object adding + // some custom private Mixpanel properties. + $mixpanel.track(message, _.clone(data)); + + if (data) { + message += ` (${JSON.stringify(data)})`; + } + + self.log(message); + }; + +}); diff --git a/lib/browser/modules/track.js b/lib/browser/modules/track.js deleted file mode 100644 index 515d5501..00000000 --- a/lib/browser/modules/track.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2016 Resin.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict'; - -/** - * @module TrackJS - */ - -const angular = require('angular'); -const track = angular.module('TrackJS', []); - -// TrackJS integration -// http://docs.trackjs.com/tracker/framework-integrations -track.config(function($provide) { - $provide.decorator('$exceptionHandler', function($delegate, $window) { - return function(exception, cause) { - $window.trackJs.track(exception); - $delegate(exception, cause); - }; - }); - - $provide.decorator('$log', function($delegate, $window) { - - // Save the original $log.debug() - let debugFn = $delegate.debug; - - $delegate.debug = function(message) { - message = new Date() + ' ' + message; - $window.trackJs.console.debug(message); - debugFn.call(null, message); - }; - - return $delegate; - }); -}); diff --git a/package.json b/package.json index 07953bf6..5b9090b1 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,8 @@ "resin-image-write": "^2.0.5", "sudo-prompt": "^2.2.0", "trackjs": "^2.1.16", - "umount": "^1.1.1" + "umount": "^1.1.1", + "username": "^2.1.0" }, "devDependencies": { "angular-mocks": "^1.4.7",