From e7aaeeaf21c31dc50cd292f87c5dac32cbaccdfc Mon Sep 17 00:00:00 2001 From: Jeffrey Morgan Date: Fri, 20 Feb 2015 15:09:25 -0800 Subject: [PATCH 1/3] Adding metrics --- gulpfile.js | 30 ++- package.json | 15 +- src/ContainerDetailsSubheader.react.js | 11 +- src/ContainerHomeFolders.react.js | 7 + src/ContainerHomeLogs.react.js | 4 + src/ContainerHomePreview.react.js | 7 + src/ContainerListItem.react.js | 5 + src/ContainerListNewItem.react.js | 5 + src/ContainerModal.react.js | 248 ------------------------- src/ContainerSettingsGeneral.react.js | 15 +- src/ContainerSettingsPorts.react.js | 4 + src/ContainerSettingsVolumes.react.js | 7 +- src/ContainerStore.js | 16 +- src/Containers.react.js | 4 +- src/ImageCard.react.js | 4 +- src/Main.js | 5 + src/MenuTemplate.js | 2 + src/Metrics.js | 58 ++++++ src/NewContainer.react.js | 2 + src/Preferences.react.js | 62 ++++--- src/Setup.react.js | 2 + src/SetupStore.js | 9 + 22 files changed, 227 insertions(+), 295 deletions(-) delete mode 100644 src/ContainerModal.react.js create mode 100644 src/Metrics.js 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..704e606c54 100644 --- a/package.json +++ b/package.json @@ -41,21 +41,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 +68,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 7cedd398b6..f065833aec 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 ContainerListItem = React.createClass({ @@ -20,6 +21,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/ContainerModal.react.js b/src/ContainerModal.react.js deleted file mode 100644 index a78ef0edd0..0000000000 --- a/src/ContainerModal.react.js +++ /dev/null @@ -1,248 +0,0 @@ -var $ = require('jquery'); -var assign = require('object-assign'); -var React = require('react/addons'); -var Modal = require('react-bootstrap').Modal; -var OverlayTrigger = require('react-bootstrap'); -var Popover = require('react-bootstrap/Popover'); -var RetinaImage = require('react-retina-image'); -var ContainerStore = require('./ContainerStore'); -var OverlayTrigger = require('react-bootstrap/OverlayTrigger'); -var Popover = require('react-bootstrap/Popover'); - -var ContainerModal = React.createClass({ - _searchRequest: null, - getInitialState: function () { - return { - query: '', - results: ContainerStore.recommended(), - loading: false, - tags: {}, - active: null, - }; - }, - componentDidMount: function () { - this.refs.searchInput.getDOMNode().focus(); - ContainerStore.on(ContainerStore.CLIENT_RECOMMENDED_EVENT, this.update); - }, - update: function () { - if (!this.state.query.length) { - this.setState({ - results: ContainerStore.recommended() - }); - } - }, - search: function (query) { - if (this._searchRequest) { - this._searchRequest.abort(); - this._searchRequest = null; - } - - if (!query.length) { - return; - } - - this.setState({ - loading: true - }); - - var self = this; - this._searchRequest = $.get('https://registry.hub.docker.com/v1/search?q=' + query, function (result) { - self.setState({ - query: query, - loading: false - }); - self._searchRequest = null; - if (self.isMounted()) { - self.setState(result); - } - }); - }, - handleChange: function (e) { - var query = e.target.value; - - if (query === this.state.query) { - return; - } - - clearTimeout(this.timeout); - if (!query.length) { - this.setState({ - query: query, - results: ContainerStore.recommended() - }); - } else { - var self = this; - this.timeout = setTimeout(function () { - self.search(query); - }, 200); - } - }, - handleClick: function (name) { - this.props.onRequestHide(); - ContainerStore.create(name, 'latest', function (err) { - if (err) { - throw err; - } - }.bind(this)); - }, - handleTagClick: function (tag, name) { - this.props.onRequestHide(); - ContainerStore.create(name, tag, function () {}); - }, - handleDropdownClick: function (name) { - this.setState({ - active: name - }); - if (this.state.tags[name]) { - return; - } - $.get('https://registry.hub.docker.com/v1/repositories/' + name + '/tags', function (result) { - var res = {}; - res[name] = result; - console.log(assign(this.state.tags, res)); - this.setState({ - tags: assign(this.state.tags, res) - }); - }.bind(this)); - }, - handleModalClick: function (event) { - if (!this.state.active) { - return; - } - if (!$('.popover').is(event.target)) { - this.setState({ - active: null - }); - } - }, - componentDidUpdate: function () { - if (!this.state.active) { - return; - } - var $dropdown = $(this.getDOMNode()).find('[data-name="' + this.state.active + '"]'); - var $popover = $(this.getDOMNode()).find('.popover'); - - $popover.offset({ - top: $dropdown.offset().top + 32, - left: $dropdown.offset().left - $popover.width() / 2 + 11 - }); - }, - render: function () { - var self = this; - var data = this.state.results.slice(0, 7); - - var results; - if (data.length) { - var items = data.map(function (r) { - var name; - if (r.is_official) { - name = {r.name}; - } else { - name = {r.name}; - } - - return ( -
  • -
    -
    - {name} -
    -
    -
    -
    {r.star_count}
    -
    -
    -
    -
    - - -
    -
    -
  • - ); - }); - - results = ( -
    - -
    - ); - } else { - results = ( -
    -

    - No Results -

    -
    - ); - } - - var title = this.state.query ? 'Results' : 'Recommended'; - var loadingClasses = React.addons.classSet({ - hidden: !this.state.loading, - loading: true - }); - var magnifierClasses = React.addons.classSet({ - hidden: this.state.loading, - icon: true, - 'icon-magnifier': true, - 'search-icon': true - }); - - var question = ( -
    - An image is a template for a container.}> - What's an image? - -
    - ); - - var tagData = self.state.tags[this.state.active]; - var tags; - if (tagData) { - var list = tagData.map(function (t) { - return
  • {t.name}
  • ; - }); - tags = ( - - ); - } else { - tags = ; - } - - var popoverClasses = React.addons.classSet({ - popover: true, - hidden: !this.state.active - }); - - return ( - -
    -
    -
    - -
    - -
    - {question} -
    -
    {title}
    - {results} -
    -
    - - {tags} - -
    -
    - ); - } -}); - -module.exports = ContainerModal; 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 d498dc0a4f..5342ffe5e9 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 ImageCard = React.createClass({ getInitialState: function () { @@ -16,8 +17,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; @@ -33,7 +36,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..fd5467a0c4 --- /dev/null +++ b/src/Metrics.js @@ -0,0 +1,58 @@ +var app = require('remote').require('app'); +var assign = require('object-assign'); +var Mixpanel = require('mixpanel'); +var uuid = require('node-uuid'); +var fs = require('fs'); +var path = require('path'); +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: app.getVersion(), + '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; }) }); From abc3f05cb58bd9fae68ee192caf50c8ad1cdc1fd Mon Sep 17 00:00:00 2001 From: Jeffrey Morgan Date: Fri, 20 Feb 2015 15:29:19 -0800 Subject: [PATCH 2/3] Fix tests --- package.json | 3 ++- src/Metrics.js | 4 ++-- testenv.js | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 testenv.js diff --git a/package.json b/package.json index 704e606c54..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", diff --git a/src/Metrics.js b/src/Metrics.js index fd5467a0c4..3447f0f577 100644 --- a/src/Metrics.js +++ b/src/Metrics.js @@ -1,9 +1,9 @@ -var app = require('remote').require('app'); 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 { @@ -48,7 +48,7 @@ var Metrics = { var os = navigator.userAgent.match(/Mac OS X (\d+_\d+_\d+)/)[1].replace(/_/g, '.'); mixpanel.track(name, assign({ distinct_id: id, - version: app.getVersion(), + version: util.packagejson().version, 'Operating System Version': os, beta: !!settings.beta }, data)); 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 }); From 72b8d6cc810e1536589400983ea1cca288dac75f Mon Sep 17 00:00:00 2001 From: Jeffrey Morgan Date: Fri, 20 Feb 2015 15:33:26 -0800 Subject: [PATCH 3/3] Fixing merge --- src/ImageCard.react.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/ImageCard.react.js b/src/ImageCard.react.js index b21be450ef..2aa36a65d2 100644 --- a/src/ImageCard.react.js +++ b/src/ImageCard.react.js @@ -2,12 +2,9 @@ var $ = require('jquery'); var React = require('react/addons'); var RetinaImage = require('react-retina-image'); var ContainerStore = require('./ContainerStore'); -<<<<<<< HEAD var metrics = require('./Metrics'); -======= var OverlayTrigger = require('react-bootstrap').OverlayTrigger; var Tooltip = require('react-bootstrap').Tooltip; ->>>>>>> master var ImageCard = React.createClass({ getInitialState: function () {