Skip to content

Commit d5d026a

Browse files
committed
refactor(telemetry): use globalState abstraction
1 parent 1c778ac commit d5d026a

File tree

8 files changed

+62
-63
lines changed

8 files changed

+62
-63
lines changed

packages/core/src/shared/globalState.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type globalKey =
2727
| stepFunctionsKey
2828
| ToolIdStateKey
2929
| JsonSchemasKey
30+
| 'amazonq.telemetry.migrated'
3031
| 'aws.amazonq.codewhisperer.newCustomizations'
3132
| 'aws.amazonq.hasShownWalkthrough'
3233
| 'aws.amazonq.showTryChatCodeLens'
@@ -61,6 +62,8 @@ type globalKey =
6162
| 'region'
6263
// TODO: implement this via `PromptSettings` instead of globalState.
6364
| 'sam.sync.updateMessage'
65+
| 'telemetryClientId'
66+
| 'telemetryId'
6467

6568
/**
6669
* Extension-local (not visible to other vscode extensions) shared state which persists after IDE
@@ -75,7 +78,7 @@ type globalKey =
7578
* - garbage collection
7679
*/
7780
export class GlobalState implements vscode.Memento {
78-
constructor(private readonly memento: vscode.Memento) {}
81+
constructor(protected readonly memento: vscode.Memento) {}
7982

8083
keys(): readonly string[] {
8184
return this.memento.keys()
@@ -93,9 +96,9 @@ export class GlobalState implements vscode.Memento {
9396
* {@link String}, {@link Boolean}, etc.
9497
* @param defaultVal Value returned if `key` has no value.
9598
*/
96-
getStrict<T>(key: globalKey, type: TypeConstructor<T>, defaulVal?: T) {
99+
getStrict<T>(key: globalKey, type: TypeConstructor<T>, defaultVal?: T) {
97100
try {
98-
const val = this.memento.get<T>(key) ?? defaulVal
101+
const val = this.memento.get<T>(key) ?? defaultVal
99102
return !type || val === undefined ? val : cast(val, type)
100103
} catch (e) {
101104
const msg = `GlobalState: invalid state (or read failed) for key: "${key}"`
@@ -117,27 +120,27 @@ export class GlobalState implements vscode.Memento {
117120
* @param key Key name
118121
* @param defaultVal Value returned if `key` has no value.
119122
*/
120-
get<T>(key: globalKey, defaulVal?: T): T | undefined {
123+
get<T>(key: globalKey, defaultVal?: T): T | undefined {
121124
const skip = (o: any) => o as T // Don't type check.
122-
return this.getStrict(key, skip, defaulVal)
125+
return this.getStrict(key, skip, defaultVal)
123126
}
124127

125128
/**
126-
* Gets the value for `key` if it satisfies the `type` specification, else logs an error and returns `defaulVal`.
129+
* Gets the value for `key` if it satisfies the `type` specification, else logs an error and returns `defaultVal`.
127130
*
128131
* @param key Key name
129132
* @param type Type validator function, or primitive type constructor such as {@link Object},
130133
* {@link String}, {@link Boolean}, etc.
131134
* @param defaultVal Value returned if `key` has no value.
132135
*/
133136
tryGet<T>(key: globalKey, type: TypeConstructor<T>): T | undefined
134-
tryGet<T>(key: globalKey, type: TypeConstructor<T>, defaulVal: T): T
135-
tryGet<T>(key: globalKey, type: TypeConstructor<T>, defaulVal?: T): T | undefined {
137+
tryGet<T>(key: globalKey, type: TypeConstructor<T>, defaultVal: T): T
138+
tryGet<T>(key: globalKey, type: TypeConstructor<T>, defaultVal?: T): T | undefined {
136139
try {
137-
return this.getStrict(key, type, defaulVal)
140+
return this.getStrict(key, type, defaultVal)
138141
} catch (e) {
139142
getLogger().error('%s', (e as Error).message)
140-
return defaulVal
143+
return defaultVal
141144
}
142145
}
143146

packages/core/src/shared/telemetry/activation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export async function activate(
4444
await config.initAmazonQSetting() // TODO: Remove after a few releases.
4545

4646
DefaultTelemetryClient.productName = productName
47-
globals.telemetry = await DefaultTelemetryService.create(extensionContext, awsContext, getComputeRegion())
47+
globals.telemetry = await DefaultTelemetryService.create(awsContext, getComputeRegion())
4848

4949
const isAmazonQExt = isAmazonQ()
5050
try {

packages/core/src/shared/telemetry/telemetryService.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,11 @@ export class DefaultTelemetryService {
4949
* Use {@link create}() to create an instance of this class.
5050
*/
5151
protected constructor(
52-
private readonly context: ExtensionContext,
5352
private readonly awsContext: AwsContext,
5453
private readonly computeRegion?: string,
5554
publisher?: TelemetryPublisher
5655
) {
57-
this.persistFilePath = path.join(context.globalStorageUri.fsPath, 'telemetryCache')
56+
this.persistFilePath = path.join(globals.context.globalStorageUri.fsPath, 'telemetryCache')
5857

5958
this.startTime = new globals.clock.Date()
6059

@@ -70,14 +69,9 @@ export class DefaultTelemetryService {
7069
* This exists since we need to first ensure the global storage
7170
* path exists before creating the instance.
7271
*/
73-
static async create(
74-
context: ExtensionContext,
75-
awsContext: AwsContext,
76-
computeRegion?: string,
77-
publisher?: TelemetryPublisher
78-
) {
79-
await DefaultTelemetryService.ensureGlobalStorageExists(context)
80-
return new DefaultTelemetryService(context, awsContext, computeRegion, publisher)
72+
static async create(awsContext: AwsContext, computeRegion?: string, publisher?: TelemetryPublisher) {
73+
await DefaultTelemetryService.ensureGlobalStorageExists(globals.context)
74+
return new DefaultTelemetryService(awsContext, computeRegion, publisher)
8175
}
8276

8377
public get logger(): TelemetryLogger {
@@ -219,8 +213,9 @@ export class DefaultTelemetryService {
219213
const clientId = getClientId(globals.globalState)
220214
// grab our Cognito identityId
221215
const poolId = DefaultTelemetryClient.config.identityPool
222-
const identityMapJson = this.context.globalState.get<string>(
216+
const identityMapJson = globals.globalState.tryGet(
223217
DefaultTelemetryService.telemetryCognitoIdKey,
218+
String,
224219
'[]'
225220
)
226221
// Maps don't cleanly de/serialize with JSON.parse/stringify so we need to do it ourselves
@@ -234,7 +229,7 @@ export class DefaultTelemetryService {
234229

235230
// save it
236231
identityMap.set(poolId, identityPublisherTuple.cognitoIdentityId)
237-
await this.context.globalState.update(
232+
await globals.globalState.update(
238233
DefaultTelemetryService.telemetryCognitoIdKey,
239234
JSON.stringify(Array.from(identityMap.entries()))
240235
)

packages/core/src/shared/telemetry/util.ts

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import * as vscode from 'vscode'
7-
import { env, Memento, version } from 'vscode'
7+
import { env, version } from 'vscode'
88
import * as os from 'os'
99
import { getLogger } from '../logger'
1010
import { fromExtensionManifest, migrateSetting, Settings } from '../settings'
@@ -26,11 +26,9 @@ const legacySettingsTelemetryValueDisable = 'Disable'
2626
const legacySettingsTelemetryValueEnable = 'Enable'
2727

2828
const TelemetryFlag = addTypeName('boolean', convertLegacy)
29-
const telemetryClientIdGlobalStatekey = 'telemetryClientId'
3029
const telemetryClientIdEnvKey = '__TELEMETRY_CLIENT_ID'
3130

3231
export class TelemetryConfig {
33-
private readonly amazonQSettingMigratedKey = 'amazonq.telemetry.migrated'
3432
private readonly _toolkitConfig
3533
private readonly _amazonQConfig
3634

@@ -60,13 +58,13 @@ export class TelemetryConfig {
6058
}
6159

6260
public async initAmazonQSetting() {
63-
if (!isAmazonQ() || globals.context.globalState.get<boolean>(this.amazonQSettingMigratedKey)) {
61+
if (!isAmazonQ() || globals.globalState.tryGet('amazonq.telemetry.migrated', Boolean, false)) {
6462
return
6563
}
6664
// aws.telemetry isn't deprecated, we are just initializing amazonQ.telemetry with its value.
6765
// This is also why we need to check that we only try this migration once.
6866
await migrateSetting({ key: 'aws.telemetry', type: Boolean }, { key: 'amazonQ.telemetry' })
69-
await globals.context.globalState.update(this.amazonQSettingMigratedKey, true)
67+
await globals.globalState.update('amazonq.telemetry.migrated', true)
7068
}
7169
}
7270

@@ -89,20 +87,23 @@ export const getClientId = memoize(
8987
/**
9088
* @param nonce Dummy parameter to allow tests to defeat memoize().
9189
*/
92-
(globalState: Memento, isTelemetryEnabled = new TelemetryConfig().isEnabled(), isTest?: false, nonce?: string) => {
90+
(
91+
globalState: typeof globals.globalState,
92+
isTelemetryEnabled = new TelemetryConfig().isEnabled(),
93+
isTest?: false,
94+
nonce?: string
95+
) => {
9396
if (isTest ?? isAutomation()) {
9497
return 'ffffffff-ffff-ffff-ffff-ffffffffffff'
9598
}
9699
if (!isTelemetryEnabled) {
97100
return '11111111-1111-1111-1111-111111111111'
98101
}
99102
try {
100-
let clientId = globalState.get<string>(telemetryClientIdGlobalStatekey)
103+
let clientId = globalState.tryGet('telemetryClientId', String)
101104
if (!clientId) {
102105
clientId = randomUUID()
103-
globalState.update(telemetryClientIdGlobalStatekey, clientId).then(undefined, (e) => {
104-
getLogger().error('getClientId: globalState.update failed: %O', e)
105-
})
106+
globalState.tryUpdate('telemetryClientId', clientId)
106107
}
107108
return clientId
108109
} catch (e) {
@@ -122,7 +123,7 @@ export const platformPair = () => `${env.appName.replace(/\s/g, '-')}/${version}
122123
*/
123124
export function getUserAgent(
124125
opt?: { includePlatform?: boolean; includeClientId?: boolean },
125-
globalState = globals.context.globalState
126+
globalState = globals.globalState
126127
): string {
127128
const pairs = isAmazonQ()
128129
? [`AmazonQ-For-VSCode/${extensionVersion}`]
@@ -212,9 +213,9 @@ export function validateMetricEvent(event: MetricDatum, fatal: boolean) {
212213
export async function setupTelemetryId(extensionContext: vscode.ExtensionContext) {
213214
try {
214215
if (isWeb()) {
215-
await globals.context.globalState.update(telemetryClientIdGlobalStatekey, vscode.env.machineId)
216+
await globals.globalState.update('telemetryClientId', vscode.env.machineId)
216217
} else {
217-
const currentClientId = globals.context.globalState.get<string>(telemetryClientIdGlobalStatekey)
218+
const currentClientId = globals.globalState.tryGet('telemetryClientId', String)
218219
const storedClientId = process.env[telemetryClientIdEnvKey]
219220
if (currentClientId && storedClientId) {
220221
if (extensionContext.extension.id === VSCODE_EXTENSION_ID.awstoolkit) {
@@ -230,13 +231,13 @@ export async function setupTelemetryId(extensionContext: vscode.ExtensionContext
230231
}
231232
} else if (isAmazonQ()) {
232233
getLogger().debug(`telemetry: Set telemetry client id to ${storedClientId}`)
233-
await globals.context.globalState.update(telemetryClientIdGlobalStatekey, storedClientId)
234+
await globals.globalState.update('telemetryClientId', storedClientId)
234235
} else {
235236
getLogger().error(`Unexpected extension id ${extensionContext.extension.id}`)
236237
}
237238
} else if (!currentClientId && storedClientId) {
238239
getLogger().debug(`telemetry: Write telemetry client id to global state ${storedClientId}`)
239-
await globals.context.globalState.update(telemetryClientIdGlobalStatekey, storedClientId)
240+
await globals.globalState.update('telemetryClientId', storedClientId)
240241
} else if (currentClientId && !storedClientId) {
241242
getLogger().debug(`telemetry: Write telemetry client id to env ${currentClientId}`)
242243
process.env[telemetryClientIdEnvKey] = currentClientId

packages/core/src/test/fakeExtensionContext.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,7 @@ export class FakeExtensionContext implements vscode.ExtensionContext {
129129
const regionProvider = createTestRegionProvider({ awsContext })
130130
const outputChannel = new MockOutputChannel()
131131
const fakeTelemetryPublisher = new FakeTelemetryPublisher()
132-
const telemetryService = await DefaultTelemetryService.create(
133-
ctx,
134-
awsContext,
135-
undefined,
136-
fakeTelemetryPublisher
137-
)
132+
const telemetryService = await DefaultTelemetryService.create(awsContext, undefined, fakeTelemetryPublisher)
138133

139134
return {
140135
extensionContext: ctx,

packages/core/src/test/shared/defaultAwsClientBuilder.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { getClientId } from '../../shared/telemetry/util'
1212
import { FakeMemento } from '../fakeExtensionContext'
1313
import { FakeAwsContext } from '../utilities/fakeAwsContext'
1414
import { TestSettings } from '../utilities/testSettingsConfiguration'
15+
import { GlobalState } from '../../shared/globalState'
1516

1617
describe('DefaultAwsClientBuilder', function () {
1718
let builder: AWSClientBuilder
@@ -23,7 +24,7 @@ describe('DefaultAwsClientBuilder', function () {
2324
describe('createAndConfigureSdkClient', function () {
2425
it('includes Toolkit user-agent if no options are specified', async function () {
2526
const service = await builder.createAwsService(Service)
26-
const clientId = getClientId(new FakeMemento())
27+
const clientId = getClientId(new GlobalState(new FakeMemento()))
2728

2829
assert.strictEqual(!!service.config.customUserAgent, true)
2930
assert.strictEqual(
@@ -34,7 +35,7 @@ describe('DefaultAwsClientBuilder', function () {
3435

3536
it('adds Client-Id to user agent', async function () {
3637
const service = await builder.createAwsService(Service)
37-
const clientId = getClientId(new FakeMemento())
38+
const clientId = getClientId(new GlobalState(new FakeMemento()))
3839
const regex = new RegExp(`ClientId/${clientId}`)
3940
assert.ok(service.config.customUserAgent?.match(regex))
4041
})

packages/core/src/test/shared/telemetry/telemetryService.test.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import * as sinon from 'sinon'
99
import * as fs from 'fs-extra'
1010
import { DefaultTelemetryService } from '../../../shared/telemetry/telemetryService'
1111
import { AccountStatus } from '../../../shared/telemetry/telemetryClient'
12-
import { FakeExtensionContext } from '../../fakeExtensionContext'
1312

1413
import {
1514
defaultTestAccountId,
@@ -39,13 +38,12 @@ describe('TelemetryService', function () {
3938
const testFlushPeriod = 10
4039
let clock: FakeTimers.InstalledClock
4140
let sandbox: sinon.SinonSandbox
42-
let mockContext: FakeExtensionContext
4341
let mockPublisher: FakeTelemetryPublisher
4442
let service: DefaultTelemetryService
4543
let logger: TelemetryLogger
4644

4745
async function initService(awsContext = new FakeAwsContext()): Promise<DefaultTelemetryService> {
48-
const newService = await DefaultTelemetryService.create(mockContext, awsContext, undefined, mockPublisher)
46+
const newService = await DefaultTelemetryService.create(awsContext, undefined, mockPublisher)
4947
newService.flushPeriod = testFlushPeriod
5048
await newService.setTelemetryEnabled(true)
5149

@@ -62,7 +60,6 @@ describe('TelemetryService', function () {
6260
})
6361

6462
beforeEach(async function () {
65-
mockContext = await FakeExtensionContext.create()
6663
mockPublisher = new FakeTelemetryPublisher()
6764
service = await initService()
6865
logger = service.logger

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

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Settings } from '../../../shared/settings'
99
import { convertLegacy, getClientId, getUserAgent, platformPair, TelemetryConfig } from '../../../shared/telemetry/util'
1010
import { extensionVersion } from '../../../shared/vscode/env'
1111
import { FakeMemento } from '../../fakeExtensionContext'
12+
import { GlobalState } from '../../../shared/globalState'
1213

1314
describe('TelemetryConfig', function () {
1415
const settingKey = 'aws.telemetry'
@@ -100,20 +101,20 @@ describe('TelemetryConfig', function () {
100101

101102
describe('getClientId', function () {
102103
it('should generate a unique id', async function () {
103-
const c1 = getClientId(new FakeMemento(), true, false, 'x1')
104-
const c2 = getClientId(new FakeMemento(), true, false, 'x2')
104+
const c1 = getClientId(new GlobalState(new FakeMemento()), true, false, 'x1')
105+
const c2 = getClientId(new GlobalState(new FakeMemento()), true, false, 'x2')
105106
assert.notStrictEqual(c1, c2)
106107
})
107108

108109
it('returns the same value across the calls sequentially', async function () {
109-
const memento = new FakeMemento()
110+
const memento = new GlobalState(new FakeMemento())
110111
const c1 = getClientId(memento, true, false, 'y1')
111112
const c2 = getClientId(memento, true, false, 'y2')
112113
assert.strictEqual(c1, c2)
113114
})
114115

115-
it('returns the nil UUID if it fails to save generated UUID', async function () {
116-
const mememto: Memento = {
116+
it('returns nil UUID if it fails to save generated UUID', async function () {
117+
const memento: Memento = {
117118
keys: () => [],
118119
get(key) {
119120
return undefined
@@ -122,34 +123,40 @@ describe('getClientId', function () {
122123
throw new Error()
123124
},
124125
}
125-
const clientId = getClientId(mememto, true, false, 'x3')
126-
assert.strictEqual(clientId, '00000000-0000-0000-0000-000000000000')
126+
const clientId = getClientId(new GlobalState(memento), true, false, 'x3')
127+
// XXX: `notStrictEqual` since getClientId() is now synchronous. Because memento.update() is async.
128+
assert.notStrictEqual(clientId, '00000000-0000-0000-0000-000000000000')
127129
})
128130

129-
it('returns the nil UUID if fails to retrive a saved UUID.', async function () {
130-
const mememto: Memento = {
131+
it('returns the nil UUID if it fails to get the saved UUID', async function () {
132+
const memento: Memento = {
131133
keys: () => [],
132134
get(key) {
133135
throw new Error()
134136
},
135137
async update(key, value) {},
136138
}
137-
const clientId = getClientId(mememto, true, false, 'x4')
139+
class FakeGlobalState extends GlobalState {
140+
override tryGet<T>(key: any, defaultVal?: T): T | undefined {
141+
return this.memento.get(key)
142+
}
143+
}
144+
const clientId = getClientId(new FakeGlobalState(memento), true, false, 'x4')
138145
assert.strictEqual(clientId, '00000000-0000-0000-0000-000000000000')
139146
})
140147

141148
it('should be ffffffff-ffff-ffff-ffff-ffffffffffff if in test enviroment', async function () {
142-
const clientId = getClientId(new FakeMemento(), true)
149+
const clientId = getClientId(new GlobalState(new FakeMemento()), true)
143150
assert.strictEqual(clientId, 'ffffffff-ffff-ffff-ffff-ffffffffffff')
144151
})
145152

146153
it('should be ffffffff-ffff-ffff-ffff-ffffffffffff if telemetry is not enabled in test enviroment', async function () {
147-
const clientId = getClientId(new FakeMemento(), false)
154+
const clientId = getClientId(new GlobalState(new FakeMemento()), false)
148155
assert.strictEqual(clientId, 'ffffffff-ffff-ffff-ffff-ffffffffffff')
149156
})
150157

151158
it('should be 11111111-1111-1111-1111-111111111111 if telemetry is not enabled', async function () {
152-
const clientId = getClientId(new FakeMemento(), false, false)
159+
const clientId = getClientId(new GlobalState(new FakeMemento()), false, false)
153160
assert.strictEqual(clientId, '11111111-1111-1111-1111-111111111111')
154161
})
155162
})

0 commit comments

Comments
 (0)