Adding metrics

This commit is contained in:
Jeffrey Morgan
2015-02-20 15:09:25 -08:00
parent d152005f17
commit e7aaeeaf21
22 changed files with 227 additions and 295 deletions

View File

@@ -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 () {

View File

@@ -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",

View File

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

View File

@@ -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 () {

View File

@@ -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 () {

View File

@@ -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 () {

View File

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

View File

@@ -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) {

View File

@@ -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 = <span><RetinaImage src="official.png"/>{r.name}</span>;
} else {
name = <span>{r.name}</span>;
}
return (
<li key={r.name}>
<div className="info">
<div className="name">
{name}
</div>
<div className="properties">
<div className="icon icon-star-9"></div>
<div className="star-count">{r.star_count}</div>
</div>
</div>
<div className="action">
<div className="btn-group">
<button type="button" className="btn btn-primary" onClick={self.handleClick.bind(self, r.name)}>Create</button>
<button type="button" className="btn btn-primary dropdown-toggle" onClick={self.handleDropdownClick.bind(self, r.name)} data-name={r.name}>
<span className="icon-dropdown icon icon-arrow-37"></span>
</button>
</div>
</div>
</li>
);
});
results = (
<div className="result-list">
<ul>
{items}
</ul>
</div>
);
} else {
results = (
<div className="no-results">
<h3>
No Results
</h3>
</div>
);
}
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 = (
<div className="question">
<OverlayTrigger trigger="hover" placement="bottom" overlay={<Popover>An image is a template for a container.</Popover>}>
<span>What&#39;s an image?</span>
</OverlayTrigger>
</div>
);
var tagData = self.state.tags[this.state.active];
var tags;
if (tagData) {
var list = tagData.map(function (t) {
return <li key={t.name} onClick={self.handleTagClick.bind(self, t.name, self.state.active)}>{t.name}</li>;
});
tags = (
<ul>
{list}
</ul>
);
} else {
tags = <RetinaImage className="tags-loading" src="loading.png"/>;
}
var popoverClasses = React.addons.classSet({
popover: true,
hidden: !this.state.active
});
return (
<Modal {...this.props} animation={false} className="create-modal">
<div className="modal-body" onClick={this.handleModalClick}>
<section className="search">
<div className="search-bar">
<input type="search" ref="searchInput" className="form-control" placeholder="Find an existing image" onChange={this.handleChange}/>
<div className={magnifierClasses}></div>
<RetinaImage className={loadingClasses} src="loading.png"/>
</div>
{question}
<div className="results">
<div className="title">{title}</div>
{results}
</div>
</section>
<Popover placement="bottom" className={popoverClasses}>
{tags}
</Popover>
</div>
</Modal>
);
}
});
module.exports = ContainerModal;

View File

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

View File

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

View File

@@ -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 = (
<span>
<a className="value-right">No Folder</a>
<a className="value-right">No Folder</a>
<a className="btn btn-action small" onClick={self.handleChooseVolumeClick.bind(self, key)}>Change</a>
</span>
);

View File

@@ -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) {

View File

@@ -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 () {

View File

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

View File

@@ -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(<Handler/>, document.body));
SetupStore.run().then(boot2docker.ip).then(ip => {
console.log(ip);

View File

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

58
src/Metrics.js Normal file
View File

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

View File

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

View File

@@ -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 (
<div className="preferences">
<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="option">
<div className="option-name">
Save Linux VM state on closing Kitematic
</div>
<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 className="title">App Settings</div>
@@ -56,10 +61,9 @@ var Preferences = React.createClass({
Report anonymous usage analytics
</div>
<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>
);

View File

@@ -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 () {

View File

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