diff --git a/gulpfile.js b/gulpfile.js index 98fd33f140..937b8a1cd8 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -17,10 +17,18 @@ var packagejson = require('./package.json'); var dependencies = Object.keys(packagejson.dependencies); var isBeta = process.argv.indexOf('--beta') !== -1; + +var settings; +try { + settings = JSON.parse(fs.readFileSync('settings.json'), 'utf8'); +} catch (err) { + settings = {}; +} +settings.beta = isBeta; + var options = { dev: process.argv.indexOf('release') === -1 && process.argv.indexOf('test') === -1, test: process.argv.indexOf('test') !== -1, - integration: process.argv.indexOf('--integration') !== -1, beta: isBeta, filename: isBeta ? 'Kitematic (Beta).app' : 'Kitematic.app', name: isBeta ? 'Kitematic (Beta)' : 'Kitematic', @@ -29,6 +37,11 @@ var options = { gulp.task('js', function () { return gulp.src('src/**/*.js') + .pipe(plumber(function(error) { + gutil.log(gutil.colors.red('Error (' + error.plugin + '): ' + error.message)); + // emit the end event, to properly end the task + this.emit('end'); + })) .pipe(gulpif(options.dev || options.test, sourcemaps.init())) .pipe(react()) .pipe(babel({blacklist: ['regenerator']})) @@ -86,7 +99,6 @@ gulp.task('dist', function () { 'mkdir -p ./dist/osx/<%= filename %>/Contents/Resources/app/node_modules', 'cp -R browser dist/osx/<%= filename %>/Contents/Resources/app', 'cp package.json dist/osx/<%= filename %>/Contents/Resources/app/', - 'cp settings.json dist/osx/<%= filename %>/Contents/Resources/app/', 'mkdir -p dist/osx/<%= filename %>/Contents/Resources/app/resources', 'cp -v resources/* dist/osx/<%= filename %>/Contents/Resources/app/resources/ || :', 'cp <%= icon %> dist/osx/<%= filename %>/Contents/Resources/atom.icns', @@ -139,8 +151,20 @@ gulp.task('zip', function () { })); }); +gulp.task('settings', function () { + var string_src = function (filename, string) { + var src = require('stream').Readable({ objectMode: true }); + src._read = function () { + this.push(new gutil.File({ cwd: "", base: "", path: filename, contents: new Buffer(string) })); + this.push(null); + }; + return src; + }; + string_src('settings.json', JSON.stringify(settings)).pipe(gulp.dest('dist/osx/' + options.filename.replace(' ', '\ ').replace('(','\(').replace(')','\)') + '/Contents/Resources/app')); +}); + gulp.task('release', function () { - runSequence('download', 'dist', ['copy', 'images', 'js', 'styles'], 'sign', 'zip'); + runSequence('download', 'dist', ['copy', 'images', 'js', 'styles', 'settings'], 'sign', 'zip'); }); gulp.task('default', ['download', 'copy', 'js', 'images', 'styles'], function () { diff --git a/package.json b/package.json index 7a764c95b7..dbf4ea630d 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "bugs": "https://github.com/kitematic/kitematic/issues", "scripts": { "start": "gulp", - "test": "jest", + "test": "NODE_PATH=./cache/Atom.app/Contents/Resources/atom/renderer/api/lib:$NODE_PATH jest", "release": "gulp release", "release:beta": "gulp release --beta", "preinstall": "./deps", @@ -26,6 +26,7 @@ ], "jest": { "scriptPreprocessor": "/preprocessor.js", + "setupEnvScriptFile": "/testenv.js", "unmockedModulePathPatterns": [ "tty", "net", @@ -41,21 +42,20 @@ "docker-version": "1.5.0", "boot2docker-version": "1.5.0", "atom-shell-version": "0.21.1", - "virtualbox-version": "4.3.20", - "virtualbox-filename": "VirtualBox-4.3.20.pkg", - "virtualbox-checksum": "89edac4cc7298c8a04fd4bb646ff2197e7673137c6566c7757f0e9cd6265d0c5", - "virtualbox-required-version": "4.3.18", + "virtualbox-version": "4.3.22", + "virtualbox-filename": "VirtualBox-4.3.22.pkg", + "virtualbox-checksum": "4a7dff25bdeef0d112e16ac11bee6d52e856d36bb412aa75576036ba560082eb", + "virtualbox-required-version": "4.3.12", "dependencies": { "ansi-to-html": "0.2.0", "async": "^0.9.0", - "babel": "^4.0.1", "bluebird": "^2.9.6", "bugsnag-js": "^2.4.7", "dockerode": "^2.0.7", - "download": "^4.0.0", "exec": "0.1.2", "jquery": "^2.1.3", "minimist": "^1.1.0", + "mixpanel": "0.0.20", "node-uuid": "^1.4.2", "object-assign": "^2.0.0", "react": "^0.12.2", @@ -69,13 +69,13 @@ "underscore": "^1.7.0" }, "devDependencies": { + "babel": "^4.0.1", "browserify": "^6.2.0", "ecstatic": "^0.5.8", "glob": "^4.0.6", "gulp": "^3.8.10", - "gulp-babel": "^3.0.0", - "gulp-atom": "0.0.5", "gulp-babel": "^4.0.0", + "gulp-atom": "0.0.5", "gulp-concat": "^2.3.4", "gulp-cssmin": "^0.1.6", "gulp-download-atom-shell": "0.0.4", diff --git a/src/ContainerDetailsSubheader.react.js b/src/ContainerDetailsSubheader.react.js index 28bd6182fb..f40727ae49 100644 --- a/src/ContainerDetailsSubheader.react.js +++ b/src/ContainerDetailsSubheader.react.js @@ -3,6 +3,7 @@ var $ = require('jquery'); var React = require('react/addons'); var exec = require('exec'); var path = require('path'); +var metrics = require('./Metrics'); var ContainerStore = require('./ContainerStore'); var ContainerUtil = require('./ContainerUtil'); var boot2docker = require('./Boot2Docker'); @@ -65,21 +66,27 @@ var ContainerDetailsSubheader = React.createClass({ }, showHome: function () { if (!this.disableTab()) { + metrics.track('Viewed Home'); this.transitionTo('containerHome', {name: this.getParams().name}); } }, showLogs: function () { if (!this.disableTab()) { + metrics.track('Viewed Logs'); this.transitionTo('containerLogs', {name: this.getParams().name}); } }, showSettings: function () { if (!this.disableTab()) { + metrics.track('Viewed Settings'); this.transitionTo('containerSettings', {name: this.getParams().name}); } }, handleRun: function () { if (this.state.defaultPort && !this.disableRun()) { + metrics.track('Opened In Browser', { + from: 'header' + }); exec(['open', this.state.ports[this.state.defaultPort].url], function (err) { if (err) { throw err; } }); @@ -87,6 +94,7 @@ var ContainerDetailsSubheader = React.createClass({ }, handleRestart: function () { if (!this.disableRestart()) { + metrics.track('Restarted Container'); ContainerStore.restart(this.props.container.Name, function (err) { console.log(err); }); @@ -94,12 +102,11 @@ var ContainerDetailsSubheader = React.createClass({ }, handleTerminal: function () { if (!this.disableTerminal()) { + metrics.track('Terminaled Into Container'); var container = this.props.container; var terminal = path.join(process.cwd(), 'resources', 'terminal'); var cmd = [terminal, boot2docker.command().replace(/ /g, '\\\\\\\\ ').replace(/\(/g, '\\\\\\\\(').replace(/\)/g, '\\\\\\\\)'), 'ssh', '-t', 'sudo', 'docker', 'exec', '-i', '-t', container.Name, 'sh']; exec(cmd, function (stderr, stdout, code) { - console.log(stderr); - console.log(stdout); if (code) { console.log(stderr); } diff --git a/src/ContainerHomeFolders.react.js b/src/ContainerHomeFolders.react.js index 46b1bba7a4..513cc974b5 100644 --- a/src/ContainerHomeFolders.react.js +++ b/src/ContainerHomeFolders.react.js @@ -3,16 +3,23 @@ var React = require('react/addons'); var RetinaImage = require('react-retina-image'); var path = require('path'); var exec = require('exec'); +var metrics = require('./Metrics'); var Router = require('react-router'); var ContainerHomeFolder = React.createClass({ mixins: [Router.State, Router.Navigation], handleClickFolder: function (path) { + metrics.track('Opened Volume Directory', { + from: 'home' + }); exec(['open', path], function (err) { if (err) { throw err; } }); }, handleClickChangeFolders: function () { + metrics.track('Viewed Volume Settings', { + from: 'preview' + }); this.transitionTo('containerSettingsVolumes', {name: this.getParams().name}); }, render: function () { diff --git a/src/ContainerHomeLogs.react.js b/src/ContainerHomeLogs.react.js index d64d8b75be..dcaaa95cff 100644 --- a/src/ContainerHomeLogs.react.js +++ b/src/ContainerHomeLogs.react.js @@ -2,6 +2,7 @@ var $ = require('jquery'); var React = require('react/addons'); var LogStore = require('./LogStore'); var Router = require('react-router'); +var metrics = require('./Metrics'); var _oldScrollTop = 0; @@ -42,6 +43,9 @@ var ContainerHomeLogs = React.createClass({ }); }, handleClickLogs: function () { + metrics.track('Viewed Logs', { + from: 'preview' + }); this.transitionTo('containerLogs', {name: this.getParams().name}); }, render: function () { diff --git a/src/ContainerHomePreview.react.js b/src/ContainerHomePreview.react.js index 8d48f50cd0..9f0f58fc5c 100644 --- a/src/ContainerHomePreview.react.js +++ b/src/ContainerHomePreview.react.js @@ -5,6 +5,7 @@ var ContainerStore = require('./ContainerStore'); var ContainerUtil = require('./ContainerUtil'); var Router = require('react-router'); var request = require('request'); +var metrics = require('./Metrics'); var ContainerHomePreview = React.createClass({ mixins: [Router.State, Router.Navigation], @@ -57,12 +58,18 @@ var ContainerHomePreview = React.createClass({ }, handleClickPreview: function () { if (this.state.defaultPort) { + metrics.track('Opened In Browser', { + from: 'preview' + }); exec(['open', this.state.ports[this.state.defaultPort].url], function (err) { if (err) { throw err; } }); } }, handleClickNotShowingCorrectly: function () { + metrics.track('Viewed Port Settings', { + from: 'preview' + }); this.transitionTo('containerSettingsPorts', {name: this.getParams().name}); }, render: function () { diff --git a/src/ContainerListItem.react.js b/src/ContainerListItem.react.js index c41e106008..1a80065852 100644 --- a/src/ContainerListItem.react.js +++ b/src/ContainerListItem.react.js @@ -3,6 +3,7 @@ var React = require('react/addons'); var Router = require('react-router'); var remote = require('remote'); var dialog = remote.require('dialog'); +var metrics = require('./Metrics'); var ContainerStore = require('./ContainerStore'); var OverlayTrigger = require('react-bootstrap').OverlayTrigger; var Tooltip = require('react-bootstrap').Tooltip; @@ -22,6 +23,10 @@ var ContainerListItem = React.createClass({ buttons: ['Delete', 'Cancel'] }, function (index) { if (index === 0) { + metrics.track('Deleted Container', { + from: 'list', + type: 'existing' + }); ContainerStore.remove(this.props.container.Name, function (err) { console.error(err); var containers = ContainerStore.sorted(); diff --git a/src/ContainerListNewItem.react.js b/src/ContainerListNewItem.react.js index 9e88d2e514..21c3d53c45 100644 --- a/src/ContainerListNewItem.react.js +++ b/src/ContainerListNewItem.react.js @@ -2,6 +2,7 @@ var $ = require('jquery'); var React = require('react/addons'); var Router = require('react-router'); var ContainerStore = require('./ContainerStore'); +var metrics = require('./Metrics'); var ContainerListNewItem = React.createClass({ mixins: [Router.State, Router.Navigation], @@ -15,6 +16,10 @@ var ContainerListNewItem = React.createClass({ }, handleDelete: function () { var self = this; + metrics.track('Deleted Container', { + from: 'list', + type: 'new' + }); var containers = ContainerStore.sorted(); $(self.getDOMNode()).fadeOut(300, function () { if (containers.length > 0) { diff --git a/src/ContainerSettingsGeneral.react.js b/src/ContainerSettingsGeneral.react.js index b1d54457d3..53a6f78865 100644 --- a/src/ContainerSettingsGeneral.react.js +++ b/src/ContainerSettingsGeneral.react.js @@ -6,6 +6,7 @@ var path = require('path'); var remote = require('remote'); var rimraf = require('rimraf'); var fs = require('fs'); +var metrics = require('./Metrics'); var dialog = remote.require('dialog'); var ContainerStore = require('./ContainerStore'); var ContainerUtil = require('./ContainerUtil'); @@ -91,8 +92,12 @@ var ContainerSettingsGeneral = React.createClass({ } ContainerStore.rename(oldName, newName, err => { if (err) { - console.log(err); + this.setState({ + nameError: err.message + }); + return; } + metrics.track('Changed Container Name'); this.transitionTo('containerSettingsGeneral', {name: newName}); var oldPath = path.join(process.env.HOME, 'Kitematic', oldName); var newPath = path.join(process.env.HOME, 'Kitematic', newName); @@ -127,6 +132,7 @@ var ContainerSettingsGeneral = React.createClass({ envVarList.push(key + '=' + val); }); var self = this; + metrics.track('Saved Environment Variables'); ContainerStore.updateContainer(self.props.container.Name, { Env: envVarList }, function (err) { @@ -151,18 +157,21 @@ var ContainerSettingsGeneral = React.createClass({ }); $('#new-env-key').val(''); $('#new-env-val').val(''); + metrics.track('Added Pending Environment Variable'); }, handleRemoveEnvVar: function (key) { var newEnv = _.omit(this.state.env, key); this.setState({ env: newEnv }); + metrics.track('Removed Environment Variable'); }, handleRemovePendingEnvVar: function (key) { var newEnv = _.omit(this.state.pendingEnv, key); this.setState({ pendingEnv: newEnv }); + metrics.track('Removed Pending Environment Variable'); }, handleDeleteContainer: function () { dialog.showMessageBox({ @@ -176,6 +185,10 @@ var ContainerSettingsGeneral = React.createClass({ }); } if (index === 0) { + metrics.track('Deleted Container', { + from: 'settings', + type: 'existing' + }); ContainerStore.remove(this.props.container.Name, function (err) { console.error(err); }); diff --git a/src/ContainerSettingsPorts.react.js b/src/ContainerSettingsPorts.react.js index b57def6d19..73397b058b 100644 --- a/src/ContainerSettingsPorts.react.js +++ b/src/ContainerSettingsPorts.react.js @@ -4,6 +4,7 @@ var Router = require('react-router'); var exec = require('exec'); var ContainerStore = require('./ContainerStore'); var ContainerUtil = require('./ContainerUtil'); +var metrics = require('./Metrics'); var ContainerSettingsPorts = React.createClass({ mixins: [Router.State, Router.Navigation], @@ -34,6 +35,9 @@ var ContainerSettingsPorts = React.createClass({ }); }, handleViewLink: function (url) { + metrics.track('Opened In Browser', { + from: 'settings' + }); exec(['open', url], function (err) { if (err) { throw err; } }); diff --git a/src/ContainerSettingsVolumes.react.js b/src/ContainerSettingsVolumes.react.js index a17d08cc3d..32e699449e 100644 --- a/src/ContainerSettingsVolumes.react.js +++ b/src/ContainerSettingsVolumes.react.js @@ -4,6 +4,7 @@ var Router = require('react-router'); var remote = require('remote'); var exec = require('exec'); var dialog = remote.require('dialog'); +var metrics = require('./Metrics'); var ContainerStore = require('./ContainerStore'); var ContainerSettingsVolumes = React.createClass({ @@ -16,6 +17,7 @@ var ContainerSettingsVolumes = React.createClass({ } var directory = filenames[0]; if (directory) { + metrics.track('Chose Directory for Volume'); var volumes = _.clone(self.props.container.Volumes); volumes[dockerVol] = directory; var binds = _.pairs(volumes).map(function (pair) { @@ -30,6 +32,9 @@ var ContainerSettingsVolumes = React.createClass({ }); }, handleOpenVolumeClick: function (path) { + metrics.track('Opened Volume Directory', { + from: 'settings' + }); exec(['open', path], function (err) { if (err) { throw err; } }); @@ -43,7 +48,7 @@ var ContainerSettingsVolumes = React.createClass({ if (!val || val.indexOf(process.env.HOME) === -1) { val = ( - No Folder + No Folder Change ); diff --git a/src/ContainerStore.js b/src/ContainerStore.js index 23f1f7b178..215a1ffc82 100644 --- a/src/ContainerStore.js +++ b/src/ContainerStore.js @@ -4,6 +4,7 @@ var async = require('async'); var path = require('path'); var assign = require('object-assign'); var docker = require('./Docker'); +var metrics = require('./Metrics'); var registry = require('./Registry'); var LogStore = require('./LogStore'); @@ -277,6 +278,7 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { _muted[containerName] = true; _progress[containerName] = 0; self._pullImage(repository, tag, function () { + metrics.track('Container Finished Creating'); delete _placeholders[containerName]; localStorage.setItem('store.placeholders', JSON.stringify(_placeholders)); self._createContainer(containerName, {Image: imageName}, function () { @@ -372,7 +374,19 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { }, sorted: function () { return _.values(this.containers()).sort(function (a, b) { - return a.Name.localeCompare(b.Name); + if (a.State.Downloading && !b.State.Downloading) { + return -1; + } else if (!a.State.Downloading && b.State.Downloading) { + return 1; + } else { + if (a.State.Running && !b.State.Running) { + return -1; + } else if (!a.State.Running && b.State.Running) { + return 1; + } else { + return a.Name.localeCompare(b.Name); + } + } }); }, progress: function (name) { diff --git a/src/Containers.react.js b/src/Containers.react.js index 59c657cc47..bbc223980d 100644 --- a/src/Containers.react.js +++ b/src/Containers.react.js @@ -6,6 +6,7 @@ var ContainerList = require('./ContainerList.react'); var Header = require('./Header.react'); var ipc = require('ipc'); var remote = require('remote'); +var metrics = require('./Metrics'); var autoUpdater = remote.require('auto-updater'); var Containers = React.createClass({ @@ -76,9 +77,10 @@ var Containers = React.createClass({ handleNewContainer: function () { $(this.getDOMNode()).find('.new-container-item').parent().fadeIn(); this.transitionTo('new'); + metrics.track('Pressed New Container'); }, handleAutoUpdateClick: function () { - console.log('CLICKED UPDATE'); + metrics.track('Restarted to Update'); ipc.send('command', 'application:quit-install'); }, render: function () { diff --git a/src/ImageCard.react.js b/src/ImageCard.react.js index 94e4dfb41d..2aa36a65d2 100644 --- a/src/ImageCard.react.js +++ b/src/ImageCard.react.js @@ -2,6 +2,7 @@ var $ = require('jquery'); var React = require('react/addons'); var RetinaImage = require('react-retina-image'); var ContainerStore = require('./ContainerStore'); +var metrics = require('./Metrics'); var OverlayTrigger = require('react-bootstrap').OverlayTrigger; var Tooltip = require('react-bootstrap').Tooltip; @@ -18,8 +19,10 @@ var ImageCard = React.createClass({ }); var $tagOverlay = $(this.getDOMNode()).find('.tag-overlay'); $tagOverlay.fadeOut(300); + metrics.track('Selected Image Tag'); }, handleClick: function (name) { + metrics.track('Created Container'); ContainerStore.create(name, this.state.chosenTag, function (err) { if (err) { throw err; @@ -35,7 +38,6 @@ var ImageCard = React.createClass({ tags: result }); }.bind(this)); - }, handleCloseTagOverlay: function () { var $tagOverlay = $(this.getDOMNode()).find('.tag-overlay'); diff --git a/src/Main.js b/src/Main.js index 5cfd4c0e46..dcc3355e7f 100644 --- a/src/Main.js +++ b/src/Main.js @@ -11,6 +11,7 @@ var ContainerStore = require('./ContainerStore'); var SetupStore = require('./SetupStore'); var MenuTemplate = require('./MenuTemplate'); var Menu = remote.require('menu'); +var metrics = require('./Metrics'); var settingsjson; try { @@ -37,6 +38,10 @@ bugsnag.appVersion = app.getVersion(); var menu = Menu.buildFromTemplate(MenuTemplate); Menu.setApplicationMenu(menu); +setInterval(function () { + metrics.track('app heartbeat'); +}, 14400000); + router.run(Handler => React.render(, document.body)); SetupStore.run().then(boot2docker.ip).then(ip => { console.log(ip); diff --git a/src/MenuTemplate.js b/src/MenuTemplate.js index 642a445574..d1bab6c0f8 100644 --- a/src/MenuTemplate.js +++ b/src/MenuTemplate.js @@ -5,6 +5,7 @@ var docker = require('./Docker'); var BrowserWindow = remote.require('browser-window'); var router = require('./Router'); var util = require('./Util'); +var metrics = require('./Metrics'); // main.js var MenuTemplate = [ @@ -71,6 +72,7 @@ var MenuTemplate = [ label: 'Open Docker Terminal', accelerator: 'Command+Shift+T', click: function() { + metrics.track('Opened Docker Terminal'); var terminal = path.join(process.cwd(), 'resources', 'terminal'); var cmd = [terminal, `DOCKER_HOST=${'tcp://' + docker.host + ':2376'} DOCKER_CERT_PATH=${path.join(process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'], '.boot2docker/certs/boot2docker-vm')} DOCKER_TLS_VERIFY=1 $SHELL`]; util.exec(cmd).then(() => {}); diff --git a/src/Metrics.js b/src/Metrics.js new file mode 100644 index 0000000000..3447f0f577 --- /dev/null +++ b/src/Metrics.js @@ -0,0 +1,58 @@ +var assign = require('object-assign'); +var Mixpanel = require('mixpanel'); +var uuid = require('node-uuid'); +var fs = require('fs'); +var path = require('path'); +var util = require('./Util'); +var settings; + +try { + settings = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'settings.json'), 'utf8')); +} catch (err) { + settings = {}; +} + +var token = process.env.NODE_ENV === 'development' ? settings['mixpanel-dev'] : settings.mixpanel; +if (!token) { + token = 'none'; +} + +var mixpanel = Mixpanel.init(token); + +if (localStorage.getItem('metrics.enabled') === null) { + localStorage.setItem('metrics.enabled', true); +} + +var Metrics = { + enabled: function () { + return localStorage.getItem('metrics.enabled') === 'true'; + }, + setEnabled: function (enabled) { + localStorage.setItem('metrics.enabled', !!enabled); + }, + track: function (name, data) { + data = data || {}; + if (!name) { + return; + } + + if (localStorage.getItem('metrics.enabled') !== 'true') { + return; + } + + var id = localStorage.getItem('metrics.id'); + if (!id) { + localStorage.setItem('metrics.id', uuid.v4()); + } + + var os = navigator.userAgent.match(/Mac OS X (\d+_\d+_\d+)/)[1].replace(/_/g, '.'); + mixpanel.track(name, assign({ + distinct_id: id, + version: util.packagejson().version, + 'Operating System Version': os, + beta: !!settings.beta + }, data)); + }, + +}; +module.exports = Metrics; diff --git a/src/NewContainer.react.js b/src/NewContainer.react.js index a6fcf7ce22..bde62dd9fa 100644 --- a/src/NewContainer.react.js +++ b/src/NewContainer.react.js @@ -5,6 +5,7 @@ var RetinaImage = require('react-retina-image'); var Radial = require('./Radial.react'); var ImageCard = require('./ImageCard.react'); var Promise = require('bluebird'); +var metrics = require('./Metrics'); var _recommended = []; var _searchPromise = null; @@ -47,6 +48,7 @@ var NewContainer = React.createClass({ }); _searchPromise = Promise.delay(200).then(() => Promise.resolve($.get('https://registry.hub.docker.com/v1/search?q=' + query))).cancellable().then(data => { + metrics.track('Searched for Images'); this.setState({ results: data.results, query: query, diff --git a/src/Preferences.react.js b/src/Preferences.react.js index f236cddb21..30d1db7f25 100644 --- a/src/Preferences.react.js +++ b/src/Preferences.react.js @@ -1,53 +1,58 @@ var React = require('react/addons'); -var assign = require('object-assign'); var ipc = require('ipc'); +var metrics = require('./Metrics'); var Router = require('react-router'); -// TODO: move this somewhere else -if (localStorage.getItem('options')) { - ipc.send('vm', JSON.parse(localStorage.getItem('options')).save_vm_on_quit); +if (localStorage.getItem('settings.saveVMOnQuit') === 'true') { + ipc.send('vm', true); +} else { + ipc.send('vm', false); } var Preferences = React.createClass({ mixins: [Router.Navigation], getInitialState: function () { - var data = JSON.parse(localStorage.getItem('options')); - return assign({ - save_vm_on_quit: true, - report_analytics: true - }, data || {}); - }, - handleChange: function (key) { - var change = {}; - change[key] = !this.state[key]; - console.log(change); - this.setState(change); - }, - saveState: function () { - ipc.send('vm', this.state.save_vm_on_quit); - localStorage.setItem('options', JSON.stringify(this.state)); - }, - componentDidMount: function () { - this.saveState(); - }, - componentDidUpdate: function () { - this.saveState(); + return { + saveVMOnQuit: localStorage.getItem('settings.saveVMOnQuit') === 'true', + metricsEnabled: metrics.enabled() + }; }, handleGoBackClick: function () { this.goBack(); + metrics.track('Went Back From Preferences'); + }, + handleChangeSaveVMOnQuit: function (e) { + var checked = e.target.checked; + this.setState({ + saveVMOnQuit: checked + }); + ipc.send('vm', checked); + metrics.track('Toggled Save VM On Quit', { + save: checked + }); + }, + handleChangeMetricsEnabled: function (e) { + var checked = e.target.checked; + this.setState({ + metricsEnabled: checked + }); + metrics.setEnabled(checked); + metrics.track('Toggled Metrics', { + enabled: checked + }); }, render: function () { return (
- Go Back + Go Back
VM Settings
Save Linux VM state on closing Kitematic
- +
App Settings
@@ -56,10 +61,9 @@ var Preferences = React.createClass({ Report anonymous usage analytics
- +
- ); diff --git a/src/Setup.react.js b/src/Setup.react.js index 9189d7cd80..c082585886 100644 --- a/src/Setup.react.js +++ b/src/Setup.react.js @@ -5,6 +5,7 @@ var SetupStore = require('./SetupStore'); var RetinaImage = require('react-retina-image'); var Header = require('./Header.react'); var Util = require('./Util'); +var metrics = require('./Metrics'); var Setup = React.createClass({ mixins: [ Router.Navigation ], @@ -28,6 +29,7 @@ var Setup = React.createClass({ SetupStore.removeListener(SetupStore.ERROR_EVENT, this.update); }, handleRetry: function () { + metrics.track('Retried Setup'); SetupStore.retry(); }, handleOpenWebsite: function () { diff --git a/src/SetupStore.js b/src/SetupStore.js index 73923a7312..2f8deee106 100644 --- a/src/SetupStore.js +++ b/src/SetupStore.js @@ -8,6 +8,7 @@ var virtualBox = require('./VirtualBox'); var setupUtil = require('./SetupUtil'); var util = require('./Util'); var assign = require('object-assign'); +var metrics = require('./Metrics'); var _currentStep = null; var _error = null; @@ -167,6 +168,7 @@ var SetupStore = assign(Object.create(EventEmitter.prototype), { return Promise.resolve(); }, run: Promise.coroutine(function* () { + metrics.track('Started Setup'); yield this.updateBinaries(); var steps = yield this.requiredSteps(); for (let step of steps) { @@ -182,9 +184,15 @@ var SetupStore = assign(Object.create(EventEmitter.prototype), { this.emit(this.PROGRESS_EVENT); } }); + metrics.track('Completed Step', { + name: step.name + }); step.percent = 100; break; } catch (err) { + metrics.track('Setup Failed', { + step: step.name + }); console.log('Setup encountered an error.'); console.log(err); if (err) { @@ -198,6 +206,7 @@ var SetupStore = assign(Object.create(EventEmitter.prototype), { } } } + metrics.track('Finished Setup'); _currentStep = null; }) }); diff --git a/testenv.js b/testenv.js new file mode 100644 index 0000000000..55db09ad7b --- /dev/null +++ b/testenv.js @@ -0,0 +1,15 @@ +var mock = (function() { + var store = {}; + return { + getItem: function(key) { + return store[key]; + }, + setItem: function(key, value) { + store[key] = value.toString(); + }, + clear: function() { + store = {}; + } + }; +})(); +Object.defineProperty(window, 'localStorage', { value: mock });