fix(GUI): log known user errors when querying S3 for updates (#1655)

We send an HTTP request to S3 to determine the latest available version.
There are various error that can happen that we don't have control over
(like `ETIMEDOUT`).

The current approach is to whitelist certain errors and pretend there is
no update available, however this commit improves that whole situation.

Instead of swallowing these errors, we throw a user error from the
function that determines the latest available version. From the
application code, we check if that function throws a user error, and if
so, instead of showing it to the user, we log a mixpanel event and carry
on.

This change is motivated by the latest reporter error we can't do
anything about: `UNABLE_TO_GET_ISSUER_CERT_LOCALLY`.

Fixes: https://github.com/resin-io/etcher/issues/1525
Change-Type: patch
Changelog-Entry: Fix `UNABLE_TO_GET_ISSUER_CERT_LOCALLY` error at startup when behind certain proxies.
Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
This commit is contained in:
Juan Cruz Viotti 2017-08-03 10:23:34 -04:00 committed by GitHub
parent d8e31665a0
commit 269aafd625
5 changed files with 109 additions and 32 deletions

View File

@ -34,6 +34,7 @@ const messages = require('../shared/messages')
const s3Packages = require('../shared/s3-packages') const s3Packages = require('../shared/s3-packages')
const release = require('../shared/release') const release = require('../shared/release')
const store = require('../shared/store') const store = require('../shared/store')
const errors = require('../shared/errors')
const packageJSON = require('../../package.json') const packageJSON = require('../../package.json')
const flashState = require('../shared/models/flash-state') const flashState = require('../shared/models/flash-state')
const settings = require('./models/settings') const settings = require('./models/settings')
@ -153,6 +154,17 @@ app.run(() => {
return updateNotifier.notify(latestVersion, { return updateNotifier.notify(latestVersion, {
allowSleepUpdateCheck: currentReleaseType === release.RELEASE_TYPE.PRODUCTION allowSleepUpdateCheck: currentReleaseType === release.RELEASE_TYPE.PRODUCTION
}) })
// If the error is an update user error, then we don't want
// to bother users each time they open the app.
// See: https://github.com/resin-io/etcher/issues/1525
}).catch((error) => {
return errors.isUserError(error) && error.code === 'UPDATE_USER_ERROR'
}, (error) => {
analytics.logEvent('Update check user error', {
title: errors.getTitle(error),
description: errors.getDescription(error)
})
}) })
}).catch(exceptionReporter.report) }).catch(exceptionReporter.report)
}) })

View File

@ -257,6 +257,10 @@ exports.createError = (options) => {
error.report = false error.report = false
} }
if (!_.isNil(options.code)) {
error.code = options.code
}
return error return error
} }
@ -288,7 +292,8 @@ exports.createUserError = (options) => {
return exports.createError({ return exports.createError({
title: options.title, title: options.title,
description: options.description, description: options.description,
report: false report: false,
code: options.code
}) })
} }

View File

@ -22,6 +22,7 @@ const Bluebird = require('bluebird')
const request = Bluebird.promisifyAll(require('request')) const request = Bluebird.promisifyAll(require('request'))
const xml = Bluebird.promisifyAll(require('xml2js')) const xml = Bluebird.promisifyAll(require('xml2js'))
const release = require('./release') const release = require('./release')
const errors = require('./errors')
/** /**
* @summary Etcher S3 bucket URLs * @summary Etcher S3 bucket URLs
@ -158,13 +159,22 @@ exports.getRemoteVersions = _.memoize((bucketUrl) => {
code: 'ETIMEDOUT' code: 'ETIMEDOUT'
}, { }, {
code: 'EHOSTDOWN' code: 'EHOSTDOWN'
}, {
// May happen when behind a proxy
code: 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY'
}, { }, {
// May happen when behind a proxy // May happen when behind a proxy
code: 'UNABLE_TO_VERIFY_LEAF_SIGNATURE' code: 'UNABLE_TO_VERIFY_LEAF_SIGNATURE'
}, () => { }, (error) => {
return [] throw errors.createUserError({
title: 'Unable to check for updates',
description: `We got an ${error.code} error in response`,
code: 'UPDATE_USER_ERROR'
})
}) })
}) })

View File

@ -445,6 +445,16 @@ describe('Shared: Errors', function () {
m.chai.expect(errors.getDescription(error)).to.equal('Something happened') m.chai.expect(errors.getDescription(error)).to.equal('Something happened')
}) })
it('should correctly add a code', function () {
const error = errors.createError({
title: 'Foo',
description: 'Something happened',
code: 'HELLO'
})
m.chai.expect(error.code).to.equal('HELLO')
})
it('should correctly add only a title', function () { it('should correctly add only a title', function () {
const error = errors.createError({ const error = errors.createError({
title: 'Foo' title: 'Foo'
@ -541,6 +551,15 @@ describe('Shared: Errors', function () {
m.chai.expect(errors.getDescription(error)).to.equal(error.stack) m.chai.expect(errors.getDescription(error)).to.equal(error.stack)
}) })
it('should correctly add a code', function () {
const error = errors.createUserError({
title: 'Foo',
code: 'HELLO'
})
m.chai.expect(error.code).to.equal('HELLO')
})
it('should ignore an empty description', function () { it('should ignore an empty description', function () {
const error = errors.createUserError({ const error = errors.createUserError({
title: 'Foo', title: 'Foo',

View File

@ -22,6 +22,7 @@ const request = Bluebird.promisifyAll(require('request'))
const nock = require('nock') const nock = require('nock')
const s3Packages = require('../../lib/shared/s3-packages') const s3Packages = require('../../lib/shared/s3-packages')
const release = require('../../lib/shared/release') const release = require('../../lib/shared/release')
const errors = require('../../lib/shared/errors')
describe('Shared: s3Packages', function () { describe('Shared: s3Packages', function () {
describe('.getBucketUrlFromReleaseType()', function () { describe('.getBucketUrlFromReleaseType()', function () {
@ -573,9 +574,10 @@ describe('Shared: s3Packages', function () {
nock.cleanAll() nock.cleanAll()
}) })
it('should be rejected with an error', function (done) { it('should be rejected with a non-user error', function (done) {
s3Packages.getRemoteVersions(s3Packages.BUCKET_URL.PRODUCTION).catch((error) => { s3Packages.getRemoteVersions(s3Packages.BUCKET_URL.PRODUCTION).catch((error) => {
m.chai.expect(error).to.be.an.instanceof(Error) m.chai.expect(error).to.be.an.instanceof(Error)
m.chai.expect(errors.isUserError(error)).to.be.false
done() done()
}) })
}) })
@ -594,11 +596,12 @@ describe('Shared: s3Packages', function () {
this.requestGetAsyncStub.restore() this.requestGetAsyncStub.restore()
}) })
it('should resolve an empty array', function (done) { it('should be rejected with a user error with code UPDATE_USER_ERROR', function (done) {
s3Packages.getRemoteVersions(s3Packages.BUCKET_URL.PRODUCTION).then((versions) => { s3Packages.getRemoteVersions(s3Packages.BUCKET_URL.PRODUCTION).catch((error) => {
m.chai.expect(versions).to.deep.equal([]) m.chai.expect(errors.isUserError(error)).to.be.true
m.chai.expect(error.code).to.equal('UPDATE_USER_ERROR')
done() done()
}).catch(done) })
}) })
}) })
@ -615,11 +618,12 @@ describe('Shared: s3Packages', function () {
this.requestGetAsyncStub.restore() this.requestGetAsyncStub.restore()
}) })
it('should resolve an empty array', function (done) { it('should be rejected with a user error with code UPDATE_USER_ERROR', function (done) {
s3Packages.getRemoteVersions(s3Packages.BUCKET_URL.PRODUCTION).then((versions) => { s3Packages.getRemoteVersions(s3Packages.BUCKET_URL.PRODUCTION).catch((error) => {
m.chai.expect(versions).to.deep.equal([]) m.chai.expect(errors.isUserError(error)).to.be.true
m.chai.expect(error.code).to.equal('UPDATE_USER_ERROR')
done() done()
}).catch(done) })
}) })
}) })
@ -636,11 +640,12 @@ describe('Shared: s3Packages', function () {
this.requestGetAsyncStub.restore() this.requestGetAsyncStub.restore()
}) })
it('should resolve an empty array', function (done) { it('should be rejected with a user error with code UPDATE_USER_ERROR', function (done) {
s3Packages.getRemoteVersions(s3Packages.BUCKET_URL.PRODUCTION).then((versions) => { s3Packages.getRemoteVersions(s3Packages.BUCKET_URL.PRODUCTION).catch((error) => {
m.chai.expect(versions).to.deep.equal([]) m.chai.expect(errors.isUserError(error)).to.be.true
m.chai.expect(error.code).to.equal('UPDATE_USER_ERROR')
done() done()
}).catch(done) })
}) })
}) })
@ -657,11 +662,12 @@ describe('Shared: s3Packages', function () {
this.requestGetAsyncStub.restore() this.requestGetAsyncStub.restore()
}) })
it('should resolve an empty array', function (done) { it('should be rejected with a user error with code UPDATE_USER_ERROR', function (done) {
s3Packages.getRemoteVersions(s3Packages.BUCKET_URL.PRODUCTION).then((versions) => { s3Packages.getRemoteVersions(s3Packages.BUCKET_URL.PRODUCTION).catch((error) => {
m.chai.expect(versions).to.deep.equal([]) m.chai.expect(errors.isUserError(error)).to.be.true
m.chai.expect(error.code).to.equal('UPDATE_USER_ERROR')
done() done()
}).catch(done) })
}) })
}) })
@ -678,11 +684,12 @@ describe('Shared: s3Packages', function () {
this.requestGetAsyncStub.restore() this.requestGetAsyncStub.restore()
}) })
it('should resolve an empty array', function (done) { it('should be rejected with a user error with code UPDATE_USER_ERROR', function (done) {
s3Packages.getRemoteVersions(s3Packages.BUCKET_URL.PRODUCTION).then((versions) => { s3Packages.getRemoteVersions(s3Packages.BUCKET_URL.PRODUCTION).catch((error) => {
m.chai.expect(versions).to.deep.equal([]) m.chai.expect(errors.isUserError(error)).to.be.true
m.chai.expect(error.code).to.equal('UPDATE_USER_ERROR')
done() done()
}).catch(done) })
}) })
}) })
@ -699,11 +706,34 @@ describe('Shared: s3Packages', function () {
this.requestGetAsyncStub.restore() this.requestGetAsyncStub.restore()
}) })
it('should resolve an empty array', function (done) { it('should be rejected with a user error with code UPDATE_USER_ERROR', function (done) {
s3Packages.getRemoteVersions(s3Packages.BUCKET_URL.PRODUCTION).then((versions) => { s3Packages.getRemoteVersions(s3Packages.BUCKET_URL.PRODUCTION).catch((error) => {
m.chai.expect(versions).to.deep.equal([]) m.chai.expect(errors.isUserError(error)).to.be.true
m.chai.expect(error.code).to.equal('UPDATE_USER_ERROR')
done() done()
}).catch(done) })
})
})
describe('given UNABLE_TO_GET_ISSUER_CERT_LOCALLY', function () {
beforeEach(function () {
const error = new Error('UNABLE_TO_GET_ISSUER_CERT_LOCALLY')
error.code = 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY'
this.requestGetAsyncStub = m.sinon.stub(request, 'getAsync')
this.requestGetAsyncStub.returns(Bluebird.reject(error))
})
afterEach(function () {
this.requestGetAsyncStub.restore()
})
it('should be rejected with a user error with code UPDATE_USER_ERROR', function (done) {
s3Packages.getRemoteVersions(s3Packages.BUCKET_URL.PRODUCTION).catch((error) => {
m.chai.expect(errors.isUserError(error)).to.be.true
m.chai.expect(error.code).to.equal('UPDATE_USER_ERROR')
done()
})
}) })
}) })
@ -720,11 +750,12 @@ describe('Shared: s3Packages', function () {
this.requestGetAsyncStub.restore() this.requestGetAsyncStub.restore()
}) })
it('should resolve an empty array', function (done) { it('should be rejected with a user error with code UPDATE_USER_ERROR', function (done) {
s3Packages.getRemoteVersions(s3Packages.BUCKET_URL.PRODUCTION).then((versions) => { s3Packages.getRemoteVersions(s3Packages.BUCKET_URL.PRODUCTION).catch((error) => {
m.chai.expect(versions).to.deep.equal([]) m.chai.expect(errors.isUserError(error)).to.be.true
m.chai.expect(error.code).to.equal('UPDATE_USER_ERROR')
done() done()
}).catch(done) })
}) })
}) })
}) })