Skip to content

Commit 471b9a1

Browse files
committed
fix(telemetry): toolkit replacing amazon q clientId
Problem: There is a function called `setupTelemetryId` which syncs the client Id between the two extensions when both are installed. However, there is a race condition that causes toolkit to generate the client ID before calling this setup function. The result is that amazon q is signalled to update its client id with a fresh one from toolkit. This results in telemetry showing we have a new user even though that is not the case. Solution: Bypass this race condition by putting the logic in the client id generation function. We will also change how we compute the client ID so that extensions more intuitively converge to the same client id. See code for computation docs. Cases: 1. New user installs an extension -> client id is generated once and stored in global state 2. Existing extension user installs another extension -> new extension will fetch the client id from the first extension via env variables, and store it itself 3. Existing users of both extensions update to the newer versions -> client ids were already synced between extensions, so they will store their identical client ids to themselves and env variables There is an edge case that will still result in a failure: 4. Existing extension user installs another extension manually while vscode is closed -> the new extension loads first and stores its fresh client id in environment variables. The first extension will see this and update its client id to the new one, resulting in the original issue. This is unavoidable because: - Extensions can load in arbitrary orders - There is no way to know which extension's client ID is the one to use unless we check which one has something stored in its global state already. - This requires the new extension to ask the old extension, so it must be initialized already. - While waiting for this request, either extension could already start emitting telemetry. It has also already initialized a bunch of internal memory states with the other client id. - Creating a local file will only help if the file pre-exists. Users updating to this version will have no file to determine from. - We want to preserve current client IDs
1 parent 408befe commit 471b9a1

File tree

3 files changed

+91
-73
lines changed

3 files changed

+91
-73
lines changed

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

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,11 @@ import { DefaultTelemetryService } from './telemetryService'
1414
import { getLogger } from '../logger'
1515
import { getComputeRegion, isAmazonQ, isCloud9, productName } from '../extensionUtilities'
1616
import { openSettingsId, Settings } from '../settings'
17-
import { TelemetryConfig, setupTelemetryId } from './util'
17+
import { TelemetryConfig } from './util'
1818
import { isAutomation, isReleaseVersion } from '../vscode/env'
1919
import { AWSProduct } from './clienttelemetry'
2020
import { DefaultTelemetryClient } from './telemetryClient'
2121
import { telemetry } from './telemetry'
22-
import { Commands } from '../vscode/commands2'
2322

2423
export const noticeResponseViewSettings = localize('AWS.telemetry.notificationViewSettings', 'Settings')
2524
export const noticeResponseOk = localize('AWS.telemetry.notificationOk', 'OK')
@@ -71,19 +70,11 @@ export async function activate(
7170
})
7271
)
7372

74-
if (isAmazonQExt) {
75-
extensionContext.subscriptions.push(
76-
Commands.register('aws.amazonq.setupTelemetryId', async () => {
77-
await setupTelemetryId(extensionContext)
78-
})
79-
)
80-
}
81-
8273
// Prompt user about telemetry if they haven't been
8374
if (!isCloud9() && !hasUserSeenTelemetryNotice()) {
8475
showTelemetryNotice()
8576
}
86-
await setupTelemetryId(extensionContext)
77+
8778
await globals.telemetry.start()
8879
} catch (e) {
8980
// Only throw in a production build because:

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

Lines changed: 41 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,14 @@ import { Result } from './telemetry.gen'
1717
import { MetricDatum } from './clienttelemetry'
1818
import { isValidationExemptMetric } from './exemptMetrics'
1919
import { isAmazonQ, isCloud9, isSageMaker } from '../../shared/extensionUtilities'
20-
import { isExtensionInstalled, VSCODE_EXTENSION_ID } from '../utilities'
2120
import { randomUUID } from '../crypto'
22-
import { activateExtension } from '../utilities/vsCodeUtils'
2321
import { ClassToInterfaceType } from '../utilities/tsUtils'
2422

2523
const legacySettingsTelemetryValueDisable = 'Disable'
2624
const legacySettingsTelemetryValueEnable = 'Enable'
2725

2826
const TelemetryFlag = addTypeName('boolean', convertLegacy)
29-
const telemetryClientIdEnvKey = '__TELEMETRY_CLIENT_ID'
27+
export const telemetryClientIdEnvKey = '__TELEMETRY_CLIENT_ID'
3028

3129
export class TelemetryConfig {
3230
private readonly _toolkitConfig
@@ -83,6 +81,19 @@ export function convertLegacy(value: unknown): boolean {
8381
}
8482
}
8583

84+
/**
85+
* Calculates the clientId for the current profile. This calculation is performed once
86+
* on first call and the result is stored for the remainder of the session.
87+
*
88+
* Web mode will always compute to whatever is stored in global state or vscode.machineId.
89+
* For normal use, the clientId is fetched from the first providing source:
90+
* 1. clientId stored in process.env
91+
* 2. clientId stored in current extension's global state.
92+
* 3. a random UUID
93+
*
94+
* The clientId in the current extension's global state AND the clientId stored in process.env
95+
* is updated to the result of above to allow other extensions to converge to the same clientId.
96+
*/
8697
export const getClientId = memoize(
8798
/**
8899
* @param nonce Dummy parameter to allow tests to defeat memoize().
@@ -100,11 +111,34 @@ export const getClientId = memoize(
100111
return '11111111-1111-1111-1111-111111111111'
101112
}
102113
try {
103-
let clientId = globalState.tryGet('telemetryClientId', String)
104-
if (!clientId) {
105-
clientId = randomUUID()
106-
globalState.tryUpdate('telemetryClientId', clientId)
114+
const globalClientId = process.env[telemetryClientIdEnvKey] // truly global across all extensions
115+
const localClientId = globalState.tryGet('telemetryClientId', String) // local to extension, despite accessing "global" state
116+
let clientId: string
117+
118+
if (isWeb()) {
119+
const machineId = vscode.env.machineId
120+
clientId = localClientId ?? machineId
121+
getLogger().debug(
122+
'getClientId: web mode determined clientId: %s, stored clientId was: %s, vscode.machineId was: %s',
123+
clientId,
124+
localClientId,
125+
machineId
126+
)
127+
} else {
128+
clientId = globalClientId ?? localClientId ?? randomUUID()
129+
getLogger().debug(
130+
'getClientId: determined clientId as: %s, process.env clientId was: %s, stored clientId was: %s',
131+
clientId,
132+
globalClientId,
133+
localClientId
134+
)
135+
if (!globalClientId) {
136+
getLogger().debug(`getClientId: setting clientId in process.env to: %s`, clientId)
137+
process.env[telemetryClientIdEnvKey] = clientId
138+
}
107139
}
140+
141+
globalState.tryUpdate('telemetryClientId', clientId)
108142
return clientId
109143
} catch (e) {
110144
getLogger().error('getClientId: failed to create client id: %O', e)
@@ -205,53 +239,6 @@ export function validateMetricEvent(event: MetricDatum, fatal: boolean) {
205239
}
206240
}
207241

208-
/**
209-
* Setup the telemetry client id at extension activation.
210-
* This function is designed to let AWS Toolkit and Amazon Q share
211-
* the same telemetry client id.
212-
*/
213-
export async function setupTelemetryId(extensionContext: vscode.ExtensionContext) {
214-
try {
215-
if (isWeb()) {
216-
await globals.globalState.update('telemetryClientId', vscode.env.machineId)
217-
} else {
218-
const currentClientId = globals.globalState.tryGet('telemetryClientId', String)
219-
const storedClientId = process.env[telemetryClientIdEnvKey]
220-
if (currentClientId && storedClientId) {
221-
if (extensionContext.extension.id === VSCODE_EXTENSION_ID.awstoolkit) {
222-
getLogger().debug(`telemetry: Store telemetry client id to env ${currentClientId}`)
223-
process.env[telemetryClientIdEnvKey] = currentClientId
224-
// notify amazon q to use this stored client id
225-
// if amazon q activates first. Do not block on activate amazon q
226-
if (isExtensionInstalled(VSCODE_EXTENSION_ID.amazonq)) {
227-
void activateExtension(VSCODE_EXTENSION_ID.amazonq).then(async () => {
228-
getLogger().debug(`telemetry: notifying Amazon Q to adopt client id ${currentClientId}`)
229-
await vscode.commands.executeCommand('aws.amazonq.setupTelemetryId')
230-
})
231-
}
232-
} else if (isAmazonQ()) {
233-
getLogger().debug(`telemetry: Set telemetry client id to ${storedClientId}`)
234-
await globals.globalState.update('telemetryClientId', storedClientId)
235-
} else {
236-
getLogger().error(`Unexpected extension id ${extensionContext.extension.id}`)
237-
}
238-
} else if (!currentClientId && storedClientId) {
239-
getLogger().debug(`telemetry: Write telemetry client id to global state ${storedClientId}`)
240-
await globals.globalState.update('telemetryClientId', storedClientId)
241-
} else if (currentClientId && !storedClientId) {
242-
getLogger().debug(`telemetry: Write telemetry client id to env ${currentClientId}`)
243-
process.env[telemetryClientIdEnvKey] = currentClientId
244-
} else {
245-
const clientId = getClientId(globals.globalState)
246-
getLogger().debug(`telemetry: Setup telemetry client id ${clientId}`)
247-
process.env[telemetryClientIdEnvKey] = clientId
248-
}
249-
}
250-
} catch (err) {
251-
getLogger().error(`Error while setting up telemetry id ${err}`)
252-
}
253-
}
254-
255242
/**
256243
* Potentially helpful values for the 'source' field in telemetry.
257244
*/

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

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,18 @@
66
import assert from 'assert'
77
import { Memento, ConfigurationTarget } from 'vscode'
88
import { Settings } from '../../../shared/settings'
9-
import { convertLegacy, getClientId, getUserAgent, platformPair, TelemetryConfig } from '../../../shared/telemetry/util'
9+
import {
10+
convertLegacy,
11+
getClientId,
12+
getUserAgent,
13+
platformPair,
14+
telemetryClientIdEnvKey,
15+
TelemetryConfig,
16+
} from '../../../shared/telemetry/util'
1017
import { extensionVersion } from '../../../shared/vscode/env'
1118
import { FakeMemento } from '../../fakeExtensionContext'
1219
import { GlobalState } from '../../../shared/globalState'
20+
import { randomUUID } from 'crypto'
1321

1422
describe('TelemetryConfig', function () {
1523
const settingKey = 'aws.telemetry'
@@ -100,16 +108,48 @@ describe('TelemetryConfig', function () {
100108
})
101109

102110
describe('getClientId', function () {
103-
it('should generate a unique id', async function () {
104-
const c1 = getClientId(new GlobalState(new FakeMemento()), true, false, 'x1')
105-
const c2 = getClientId(new GlobalState(new FakeMemento()), true, false, 'x2')
111+
before(function () {
112+
delete process.env[telemetryClientIdEnvKey]
113+
})
114+
115+
afterEach(function () {
116+
delete process.env[telemetryClientIdEnvKey]
117+
})
118+
119+
function testGetClientId(globalState: GlobalState) {
120+
return getClientId(globalState, true, false, randomUUID())
121+
}
122+
123+
it('generates a unique id if no other id is available', function () {
124+
const c1 = testGetClientId(new GlobalState(new FakeMemento()))
125+
delete process.env[telemetryClientIdEnvKey]
126+
const c2 = testGetClientId(new GlobalState(new FakeMemento()))
106127
assert.notStrictEqual(c1, c2)
107128
})
108129

130+
it('uses id stored in global state if an id is not found in process.env', async function () {
131+
const expectedClientId = 'myId'
132+
133+
const memento = new GlobalState(new FakeMemento())
134+
await memento.update('telemetryClientId', expectedClientId)
135+
136+
assert.strictEqual(testGetClientId(memento), expectedClientId)
137+
})
138+
139+
it('uses the id stored in process.env if available', async function () {
140+
const expectedClientId = 'myId'
141+
142+
const e = new GlobalState(new FakeMemento())
143+
await e.update('telemetryClientId', randomUUID())
144+
process.env[telemetryClientIdEnvKey] = expectedClientId
145+
146+
assert.strictEqual(testGetClientId(new GlobalState(new FakeMemento())), expectedClientId)
147+
})
148+
109149
it('returns the same value across the calls sequentially', async function () {
110150
const memento = new GlobalState(new FakeMemento())
111-
const c1 = getClientId(memento, true, false, 'y1')
112-
const c2 = getClientId(memento, true, false, 'y2')
151+
const c1 = testGetClientId(memento)
152+
const c2 = testGetClientId(memento)
113153
assert.strictEqual(c1, c2)
114154
})
115155

@@ -123,7 +163,7 @@ describe('getClientId', function () {
123163
throw new Error()
124164
},
125165
}
126-
const clientId = getClientId(new GlobalState(memento), true, false, 'x3')
166+
const clientId = testGetClientId(new GlobalState(memento))
127167
// XXX: `notStrictEqual` since getClientId() is now synchronous. Because memento.update() is async.
128168
assert.notStrictEqual(clientId, '00000000-0000-0000-0000-000000000000')
129169
})
@@ -141,7 +181,7 @@ describe('getClientId', function () {
141181
return this.memento.get(key)
142182
}
143183
}
144-
const clientId = getClientId(new FakeGlobalState(memento), true, false, 'x4')
184+
const clientId = testGetClientId(new FakeGlobalState(memento))
145185
assert.strictEqual(clientId, '00000000-0000-0000-0000-000000000000')
146186
})
147187

0 commit comments

Comments
 (0)