diff --git a/lib/gui/app.js b/lib/gui/app.js index e1861c77..ad3b1471 100644 --- a/lib/gui/app.js +++ b/lib/gui/app.js @@ -206,7 +206,9 @@ app.run(($window, AnalyticsService, WarningModalService, ErrorService, OSDialogS description: messages.warning.exitWhileFlashing() }).then((confirmed) => { if (confirmed) { - AnalyticsService.logEvent('Close confirmed while flashing'); + AnalyticsService.logEvent('Close confirmed while flashing', { + uuid: flashState.getFlashUuid() + }); // This circumvents the 'beforeunload' event unlike // electron.remote.app.quit() which does not. diff --git a/lib/gui/models/flash-state.js b/lib/gui/models/flash-state.js index 9ab71414..32b2b1ca 100644 --- a/lib/gui/models/flash-state.js +++ b/lib/gui/models/flash-state.js @@ -220,3 +220,20 @@ exports.getLastFlashSourceChecksum = () => { exports.getLastFlashErrorCode = () => { return exports.getFlashResults().errorCode; }; + +/** + * @summary Get current (or last) flash uuid + * @function + * @public + * + * @description + * This function returns undefined if no flash has been started yet. + * + * @returns {String} the last flash uuid + * + * @example + * const uuid = flashState.getFlashUuid(); + */ +exports.getFlashUuid = () => { + return Store.getState().toJS().flashUuid; +}; diff --git a/lib/gui/models/store.js b/lib/gui/models/store.js index d50ba02f..8c64216f 100644 --- a/lib/gui/models/store.js +++ b/lib/gui/models/store.js @@ -20,6 +20,7 @@ const Immutable = require('immutable'); const _ = require('lodash'); const redux = require('redux'); const persistState = require('redux-localstorage'); +const uuidV4 = require('uuid/v4'); const constraints = require('../../shared/drive-constraints'); const errors = require('../../shared/errors'); @@ -226,13 +227,16 @@ const storeReducer = (state = DEFAULT_STATE, action) => { case ACTIONS.RESET_FLASH_STATE: { return state + .set('isFlashing', false) .set('flashState', DEFAULT_STATE.get('flashState')) - .set('flashResults', DEFAULT_STATE.get('flashResults')); + .set('flashResults', DEFAULT_STATE.get('flashResults')) + .delete('flashUuid'); } case ACTIONS.SET_FLASHING_FLAG: { return state .set('isFlashing', true) + .set('flashUuid', uuidV4()) .set('flashResults', DEFAULT_STATE.get('flashResults')); } diff --git a/lib/gui/modules/image-writer.js b/lib/gui/modules/image-writer.js index ca5093a6..ab23128b 100644 --- a/lib/gui/modules/image-writer.js +++ b/lib/gui/modules/image-writer.js @@ -21,16 +21,18 @@ */ const angular = require('angular'); +const _ = require('lodash'); const childWriter = require('../../child-writer'); const settings = require('../models/settings'); const flashState = require('../models/flash-state'); +const windowProgress = require('../os/window-progress'); const MODULE_NAME = 'Etcher.Modules.ImageWriter'; const imageWriter = angular.module(MODULE_NAME, [ - require('../models/selection-state') + require('./analytics') ]); -imageWriter.service('ImageWriterService', function($q, $rootScope) { +imageWriter.service('ImageWriterService', function($q, $rootScope, AnalyticsService) { /** * @summary Perform write operation @@ -92,6 +94,16 @@ imageWriter.service('ImageWriterService', function($q, $rootScope) { flashState.setFlashingFlag(); + const analyticsData = { + image, + drive, + uuid: flashState.getFlashUuid(), + unmountOnSuccess: settings.get('unmountOnSuccess'), + validateWriteOnSuccess: settings.get('validateWriteOnSuccess') + }; + + AnalyticsService.logEvent('Flash', analyticsData); + return this.performWrite(image, drive, (state) => { // Bring this value to the world of angular. @@ -102,12 +114,34 @@ imageWriter.service('ImageWriterService', function($q, $rootScope) { flashState.setProgressState(state); }); - }).then(flashState.unsetFlashingFlag).catch((error) => { + }).then(flashState.unsetFlashingFlag).then(() => { + if (flashState.wasLastFlashCancelled()) { + AnalyticsService.logEvent('Elevation cancelled', analyticsData); + } else { + AnalyticsService.logEvent('Done', analyticsData); + } + }).catch((error) => { flashState.unsetFlashingFlag({ errorCode: error.code }); + if (error.code === 'EVALIDATION') { + AnalyticsService.logEvent('Validation error', analyticsData); + } else if (error.code === 'EUNPLUGGED') { + AnalyticsService.logEvent('Drive unplugged', analyticsData); + } else if (error.code === 'EIO') { + AnalyticsService.logEvent('Input/output error', analyticsData); + } else if (error.code === 'ENOSPC') { + AnalyticsService.logEvent('Out of space', analyticsData); + } else { + AnalyticsService.logEvent('Flash error', _.merge({ + error + }, analyticsData)); + } + return $q.reject(error); + }).finally(() => { + windowProgress.clear(); }); }; diff --git a/lib/gui/pages/main/controllers/flash.js b/lib/gui/pages/main/controllers/flash.js index 7cc2fc17..8b71bd1a 100644 --- a/lib/gui/pages/main/controllers/flash.js +++ b/lib/gui/pages/main/controllers/flash.js @@ -19,13 +19,11 @@ const messages = require('../../../../shared/messages'); const settings = require('../../../models/settings'); const flashState = require('../../../models/flash-state'); -const windowProgress = require('../../../os/window-progress'); module.exports = function( $state, DriveScannerService, ImageWriterService, - AnalyticsService, FlashErrorModalService, ErrorService, OSNotificationService @@ -60,69 +58,33 @@ module.exports = function( // otherwise Windows throws EPERM DriveScannerService.stop(); - AnalyticsService.logEvent('Flash', { - image, - drive, - unmountOnSuccess: settings.get('unmountOnSuccess'), - validateWriteOnSuccess: settings.get('validateWriteOnSuccess') - }); - ImageWriterService.flash(image.path, drive).then(() => { - if (flashState.wasLastFlashCancelled()) { - AnalyticsService.logEvent('Elevation cancelled', { - image, - drive - }); - return; + if (!flashState.wasLastFlashCancelled()) { + OSNotificationService.send('Success!', messages.info.flashComplete()); + $state.go('success'); } - - OSNotificationService.send('Success!', messages.info.flashComplete()); - AnalyticsService.logEvent('Done', { - image, - drive - }); - $state.go('success'); }) .catch((error) => { OSNotificationService.send('Oops!', messages.error.flashFailure()); + // TODO: All these error codes to messages translations + // should go away if the writer emitted user friendly + // messages on the first place. if (error.code === 'EVALIDATION') { FlashErrorModalService.show(messages.error.validation()); - AnalyticsService.logEvent('Validation error', { - image, - drive - }); } else if (error.code === 'EUNPLUGGED') { FlashErrorModalService.show(messages.error.driveUnplugged()); - AnalyticsService.logEvent('Drive unplugged', { - image, - drive - }); } else if (error.code === 'EIO') { FlashErrorModalService.show(messages.error.inputOutput()); - AnalyticsService.logEvent('Input/output error', { - image, - drive - }); } else if (error.code === 'ENOSPC') { FlashErrorModalService.show(messages.error.notEnoughSpaceInDrive()); - AnalyticsService.logEvent('Out of space', { - image, - drive - }); } else { FlashErrorModalService.show(messages.error.genericFlashError()); ErrorService.reportException(error); - AnalyticsService.logEvent('Flash error', { - error, - image, - drive - }); } }) .finally(() => { - windowProgress.clear(); DriveScannerService.start(); }); }; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index d71a2b7f..8cef5eda 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -6942,6 +6942,11 @@ } } }, + "uuid": { + "version": "3.0.1", + "from": "uuid@latest", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz" + }, "validate-npm-package-license": { "version": "3.0.1", "from": "validate-npm-package-license@>=3.0.1 <4.0.0", diff --git a/package.json b/package.json index 020e212b..96af1bdb 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "trackjs": "^2.1.16", "udif": "^0.9.0", "unbzip2-stream": "^1.0.11", + "uuid": "^3.0.1", "yargs": "^4.6.0", "yauzl": "^2.6.0" }, diff --git a/tests/gui/models/flash-state.spec.js b/tests/gui/models/flash-state.spec.js index 63d2372f..39df0340 100644 --- a/tests/gui/models/flash-state.spec.js +++ b/tests/gui/models/flash-state.spec.js @@ -5,6 +5,10 @@ const flashState = require('../../../lib/gui/models/flash-state'); describe('Browser: flashState', function() { + beforeEach(function() { + flashState.resetState(); + }); + describe('flashState', function() { describe('.resetState()', function() { @@ -36,6 +40,18 @@ describe('Browser: flashState', function() { m.chai.expect(flashState.getFlashResults()).to.deep.equal({}); }); + it('should unset the flashing flag', function() { + flashState.setFlashingFlag(); + flashState.resetState(); + m.chai.expect(flashState.isFlashing()).to.be.false; + }); + + it('should unset the flash uuid', function() { + flashState.setFlashingFlag(); + flashState.resetState(); + m.chai.expect(flashState.getFlashUuid()).to.be.undefined; + }); + }); describe('.isFlashing()', function() { @@ -337,6 +353,19 @@ describe('Browser: flashState', function() { }); }); + it('should not reset the flash uuid', function() { + flashState.setFlashingFlag(); + const uuidBeforeUnset = flashState.getFlashUuid(); + + flashState.unsetFlashingFlag({ + sourceChecksum: '1234', + cancelled: false + }); + + const uuidAfterUnset = flashState.getFlashUuid(); + m.chai.expect(uuidBeforeUnset).to.equal(uuidAfterUnset); + }); + }); describe('.setFlashingFlag()', function() { @@ -441,6 +470,52 @@ describe('Browser: flashState', function() { }); + describe('.getFlashUuid()', function() { + + const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; + + it('should be initially undefined', function() { + m.chai.expect(flashState.getFlashUuid()).to.be.undefined; + }); + + it('should be a valid uuid if the flashing flag is set', function() { + flashState.setFlashingFlag(); + const uuid = flashState.getFlashUuid(); + m.chai.expect(UUID_REGEX.test(uuid)).to.be.true; + }); + + it('should return different uuids every time the flashing flag is set', function() { + flashState.setFlashingFlag(); + const uuid1 = flashState.getFlashUuid(); + flashState.unsetFlashingFlag({ + sourceChecksum: '1234', + cancelled: false + }); + + flashState.setFlashingFlag(); + const uuid2 = flashState.getFlashUuid(); + flashState.unsetFlashingFlag({ + cancelled: true + }); + + flashState.setFlashingFlag(); + const uuid3 = flashState.getFlashUuid(); + flashState.unsetFlashingFlag({ + sourceChecksum: '1234', + cancelled: false + }); + + m.chai.expect(UUID_REGEX.test(uuid1)).to.be.true; + m.chai.expect(UUID_REGEX.test(uuid2)).to.be.true; + m.chai.expect(UUID_REGEX.test(uuid3)).to.be.true; + + m.chai.expect(uuid1).to.not.equal(uuid2); + m.chai.expect(uuid2).to.not.equal(uuid3); + m.chai.expect(uuid3).to.not.equal(uuid1); + }); + + }); + }); });