Merge pull request #194 from kitematic/metrics

Adding metrics
This commit is contained in:
Jeffrey Morgan 2015-02-20 15:36:35 -08:00
commit d82560c313
22 changed files with 244 additions and 48 deletions

View File

@ -17,10 +17,18 @@ var packagejson = require('./package.json');
var dependencies = Object.keys(packagejson.dependencies); var dependencies = Object.keys(packagejson.dependencies);
var isBeta = process.argv.indexOf('--beta') !== -1; 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 = { var options = {
dev: process.argv.indexOf('release') === -1 && process.argv.indexOf('test') === -1, dev: process.argv.indexOf('release') === -1 && process.argv.indexOf('test') === -1,
test: process.argv.indexOf('test') !== -1, test: process.argv.indexOf('test') !== -1,
integration: process.argv.indexOf('--integration') !== -1,
beta: isBeta, beta: isBeta,
filename: isBeta ? 'Kitematic (Beta).app' : 'Kitematic.app', filename: isBeta ? 'Kitematic (Beta).app' : 'Kitematic.app',
name: isBeta ? 'Kitematic (Beta)' : 'Kitematic', name: isBeta ? 'Kitematic (Beta)' : 'Kitematic',
@ -29,6 +37,11 @@ var options = {
gulp.task('js', function () { gulp.task('js', function () {
return gulp.src('src/**/*.js') 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(gulpif(options.dev || options.test, sourcemaps.init()))
.pipe(react()) .pipe(react())
.pipe(babel({blacklist: ['regenerator']})) .pipe(babel({blacklist: ['regenerator']}))
@ -86,7 +99,6 @@ gulp.task('dist', function () {
'mkdir -p ./dist/osx/<%= filename %>/Contents/Resources/app/node_modules', 'mkdir -p ./dist/osx/<%= filename %>/Contents/Resources/app/node_modules',
'cp -R browser dist/osx/<%= filename %>/Contents/Resources/app', 'cp -R browser dist/osx/<%= filename %>/Contents/Resources/app',
'cp package.json 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', 'mkdir -p dist/osx/<%= filename %>/Contents/Resources/app/resources',
'cp -v resources/* 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', '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 () { 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 () { gulp.task('default', ['download', 'copy', 'js', 'images', 'styles'], function () {

View File

@ -12,7 +12,7 @@
"bugs": "https://github.com/kitematic/kitematic/issues", "bugs": "https://github.com/kitematic/kitematic/issues",
"scripts": { "scripts": {
"start": "gulp", "start": "gulp",
"test": "jest", "test": "NODE_PATH=./cache/Atom.app/Contents/Resources/atom/renderer/api/lib:$NODE_PATH jest",
"release": "gulp release", "release": "gulp release",
"release:beta": "gulp release --beta", "release:beta": "gulp release --beta",
"preinstall": "./deps", "preinstall": "./deps",
@ -26,6 +26,7 @@
], ],
"jest": { "jest": {
"scriptPreprocessor": "<rootDir>/preprocessor.js", "scriptPreprocessor": "<rootDir>/preprocessor.js",
"setupEnvScriptFile": "<rootDir>/testenv.js",
"unmockedModulePathPatterns": [ "unmockedModulePathPatterns": [
"tty", "tty",
"net", "net",
@ -41,21 +42,20 @@
"docker-version": "1.5.0", "docker-version": "1.5.0",
"boot2docker-version": "1.5.0", "boot2docker-version": "1.5.0",
"atom-shell-version": "0.21.1", "atom-shell-version": "0.21.1",
"virtualbox-version": "4.3.20", "virtualbox-version": "4.3.22",
"virtualbox-filename": "VirtualBox-4.3.20.pkg", "virtualbox-filename": "VirtualBox-4.3.22.pkg",
"virtualbox-checksum": "89edac4cc7298c8a04fd4bb646ff2197e7673137c6566c7757f0e9cd6265d0c5", "virtualbox-checksum": "4a7dff25bdeef0d112e16ac11bee6d52e856d36bb412aa75576036ba560082eb",
"virtualbox-required-version": "4.3.18", "virtualbox-required-version": "4.3.12",
"dependencies": { "dependencies": {
"ansi-to-html": "0.2.0", "ansi-to-html": "0.2.0",
"async": "^0.9.0", "async": "^0.9.0",
"babel": "^4.0.1",
"bluebird": "^2.9.6", "bluebird": "^2.9.6",
"bugsnag-js": "^2.4.7", "bugsnag-js": "^2.4.7",
"dockerode": "^2.0.7", "dockerode": "^2.0.7",
"download": "^4.0.0",
"exec": "0.1.2", "exec": "0.1.2",
"jquery": "^2.1.3", "jquery": "^2.1.3",
"minimist": "^1.1.0", "minimist": "^1.1.0",
"mixpanel": "0.0.20",
"node-uuid": "^1.4.2", "node-uuid": "^1.4.2",
"object-assign": "^2.0.0", "object-assign": "^2.0.0",
"react": "^0.12.2", "react": "^0.12.2",
@ -69,13 +69,13 @@
"underscore": "^1.7.0" "underscore": "^1.7.0"
}, },
"devDependencies": { "devDependencies": {
"babel": "^4.0.1",
"browserify": "^6.2.0", "browserify": "^6.2.0",
"ecstatic": "^0.5.8", "ecstatic": "^0.5.8",
"glob": "^4.0.6", "glob": "^4.0.6",
"gulp": "^3.8.10", "gulp": "^3.8.10",
"gulp-babel": "^3.0.0",
"gulp-atom": "0.0.5",
"gulp-babel": "^4.0.0", "gulp-babel": "^4.0.0",
"gulp-atom": "0.0.5",
"gulp-concat": "^2.3.4", "gulp-concat": "^2.3.4",
"gulp-cssmin": "^0.1.6", "gulp-cssmin": "^0.1.6",
"gulp-download-atom-shell": "0.0.4", "gulp-download-atom-shell": "0.0.4",

View File

@ -3,6 +3,7 @@ var $ = require('jquery');
var React = require('react/addons'); var React = require('react/addons');
var exec = require('exec'); var exec = require('exec');
var path = require('path'); var path = require('path');
var metrics = require('./Metrics');
var ContainerStore = require('./ContainerStore'); var ContainerStore = require('./ContainerStore');
var ContainerUtil = require('./ContainerUtil'); var ContainerUtil = require('./ContainerUtil');
var boot2docker = require('./Boot2Docker'); var boot2docker = require('./Boot2Docker');
@ -65,21 +66,27 @@ var ContainerDetailsSubheader = React.createClass({
}, },
showHome: function () { showHome: function () {
if (!this.disableTab()) { if (!this.disableTab()) {
metrics.track('Viewed Home');
this.transitionTo('containerHome', {name: this.getParams().name}); this.transitionTo('containerHome', {name: this.getParams().name});
} }
}, },
showLogs: function () { showLogs: function () {
if (!this.disableTab()) { if (!this.disableTab()) {
metrics.track('Viewed Logs');
this.transitionTo('containerLogs', {name: this.getParams().name}); this.transitionTo('containerLogs', {name: this.getParams().name});
} }
}, },
showSettings: function () { showSettings: function () {
if (!this.disableTab()) { if (!this.disableTab()) {
metrics.track('Viewed Settings');
this.transitionTo('containerSettings', {name: this.getParams().name}); this.transitionTo('containerSettings', {name: this.getParams().name});
} }
}, },
handleRun: function () { handleRun: function () {
if (this.state.defaultPort && !this.disableRun()) { if (this.state.defaultPort && !this.disableRun()) {
metrics.track('Opened In Browser', {
from: 'header'
});
exec(['open', this.state.ports[this.state.defaultPort].url], function (err) { exec(['open', this.state.ports[this.state.defaultPort].url], function (err) {
if (err) { throw err; } if (err) { throw err; }
}); });
@ -87,6 +94,7 @@ var ContainerDetailsSubheader = React.createClass({
}, },
handleRestart: function () { handleRestart: function () {
if (!this.disableRestart()) { if (!this.disableRestart()) {
metrics.track('Restarted Container');
ContainerStore.restart(this.props.container.Name, function (err) { ContainerStore.restart(this.props.container.Name, function (err) {
console.log(err); console.log(err);
}); });
@ -94,12 +102,11 @@ var ContainerDetailsSubheader = React.createClass({
}, },
handleTerminal: function () { handleTerminal: function () {
if (!this.disableTerminal()) { if (!this.disableTerminal()) {
metrics.track('Terminaled Into Container');
var container = this.props.container; var container = this.props.container;
var terminal = path.join(process.cwd(), 'resources', 'terminal'); 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']; 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) { exec(cmd, function (stderr, stdout, code) {
console.log(stderr);
console.log(stdout);
if (code) { if (code) {
console.log(stderr); console.log(stderr);
} }

View File

@ -3,16 +3,23 @@ var React = require('react/addons');
var RetinaImage = require('react-retina-image'); var RetinaImage = require('react-retina-image');
var path = require('path'); var path = require('path');
var exec = require('exec'); var exec = require('exec');
var metrics = require('./Metrics');
var Router = require('react-router'); var Router = require('react-router');
var ContainerHomeFolder = React.createClass({ var ContainerHomeFolder = React.createClass({
mixins: [Router.State, Router.Navigation], mixins: [Router.State, Router.Navigation],
handleClickFolder: function (path) { handleClickFolder: function (path) {
metrics.track('Opened Volume Directory', {
from: 'home'
});
exec(['open', path], function (err) { exec(['open', path], function (err) {
if (err) { throw err; } if (err) { throw err; }
}); });
}, },
handleClickChangeFolders: function () { handleClickChangeFolders: function () {
metrics.track('Viewed Volume Settings', {
from: 'preview'
});
this.transitionTo('containerSettingsVolumes', {name: this.getParams().name}); this.transitionTo('containerSettingsVolumes', {name: this.getParams().name});
}, },
render: function () { render: function () {

View File

@ -2,6 +2,7 @@ var $ = require('jquery');
var React = require('react/addons'); var React = require('react/addons');
var LogStore = require('./LogStore'); var LogStore = require('./LogStore');
var Router = require('react-router'); var Router = require('react-router');
var metrics = require('./Metrics');
var _oldScrollTop = 0; var _oldScrollTop = 0;
@ -42,6 +43,9 @@ var ContainerHomeLogs = React.createClass({
}); });
}, },
handleClickLogs: function () { handleClickLogs: function () {
metrics.track('Viewed Logs', {
from: 'preview'
});
this.transitionTo('containerLogs', {name: this.getParams().name}); this.transitionTo('containerLogs', {name: this.getParams().name});
}, },
render: function () { render: function () {

View File

@ -5,6 +5,7 @@ var ContainerStore = require('./ContainerStore');
var ContainerUtil = require('./ContainerUtil'); var ContainerUtil = require('./ContainerUtil');
var Router = require('react-router'); var Router = require('react-router');
var request = require('request'); var request = require('request');
var metrics = require('./Metrics');
var ContainerHomePreview = React.createClass({ var ContainerHomePreview = React.createClass({
mixins: [Router.State, Router.Navigation], mixins: [Router.State, Router.Navigation],
@ -57,12 +58,18 @@ var ContainerHomePreview = React.createClass({
}, },
handleClickPreview: function () { handleClickPreview: function () {
if (this.state.defaultPort) { if (this.state.defaultPort) {
metrics.track('Opened In Browser', {
from: 'preview'
});
exec(['open', this.state.ports[this.state.defaultPort].url], function (err) { exec(['open', this.state.ports[this.state.defaultPort].url], function (err) {
if (err) { throw err; } if (err) { throw err; }
}); });
} }
}, },
handleClickNotShowingCorrectly: function () { handleClickNotShowingCorrectly: function () {
metrics.track('Viewed Port Settings', {
from: 'preview'
});
this.transitionTo('containerSettingsPorts', {name: this.getParams().name}); this.transitionTo('containerSettingsPorts', {name: this.getParams().name});
}, },
render: function () { render: function () {

View File

@ -3,6 +3,7 @@ var React = require('react/addons');
var Router = require('react-router'); var Router = require('react-router');
var remote = require('remote'); var remote = require('remote');
var dialog = remote.require('dialog'); var dialog = remote.require('dialog');
var metrics = require('./Metrics');
var ContainerStore = require('./ContainerStore'); var ContainerStore = require('./ContainerStore');
var OverlayTrigger = require('react-bootstrap').OverlayTrigger; var OverlayTrigger = require('react-bootstrap').OverlayTrigger;
var Tooltip = require('react-bootstrap').Tooltip; var Tooltip = require('react-bootstrap').Tooltip;
@ -22,6 +23,10 @@ var ContainerListItem = React.createClass({
buttons: ['Delete', 'Cancel'] buttons: ['Delete', 'Cancel']
}, function (index) { }, function (index) {
if (index === 0) { if (index === 0) {
metrics.track('Deleted Container', {
from: 'list',
type: 'existing'
});
ContainerStore.remove(this.props.container.Name, function (err) { ContainerStore.remove(this.props.container.Name, function (err) {
console.error(err); console.error(err);
var containers = ContainerStore.sorted(); var containers = ContainerStore.sorted();

View File

@ -2,6 +2,7 @@ var $ = require('jquery');
var React = require('react/addons'); var React = require('react/addons');
var Router = require('react-router'); var Router = require('react-router');
var ContainerStore = require('./ContainerStore'); var ContainerStore = require('./ContainerStore');
var metrics = require('./Metrics');
var ContainerListNewItem = React.createClass({ var ContainerListNewItem = React.createClass({
mixins: [Router.State, Router.Navigation], mixins: [Router.State, Router.Navigation],
@ -15,6 +16,10 @@ var ContainerListNewItem = React.createClass({
}, },
handleDelete: function () { handleDelete: function () {
var self = this; var self = this;
metrics.track('Deleted Container', {
from: 'list',
type: 'new'
});
var containers = ContainerStore.sorted(); var containers = ContainerStore.sorted();
$(self.getDOMNode()).fadeOut(300, function () { $(self.getDOMNode()).fadeOut(300, function () {
if (containers.length > 0) { if (containers.length > 0) {

View File

@ -6,6 +6,7 @@ var path = require('path');
var remote = require('remote'); var remote = require('remote');
var rimraf = require('rimraf'); var rimraf = require('rimraf');
var fs = require('fs'); var fs = require('fs');
var metrics = require('./Metrics');
var dialog = remote.require('dialog'); var dialog = remote.require('dialog');
var ContainerStore = require('./ContainerStore'); var ContainerStore = require('./ContainerStore');
var ContainerUtil = require('./ContainerUtil'); var ContainerUtil = require('./ContainerUtil');
@ -91,8 +92,12 @@ var ContainerSettingsGeneral = React.createClass({
} }
ContainerStore.rename(oldName, newName, err => { ContainerStore.rename(oldName, newName, err => {
if (err) { if (err) {
console.log(err); this.setState({
nameError: err.message
});
return;
} }
metrics.track('Changed Container Name');
this.transitionTo('containerSettingsGeneral', {name: newName}); this.transitionTo('containerSettingsGeneral', {name: newName});
var oldPath = path.join(process.env.HOME, 'Kitematic', oldName); var oldPath = path.join(process.env.HOME, 'Kitematic', oldName);
var newPath = path.join(process.env.HOME, 'Kitematic', newName); var newPath = path.join(process.env.HOME, 'Kitematic', newName);
@ -127,6 +132,7 @@ var ContainerSettingsGeneral = React.createClass({
envVarList.push(key + '=' + val); envVarList.push(key + '=' + val);
}); });
var self = this; var self = this;
metrics.track('Saved Environment Variables');
ContainerStore.updateContainer(self.props.container.Name, { ContainerStore.updateContainer(self.props.container.Name, {
Env: envVarList Env: envVarList
}, function (err) { }, function (err) {
@ -151,18 +157,21 @@ var ContainerSettingsGeneral = React.createClass({
}); });
$('#new-env-key').val(''); $('#new-env-key').val('');
$('#new-env-val').val(''); $('#new-env-val').val('');
metrics.track('Added Pending Environment Variable');
}, },
handleRemoveEnvVar: function (key) { handleRemoveEnvVar: function (key) {
var newEnv = _.omit(this.state.env, key); var newEnv = _.omit(this.state.env, key);
this.setState({ this.setState({
env: newEnv env: newEnv
}); });
metrics.track('Removed Environment Variable');
}, },
handleRemovePendingEnvVar: function (key) { handleRemovePendingEnvVar: function (key) {
var newEnv = _.omit(this.state.pendingEnv, key); var newEnv = _.omit(this.state.pendingEnv, key);
this.setState({ this.setState({
pendingEnv: newEnv pendingEnv: newEnv
}); });
metrics.track('Removed Pending Environment Variable');
}, },
handleDeleteContainer: function () { handleDeleteContainer: function () {
dialog.showMessageBox({ dialog.showMessageBox({
@ -176,6 +185,10 @@ var ContainerSettingsGeneral = React.createClass({
}); });
} }
if (index === 0) { if (index === 0) {
metrics.track('Deleted Container', {
from: 'settings',
type: 'existing'
});
ContainerStore.remove(this.props.container.Name, function (err) { ContainerStore.remove(this.props.container.Name, function (err) {
console.error(err); console.error(err);
}); });

View File

@ -4,6 +4,7 @@ var Router = require('react-router');
var exec = require('exec'); var exec = require('exec');
var ContainerStore = require('./ContainerStore'); var ContainerStore = require('./ContainerStore');
var ContainerUtil = require('./ContainerUtil'); var ContainerUtil = require('./ContainerUtil');
var metrics = require('./Metrics');
var ContainerSettingsPorts = React.createClass({ var ContainerSettingsPorts = React.createClass({
mixins: [Router.State, Router.Navigation], mixins: [Router.State, Router.Navigation],
@ -34,6 +35,9 @@ var ContainerSettingsPorts = React.createClass({
}); });
}, },
handleViewLink: function (url) { handleViewLink: function (url) {
metrics.track('Opened In Browser', {
from: 'settings'
});
exec(['open', url], function (err) { exec(['open', url], function (err) {
if (err) { throw err; } if (err) { throw err; }
}); });

View File

@ -4,6 +4,7 @@ var Router = require('react-router');
var remote = require('remote'); var remote = require('remote');
var exec = require('exec'); var exec = require('exec');
var dialog = remote.require('dialog'); var dialog = remote.require('dialog');
var metrics = require('./Metrics');
var ContainerStore = require('./ContainerStore'); var ContainerStore = require('./ContainerStore');
var ContainerSettingsVolumes = React.createClass({ var ContainerSettingsVolumes = React.createClass({
@ -16,6 +17,7 @@ var ContainerSettingsVolumes = React.createClass({
} }
var directory = filenames[0]; var directory = filenames[0];
if (directory) { if (directory) {
metrics.track('Chose Directory for Volume');
var volumes = _.clone(self.props.container.Volumes); var volumes = _.clone(self.props.container.Volumes);
volumes[dockerVol] = directory; volumes[dockerVol] = directory;
var binds = _.pairs(volumes).map(function (pair) { var binds = _.pairs(volumes).map(function (pair) {
@ -30,6 +32,9 @@ var ContainerSettingsVolumes = React.createClass({
}); });
}, },
handleOpenVolumeClick: function (path) { handleOpenVolumeClick: function (path) {
metrics.track('Opened Volume Directory', {
from: 'settings'
});
exec(['open', path], function (err) { exec(['open', path], function (err) {
if (err) { throw err; } if (err) { throw err; }
}); });

View File

@ -4,6 +4,7 @@ var async = require('async');
var path = require('path'); var path = require('path');
var assign = require('object-assign'); var assign = require('object-assign');
var docker = require('./Docker'); var docker = require('./Docker');
var metrics = require('./Metrics');
var registry = require('./Registry'); var registry = require('./Registry');
var LogStore = require('./LogStore'); var LogStore = require('./LogStore');
@ -277,6 +278,7 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), {
_muted[containerName] = true; _muted[containerName] = true;
_progress[containerName] = 0; _progress[containerName] = 0;
self._pullImage(repository, tag, function () { self._pullImage(repository, tag, function () {
metrics.track('Container Finished Creating');
delete _placeholders[containerName]; delete _placeholders[containerName];
localStorage.setItem('store.placeholders', JSON.stringify(_placeholders)); localStorage.setItem('store.placeholders', JSON.stringify(_placeholders));
self._createContainer(containerName, {Image: imageName}, function () { self._createContainer(containerName, {Image: imageName}, function () {
@ -372,7 +374,19 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), {
}, },
sorted: function () { sorted: function () {
return _.values(this.containers()).sort(function (a, b) { 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) { progress: function (name) {

View File

@ -6,6 +6,7 @@ var ContainerList = require('./ContainerList.react');
var Header = require('./Header.react'); var Header = require('./Header.react');
var ipc = require('ipc'); var ipc = require('ipc');
var remote = require('remote'); var remote = require('remote');
var metrics = require('./Metrics');
var autoUpdater = remote.require('auto-updater'); var autoUpdater = remote.require('auto-updater');
var Containers = React.createClass({ var Containers = React.createClass({
@ -76,9 +77,10 @@ var Containers = React.createClass({
handleNewContainer: function () { handleNewContainer: function () {
$(this.getDOMNode()).find('.new-container-item').parent().fadeIn(); $(this.getDOMNode()).find('.new-container-item').parent().fadeIn();
this.transitionTo('new'); this.transitionTo('new');
metrics.track('Pressed New Container');
}, },
handleAutoUpdateClick: function () { handleAutoUpdateClick: function () {
console.log('CLICKED UPDATE'); metrics.track('Restarted to Update');
ipc.send('command', 'application:quit-install'); ipc.send('command', 'application:quit-install');
}, },
render: function () { render: function () {

View File

@ -2,6 +2,7 @@ var $ = require('jquery');
var React = require('react/addons'); var React = require('react/addons');
var RetinaImage = require('react-retina-image'); var RetinaImage = require('react-retina-image');
var ContainerStore = require('./ContainerStore'); var ContainerStore = require('./ContainerStore');
var metrics = require('./Metrics');
var OverlayTrigger = require('react-bootstrap').OverlayTrigger; var OverlayTrigger = require('react-bootstrap').OverlayTrigger;
var Tooltip = require('react-bootstrap').Tooltip; var Tooltip = require('react-bootstrap').Tooltip;
@ -18,8 +19,10 @@ var ImageCard = React.createClass({
}); });
var $tagOverlay = $(this.getDOMNode()).find('.tag-overlay'); var $tagOverlay = $(this.getDOMNode()).find('.tag-overlay');
$tagOverlay.fadeOut(300); $tagOverlay.fadeOut(300);
metrics.track('Selected Image Tag');
}, },
handleClick: function (name) { handleClick: function (name) {
metrics.track('Created Container');
ContainerStore.create(name, this.state.chosenTag, function (err) { ContainerStore.create(name, this.state.chosenTag, function (err) {
if (err) { if (err) {
throw err; throw err;
@ -35,7 +38,6 @@ var ImageCard = React.createClass({
tags: result tags: result
}); });
}.bind(this)); }.bind(this));
}, },
handleCloseTagOverlay: function () { handleCloseTagOverlay: function () {
var $tagOverlay = $(this.getDOMNode()).find('.tag-overlay'); var $tagOverlay = $(this.getDOMNode()).find('.tag-overlay');

View File

@ -11,6 +11,7 @@ var ContainerStore = require('./ContainerStore');
var SetupStore = require('./SetupStore'); var SetupStore = require('./SetupStore');
var MenuTemplate = require('./MenuTemplate'); var MenuTemplate = require('./MenuTemplate');
var Menu = remote.require('menu'); var Menu = remote.require('menu');
var metrics = require('./Metrics');
var settingsjson; var settingsjson;
try { try {
@ -37,6 +38,10 @@ bugsnag.appVersion = app.getVersion();
var menu = Menu.buildFromTemplate(MenuTemplate); var menu = Menu.buildFromTemplate(MenuTemplate);
Menu.setApplicationMenu(menu); Menu.setApplicationMenu(menu);
setInterval(function () {
metrics.track('app heartbeat');
}, 14400000);
router.run(Handler => React.render(<Handler/>, document.body)); router.run(Handler => React.render(<Handler/>, document.body));
SetupStore.run().then(boot2docker.ip).then(ip => { SetupStore.run().then(boot2docker.ip).then(ip => {
console.log(ip); console.log(ip);

View File

@ -5,6 +5,7 @@ var docker = require('./Docker');
var BrowserWindow = remote.require('browser-window'); var BrowserWindow = remote.require('browser-window');
var router = require('./Router'); var router = require('./Router');
var util = require('./Util'); var util = require('./Util');
var metrics = require('./Metrics');
// main.js // main.js
var MenuTemplate = [ var MenuTemplate = [
@ -71,6 +72,7 @@ var MenuTemplate = [
label: 'Open Docker Terminal', label: 'Open Docker Terminal',
accelerator: 'Command+Shift+T', accelerator: 'Command+Shift+T',
click: function() { click: function() {
metrics.track('Opened Docker Terminal');
var terminal = path.join(process.cwd(), 'resources', '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`]; 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(() => {}); util.exec(cmd).then(() => {});

58
src/Metrics.js Normal file
View File

@ -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;

View File

@ -5,6 +5,7 @@ var RetinaImage = require('react-retina-image');
var Radial = require('./Radial.react'); var Radial = require('./Radial.react');
var ImageCard = require('./ImageCard.react'); var ImageCard = require('./ImageCard.react');
var Promise = require('bluebird'); var Promise = require('bluebird');
var metrics = require('./Metrics');
var _recommended = []; var _recommended = [];
var _searchPromise = null; 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 => { _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({ this.setState({
results: data.results, results: data.results,
query: query, query: query,

View File

@ -1,53 +1,58 @@
var React = require('react/addons'); var React = require('react/addons');
var assign = require('object-assign');
var ipc = require('ipc'); var ipc = require('ipc');
var metrics = require('./Metrics');
var Router = require('react-router'); var Router = require('react-router');
// TODO: move this somewhere else if (localStorage.getItem('settings.saveVMOnQuit') === 'true') {
if (localStorage.getItem('options')) { ipc.send('vm', true);
ipc.send('vm', JSON.parse(localStorage.getItem('options')).save_vm_on_quit); } else {
ipc.send('vm', false);
} }
var Preferences = React.createClass({ var Preferences = React.createClass({
mixins: [Router.Navigation], mixins: [Router.Navigation],
getInitialState: function () { getInitialState: function () {
var data = JSON.parse(localStorage.getItem('options')); return {
return assign({ saveVMOnQuit: localStorage.getItem('settings.saveVMOnQuit') === 'true',
save_vm_on_quit: true, metricsEnabled: metrics.enabled()
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();
}, },
handleGoBackClick: function () { handleGoBackClick: function () {
this.goBack(); 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 () { render: function () {
return ( return (
<div className="preferences"> <div className="preferences">
<div className="preferences-content"> <div className="preferences-content">
<a href="#" onClick={this.handleGoBackClick}>Go Back</a> <a onClick={this.handleGoBackClick}>Go Back</a>
<div className="title">VM Settings</div> <div className="title">VM Settings</div>
<div className="option"> <div className="option">
<div className="option-name"> <div className="option-name">
Save Linux VM state on closing Kitematic Save Linux VM state on closing Kitematic
</div> </div>
<div className="option-value"> <div className="option-value">
<input type="checkbox" checked={this.state.save_vm_on_quit} onChange={this.handleChange.bind(this, 'save_vm_on_quit')}/> <input type="checkbox" checked={this.state.saveVMOnQuit} onChange={this.handleChangeSaveVMOnQuit}/>
</div> </div>
</div> </div>
<div className="title">App Settings</div> <div className="title">App Settings</div>
@ -56,10 +61,9 @@ var Preferences = React.createClass({
Report anonymous usage analytics Report anonymous usage analytics
</div> </div>
<div className="option-value"> <div className="option-value">
<input type="checkbox" checked={this.state.report_analytics} onChange={this.handleChange.bind(this, 'report_analytics')}/> <input type="checkbox" checked={this.state.metricsEnabled} onChange={this.handleChangeMetricsEnabled}/>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
); );

View File

@ -5,6 +5,7 @@ var SetupStore = require('./SetupStore');
var RetinaImage = require('react-retina-image'); var RetinaImage = require('react-retina-image');
var Header = require('./Header.react'); var Header = require('./Header.react');
var Util = require('./Util'); var Util = require('./Util');
var metrics = require('./Metrics');
var Setup = React.createClass({ var Setup = React.createClass({
mixins: [ Router.Navigation ], mixins: [ Router.Navigation ],
@ -28,6 +29,7 @@ var Setup = React.createClass({
SetupStore.removeListener(SetupStore.ERROR_EVENT, this.update); SetupStore.removeListener(SetupStore.ERROR_EVENT, this.update);
}, },
handleRetry: function () { handleRetry: function () {
metrics.track('Retried Setup');
SetupStore.retry(); SetupStore.retry();
}, },
handleOpenWebsite: function () { handleOpenWebsite: function () {

View File

@ -8,6 +8,7 @@ var virtualBox = require('./VirtualBox');
var setupUtil = require('./SetupUtil'); var setupUtil = require('./SetupUtil');
var util = require('./Util'); var util = require('./Util');
var assign = require('object-assign'); var assign = require('object-assign');
var metrics = require('./Metrics');
var _currentStep = null; var _currentStep = null;
var _error = null; var _error = null;
@ -167,6 +168,7 @@ var SetupStore = assign(Object.create(EventEmitter.prototype), {
return Promise.resolve(); return Promise.resolve();
}, },
run: Promise.coroutine(function* () { run: Promise.coroutine(function* () {
metrics.track('Started Setup');
yield this.updateBinaries(); yield this.updateBinaries();
var steps = yield this.requiredSteps(); var steps = yield this.requiredSteps();
for (let step of steps) { for (let step of steps) {
@ -182,9 +184,15 @@ var SetupStore = assign(Object.create(EventEmitter.prototype), {
this.emit(this.PROGRESS_EVENT); this.emit(this.PROGRESS_EVENT);
} }
}); });
metrics.track('Completed Step', {
name: step.name
});
step.percent = 100; step.percent = 100;
break; break;
} catch (err) { } catch (err) {
metrics.track('Setup Failed', {
step: step.name
});
console.log('Setup encountered an error.'); console.log('Setup encountered an error.');
console.log(err); console.log(err);
if (err) { if (err) {
@ -198,6 +206,7 @@ var SetupStore = assign(Object.create(EventEmitter.prototype), {
} }
} }
} }
metrics.track('Finished Setup');
_currentStep = null; _currentStep = null;
}) })
}); });

15
testenv.js Normal file
View File

@ -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 });