Skip to content

Commit b603c07

Browse files
authored
fix(settings): improve error messages and robustness (#2700)
* Clean up logging/errors for `settings.ts` * Improve `onDidChange` fidelity for C9
1 parent ccb7bf8 commit b603c07

File tree

6 files changed

+71
-64
lines changed

6 files changed

+71
-64
lines changed

src/extension.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,8 @@ export async function activate(context: vscode.ExtensionContext) {
113113
const settings = Settings.instance
114114

115115
await initializeCredentials(context, awsContext, settings)
116-
117116
await activateTelemetry(context, awsContext, settings)
118-
await globals.telemetry.start()
117+
119118
await globals.schemaService.start()
120119
awsFiletypes.activate()
121120

src/shared/settings.ts

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import { getLogger } from './logger'
99
import { cast, FromDescriptor, TypeConstructor, TypeDescriptor } from './utilities/typeConstructors'
1010
import { ClassToInterfaceType, keys } from './utilities/tsUtils'
1111
import { toRecord } from './utilities/collectionUtils'
12-
import { isAutomation, isNameMangled } from './vscode/env'
12+
import { isNameMangled } from './vscode/env'
1313
import { once } from './utilities/functionUtils'
14+
import { UnknownError } from './toolkitError'
1415

1516
type Workspace = Pick<typeof vscode.workspace, 'getConfiguration' | 'onDidChangeConfiguration'>
1617

@@ -181,11 +182,7 @@ function createSettingsClass<T extends TypeDescriptor>(section: string, descript
181182
protected readonly log = makeLogger(Object.getPrototypeOf(this)?.constructor?.name, 'debug')
182183
protected readonly logErr = makeLogger(Object.getPrototypeOf(this)?.constructor?.name, 'error')
183184

184-
public constructor(
185-
private readonly settings: ClassToInterfaceType<Settings> = Settings.instance,
186-
/** Throw an exception if the user config is invalid or the caller type doesn't match the stored type. */
187-
private readonly throwInvalid: boolean = isAutomation()
188-
) {}
185+
public constructor(private readonly settings: ClassToInterfaceType<Settings> = Settings.instance) {}
189186

190187
public get onDidChange() {
191188
return this.getChangedEmitter().event
@@ -195,12 +192,14 @@ function createSettingsClass<T extends TypeDescriptor>(section: string, descript
195192
try {
196193
return this.getOrThrow(key, defaultValue)
197194
} catch (e) {
198-
if (this.throwInvalid || defaultValue === undefined) {
199-
throw new Error(`Failed to read key "${key}": ${e}`)
195+
const message = UnknownError.cast(e)
196+
if (arguments.length === 1) {
197+
throw new Error(`Failed to read key "${section}.${key}": ${message}`)
200198
}
201-
this.logErr(`using default for key "${key}", read failed: ${(e as Error).message ?? '?'}`)
202199

203-
return defaultValue
200+
this.logErr(`using default for key "${key}", read failed: ${message}`)
201+
202+
return defaultValue as Inner[K]
204203
}
205204
}
206205

@@ -252,21 +251,26 @@ function createSettingsClass<T extends TypeDescriptor>(section: string, descript
252251
// - the "key" is `bar`
253252
//
254253
// So if `aws.foo.bar` changed, this would fire with data `{ key: 'bar' }`
254+
//
255+
// Note that `undefined` is not a valid JSON value. So using it as a default
256+
// value is a valid way to express that the key exists but no (valid) value is set.
257+
255258
const props = keys(descriptor)
256-
const store = toRecord(props, p => this.get(p))
259+
const store = toRecord(props, p => this.get(p, undefined))
257260
const emitter = new vscode.EventEmitter<{ readonly key: keyof T }>()
258261
const listener = this.settings.onDidChangeSection(section, event => {
259-
const isDifferent = (p: keyof T & string) => event.affectsConfiguration(p) || store[p] !== this.get(p)
262+
const isDifferent = (p: keyof T & string) => {
263+
const isDifferentLazy = () => {
264+
const previous = store[p]
265+
return previous !== (store[p] = this.get(p, undefined))
266+
}
267+
268+
return event.affectsConfiguration(p) || isDifferentLazy()
269+
}
260270

261271
for (const key of props.filter(isDifferent)) {
262272
this.log(`key "${key}" changed`)
263-
try {
264-
store[key] = this.get(key)
265-
emitter.fire({ key })
266-
} catch {
267-
// getChangedEmitter() can't provide a default value so
268-
// we must silence errors here. Logging is done by this.get().
269-
}
273+
emitter.fire({ key })
270274
}
271275
})
272276

src/shared/telemetry/activation.ts

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { getLogger } from '../logger'
1515
import { getComputeRegion, getIdeProperties, isCloud9 } from '../extensionUtilities'
1616
import { openSettings, Settings } from '../settings'
1717
import { TelemetryConfig } from './util'
18+
import { isAutomation, isReleaseVersion } from '../vscode/env'
19+
import { UnknownError } from '../toolkitError'
1820

1921
export const noticeResponseViewSettings = localize('AWS.telemetry.notificationViewSettings', 'Settings')
2022
export const noticeResponseOk = localize('AWS.telemetry.notificationOk', 'OK')
@@ -32,28 +34,35 @@ const CURRENT_TELEMETRY_NOTICE_VERSION = 2
3234
* Sets up the Metrics system and initializes globals.telemetry
3335
*/
3436
export async function activate(extensionContext: vscode.ExtensionContext, awsContext: AwsContext, settings: Settings) {
37+
const config = new TelemetryConfig(settings)
3538
globals.telemetry = new DefaultTelemetryService(extensionContext, awsContext, getComputeRegion())
3639

37-
const config = new TelemetryConfig(
38-
settings,
39-
// Disable `throwInvalid` because:
40+
try {
41+
globals.telemetry.telemetryEnabled = config.isEnabled()
42+
43+
extensionContext.subscriptions.push(
44+
config.onDidChange(event => {
45+
if (event.key === 'telemetry') {
46+
globals.telemetry.telemetryEnabled = config.isEnabled()
47+
}
48+
})
49+
)
50+
51+
// Prompt user about telemetry if they haven't been
52+
if (!isCloud9() && !hasUserSeenTelemetryNotice(extensionContext)) {
53+
showTelemetryNotice(extensionContext)
54+
}
55+
56+
await globals.telemetry.start()
57+
} catch (e) {
58+
// Only throw in a production build because:
4059
// 1. Telemetry must never prevent normal Toolkit operation.
41-
// 2. For tests, bad data is intentional, so these errors add unwanted noise in the test logs.
42-
false
43-
)
44-
globals.telemetry.telemetryEnabled = config.isEnabled()
45-
46-
extensionContext.subscriptions.push(
47-
config.onDidChange(event => {
48-
if (event.key === 'telemetry') {
49-
globals.telemetry.telemetryEnabled = config.isEnabled()
50-
}
51-
})
52-
)
60+
// 2. We want to know if something is not working ASAP during development.
61+
if (isAutomation() || !isReleaseVersion()) {
62+
throw e
63+
}
5364

54-
// Prompt user about telemetry if they haven't been
55-
if (!isCloud9() && !hasUserSeenTelemetryNotice(extensionContext)) {
56-
showTelemetryNotice(extensionContext)
65+
getLogger().error(`telemetry: failed to activate: ${UnknownError.cast(e).message}`)
5766
}
5867
}
5968

src/shared/telemetry/util.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@ import { fromExtensionManifest } from '../settings'
99
import { shared } from '../utilities/functionUtils'
1010
import { isAutomation } from '../vscode/env'
1111
import { v4 as uuidv4 } from 'uuid'
12+
import { addTypeName } from '../utilities/typeConstructors'
1213

1314
const LEGACY_SETTINGS_TELEMETRY_VALUE_DISABLE = 'Disable'
1415
const LEGACY_SETTINGS_TELEMETRY_VALUE_ENABLE = 'Enable'
1516

16-
export class TelemetryConfig extends fromExtensionManifest('aws', { telemetry: convertLegacy }) {
17+
const TelemetryFlag = addTypeName('boolean', convertLegacy)
18+
19+
export class TelemetryConfig extends fromExtensionManifest('aws', { telemetry: TelemetryFlag }) {
1720
public isEnabled(): boolean {
1821
return this.get('telemetry', true)
1922
}

src/test/shared/settingsConfiguration.test.ts

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ import * as vscode from 'vscode'
88
import { DevSettings, Experiments, fromExtensionManifest, PromptSettings, Settings } from '../../shared/settings'
99
import { TestSettings } from '../utilities/testSettingsConfiguration'
1010
import { ClassToInterfaceType } from '../../shared/utilities/tsUtils'
11+
import { Optional } from '../../shared/utilities/typeConstructors'
1112

1213
const SETTINGS_TARGET = vscode.ConfigurationTarget.Workspace
1314

1415
describe('Settings', function () {
1516
// These tests use an actual extension setting, because vscode.WorkspaceConfiguration fails when
1617
// you attempt to update one that isn't defined in package.json. We will restore the setting value
1718
// at the end of the tests.
18-
const SETTING_KEY = 'aws.telemetry'
19+
const SETTING_KEY = 'aws.samcli.lambdaTimeout'
1920

2021
let sut: Settings
2122

@@ -67,11 +68,10 @@ describe('Settings', function () {
6768
//
6869
// Setting exists but has wrong type:
6970
//
70-
await settings.update(SETTING_KEY, true, SETTINGS_TARGET)
71+
await settings.update(SETTING_KEY, 123, SETTINGS_TARGET)
7172
assert.throws(() => sut.get(SETTING_KEY, String))
72-
assert.throws(() => sut.get(SETTING_KEY, Number))
7373
assert.throws(() => sut.get(SETTING_KEY, Object))
74-
assert.throws(() => sut.get(SETTING_KEY, Number))
74+
assert.throws(() => sut.get(SETTING_KEY, Boolean))
7575
})
7676
})
7777

@@ -140,6 +140,11 @@ describe('Settings', function () {
140140
assert.strictEqual(instance.get('profile', 'bar'), 'bar')
141141
})
142142

143+
it('can use `undefined` as a default value', function () {
144+
const OptionalProfile = fromExtensionManifest('aws', { profile: Optional(String) })
145+
assert.strictEqual(new OptionalProfile(settings).get('profile', undefined), undefined)
146+
})
147+
143148
it('can use a saved setting', async function () {
144149
await settings.update('aws.profile', 'foo')
145150
assert.strictEqual(instance.get('profile'), 'foo')
@@ -150,6 +155,11 @@ describe('Settings', function () {
150155
assert.strictEqual(instance.get('profile', 'bar'), 'foo')
151156
})
152157

158+
it('uses the default value if the setting is invalid', async function () {
159+
await settings.update('aws.profile', true)
160+
assert.strictEqual(instance.get('profile', 'foo'), 'foo')
161+
})
162+
153163
it('throws when the types do not match', async function () {
154164
assert.throws(() => instance.get('profile'))
155165

@@ -200,17 +210,6 @@ describe('DevSetting', function () {
200210
assert.strictEqual(sut.get(TEST_SETTING, false), true)
201211
assert.deepStrictEqual(await state, { [TEST_SETTING]: true })
202212
})
203-
204-
it('bubbles up errors when in automation', async function () {
205-
await settings.update(`aws.dev.${TEST_SETTING}`, 'junk')
206-
assert.throws(() => sut.get(TEST_SETTING, false))
207-
})
208-
209-
it('only throws in automation', async function () {
210-
const noAutomation = new DevSettings(settings, false)
211-
await settings.update(`aws.dev.${TEST_SETTING}`, 'junk')
212-
assert.strictEqual(noAutomation.get(TEST_SETTING, true), true)
213-
})
214213
})
215214

216215
describe('PromptSetting', function () {

src/test/shared/telemetry/util.test.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,12 @@ import { FakeMemento } from '../../fakeExtensionContext'
1111

1212
describe('TelemetryConfig', function () {
1313
const SETTING_KEY = 'aws.telemetry'
14-
15-
const target = ConfigurationTarget.Workspace
16-
const settings = new Settings(target)
14+
const settings = new Settings(ConfigurationTarget.Workspace)
1715

1816
let sut: TelemetryConfig
1917

2018
beforeEach(function () {
21-
sut = new TelemetryConfig(
22-
settings,
23-
// Disable `throwInvalid`. These tests intentionally try invalid
24-
// data, so the errors are unwanted noise in the test logs.
25-
false
26-
)
19+
sut = new TelemetryConfig(settings)
2720
})
2821

2922
afterEach(async function () {

0 commit comments

Comments
 (0)