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 release = require('../shared/release')
const store = require('../shared/store')
const errors = require('../shared/errors')
const packageJSON = require('../../package.json')
const flashState = require('../shared/models/flash-state')
const settings = require('./models/settings')
@ -153,6 +154,17 @@ app.run(() => {
return updateNotifier.notify(latestVersion, {
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)
})

View File

@ -257,6 +257,10 @@ exports.createError = (options) => {
error.report = false
}
if (!_.isNil(options.code)) {
error.code = options.code
}
return error
}
@ -288,7 +292,8 @@ exports.createUserError = (options) => {
return exports.createError({
title: options.title,
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 xml = Bluebird.promisifyAll(require('xml2js'))
const release = require('./release')
const errors = require('./errors')
/**
* @summary Etcher S3 bucket URLs
@ -158,13 +159,22 @@ exports.getRemoteVersions = _.memoize((bucketUrl) => {
code: 'ETIMEDOUT'
}, {
code: 'EHOSTDOWN'
}, {
// May happen when behind a proxy
code: 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY'
}, {
// May happen when behind a proxy
code: 'UNABLE_TO_VERIFY_LEAF_SIGNATURE'
}, () => {
return []
}, (error) => {
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')
})
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 () {
const error = errors.createError({
title: 'Foo'
@ -541,6 +551,15 @@ describe('Shared: Errors', function () {
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 () {
const error = errors.createUserError({
title: 'Foo',

View File

@ -22,6 +22,7 @@ const request = Bluebird.promisifyAll(require('request'))
const nock = require('nock')
const s3Packages = require('../../lib/shared/s3-packages')
const release = require('../../lib/shared/release')
const errors = require('../../lib/shared/errors')
describe('Shared: s3Packages', function () {
describe('.getBucketUrlFromReleaseType()', function () {
@ -573,9 +574,10 @@ describe('Shared: s3Packages', function () {
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) => {
m.chai.expect(error).to.be.an.instanceof(Error)
m.chai.expect(errors.isUserError(error)).to.be.false
done()
})
})
@ -594,11 +596,12 @@ describe('Shared: s3Packages', function () {
this.requestGetAsyncStub.restore()
})
it('should resolve an empty array', function (done) {
s3Packages.getRemoteVersions(s3Packages.BUCKET_URL.PRODUCTION).then((versions) => {
m.chai.expect(versions).to.deep.equal([])
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()
}).catch(done)
})
})
})
@ -615,11 +618,12 @@ describe('Shared: s3Packages', function () {
this.requestGetAsyncStub.restore()
})
it('should resolve an empty array', function (done) {
s3Packages.getRemoteVersions(s3Packages.BUCKET_URL.PRODUCTION).then((versions) => {
m.chai.expect(versions).to.deep.equal([])
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()
}).catch(done)
})
})
})
@ -636,11 +640,12 @@ describe('Shared: s3Packages', function () {
this.requestGetAsyncStub.restore()
})
it('should resolve an empty array', function (done) {
s3Packages.getRemoteVersions(s3Packages.BUCKET_URL.PRODUCTION).then((versions) => {
m.chai.expect(versions).to.deep.equal([])
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()
}).catch(done)
})
})
})
@ -657,11 +662,12 @@ describe('Shared: s3Packages', function () {
this.requestGetAsyncStub.restore()
})
it('should resolve an empty array', function (done) {
s3Packages.getRemoteVersions(s3Packages.BUCKET_URL.PRODUCTION).then((versions) => {
m.chai.expect(versions).to.deep.equal([])
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()
}).catch(done)
})
})
})
@ -678,11 +684,12 @@ describe('Shared: s3Packages', function () {
this.requestGetAsyncStub.restore()
})
it('should resolve an empty array', function (done) {
s3Packages.getRemoteVersions(s3Packages.BUCKET_URL.PRODUCTION).then((versions) => {
m.chai.expect(versions).to.deep.equal([])
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()
}).catch(done)
})
})
})
@ -699,11 +706,34 @@ describe('Shared: s3Packages', function () {
this.requestGetAsyncStub.restore()
})
it('should resolve an empty array', function (done) {
s3Packages.getRemoteVersions(s3Packages.BUCKET_URL.PRODUCTION).then((versions) => {
m.chai.expect(versions).to.deep.equal([])
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()
}).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()
})
it('should resolve an empty array', function (done) {
s3Packages.getRemoteVersions(s3Packages.BUCKET_URL.PRODUCTION).then((versions) => {
m.chai.expect(versions).to.deep.equal([])
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()
}).catch(done)
})
})
})
})