Skip to content

Commit 800b6a5

Browse files
bug(telemetry): sessionId not unique
Problem: There seems to be a bug with vscode.env.sessionId since we observed in telemetry multiple clientIds who has the same sessionId. Solution: Generate our own sessionId and share it between extensions through globalThis Signed-off-by: nkomonen-amazon <[email protected]>
1 parent 871b894 commit 800b6a5

File tree

4 files changed

+120
-87
lines changed

4 files changed

+120
-87
lines changed

packages/core/src/dev/activation.ts

Lines changed: 54 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -61,57 +61,59 @@ let targetAuth: Auth
6161
* on selection. There is no support for name-spacing. Just add the relevant
6262
* feature/module as a description so it can be moved around easier.
6363
*/
64-
const menuOptions: Record<DevFunction, MenuOption> = {
65-
installVsix: {
66-
label: 'Install VSIX on Remote Environment',
67-
description: 'CodeCatalyst',
68-
detail: 'Automatically upload/install a VSIX to a remote host',
69-
executor: installVsixCommand,
70-
},
71-
openTerminal: {
72-
label: 'Open Remote Terminal',
73-
description: 'CodeCatalyst',
74-
detail: 'Opens a new terminal connected to the remote environment',
75-
executor: openTerminalCommand,
76-
},
77-
deleteDevEnv: {
78-
label: 'Delete Workspace',
79-
description: 'CodeCatalyst',
80-
detail: 'Deletes the selected Dev Environment',
81-
executor: deleteDevEnvCommand,
82-
},
83-
editStorage: {
84-
label: 'Show or Edit globalState',
85-
description: 'VS Code',
86-
detail: 'Shows all globalState values, or edit a globalState/secret item',
87-
executor: openStorageFromInput,
88-
},
89-
showEnvVars: {
90-
label: 'Show Environment Variables',
91-
description: 'AWS Toolkit',
92-
detail: 'Shows all environment variable values',
93-
executor: () => showState('envvars'),
94-
},
95-
deleteSsoConnections: {
96-
label: 'Auth: Delete SSO Connections',
97-
detail: 'Deletes all SSO Connections the extension is using.',
98-
executor: deleteSsoConnections,
99-
},
100-
expireSsoConnections: {
101-
label: 'Auth: Expire SSO Connections',
102-
detail: 'Force expires all SSO Connections, in to a "needs reauthentication" state.',
103-
executor: expireSsoConnections,
104-
},
105-
editAuthConnections: {
106-
label: 'Auth: Edit Connections',
107-
detail: 'Opens editor to all Auth Connections the extension is using.',
108-
executor: editSsoConnections,
109-
},
110-
forceIdeCrash: {
111-
label: 'Crash: Force IDE ExtHost Crash',
112-
detail: `Will SIGKILL ExtHost, { pid: ${process.pid}, sessionId: '${getSessionId().slice(0, 8)}-...' }, but the IDE itself will not crash.`,
113-
executor: forceQuitIde,
114-
},
64+
const menuOptions: () => Record<DevFunction, MenuOption> = () => {
65+
return {
66+
installVsix: {
67+
label: 'Install VSIX on Remote Environment',
68+
description: 'CodeCatalyst',
69+
detail: 'Automatically upload/install a VSIX to a remote host',
70+
executor: installVsixCommand,
71+
},
72+
openTerminal: {
73+
label: 'Open Remote Terminal',
74+
description: 'CodeCatalyst',
75+
detail: 'Opens a new terminal connected to the remote environment',
76+
executor: openTerminalCommand,
77+
},
78+
deleteDevEnv: {
79+
label: 'Delete Workspace',
80+
description: 'CodeCatalyst',
81+
detail: 'Deletes the selected Dev Environment',
82+
executor: deleteDevEnvCommand,
83+
},
84+
editStorage: {
85+
label: 'Show or Edit globalState',
86+
description: 'VS Code',
87+
detail: 'Shows all globalState values, or edit a globalState/secret item',
88+
executor: openStorageFromInput,
89+
},
90+
showEnvVars: {
91+
label: 'Show Environment Variables',
92+
description: 'AWS Toolkit',
93+
detail: 'Shows all environment variable values',
94+
executor: () => showState('envvars'),
95+
},
96+
deleteSsoConnections: {
97+
label: 'Auth: Delete SSO Connections',
98+
detail: 'Deletes all SSO Connections the extension is using.',
99+
executor: deleteSsoConnections,
100+
},
101+
expireSsoConnections: {
102+
label: 'Auth: Expire SSO Connections',
103+
detail: 'Force expires all SSO Connections, in to a "needs reauthentication" state.',
104+
executor: expireSsoConnections,
105+
},
106+
editAuthConnections: {
107+
label: 'Auth: Edit Connections',
108+
detail: 'Opens editor to all Auth Connections the extension is using.',
109+
executor: editSsoConnections,
110+
},
111+
forceIdeCrash: {
112+
label: 'Crash: Force IDE ExtHost Crash',
113+
detail: `Will SIGKILL ExtHost, { pid: ${process.pid}, sessionId: '${getSessionId().slice(0, 8)}-...' }, but the IDE itself will not crash.`,
114+
executor: forceQuitIde,
115+
},
116+
}
115117
}
116118

117119
/**
@@ -167,7 +169,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
167169
globalState = targetContext.globalState
168170
targetAuth = opts.auth
169171
void openMenu(
170-
entries(menuOptions)
172+
entries(menuOptions())
171173
.filter((e) => (opts.menuOptions ?? Object.keys(menuOptions)).includes(e[0]))
172174
.map((e) => e[1])
173175
)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ export async function activate(
7979

8080
if (globals.telemetry.telemetryEnabled) {
8181
// Only log the IDs if telemetry is enabled, so that users who have it disabled do not think we are sending events.
82-
getLogger().info(`Telemetry Client ID: ${globals.telemetry.clientId}`)
83-
getLogger().info(`Telemetry Session ID: ${getSessionId()}`)
82+
getLogger().info(`Telemetry clientId: ${globals.telemetry.clientId}`)
83+
getLogger().info(`Telemetry sessionId: ${getSessionId()}`)
8484
}
8585
} catch (e) {
8686
// Only throw in a production build because:

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

Lines changed: 55 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ import { Result } from './telemetry.gen'
2424
import { MetricDatum } from './clienttelemetry'
2525
import { isValidationExemptMetric } from './exemptMetrics'
2626
import { isAmazonQ, isCloud9, isSageMaker } from '../../shared/extensionUtilities'
27-
import { randomUUID } from '../crypto'
27+
import { isUuid, randomUUID } from '../crypto'
2828
import { ClassToInterfaceType } from '../utilities/tsUtils'
2929
import { FunctionEntry, type TelemetryTracer } from './spans'
3030
import { telemetry } from './telemetry'
31-
import { v5 as uuidV5 } from 'uuid'
31+
import { v4 as uuidV4, v5 as uuidV5 } from 'uuid'
3232

3333
const legacySettingsTelemetryValueDisable = 'Disable'
3434
const legacySettingsTelemetryValueEnable = 'Enable'
@@ -98,33 +98,60 @@ export function convertLegacy(value: unknown): boolean {
9898
* can be used in conjunction with the client ID to differntiate between
9999
* different VS Code windows on a users machine.
100100
*
101-
* ### Rules:
102-
*
103-
* - On startup of a new application instance, a new session ID must be created.
104-
* - This identifier must be a `UUID`, it is enforced by the telemetry service.
105-
* - The session ID must be different from all other IDs on the machine
106-
* - A session ID exists until the application instance is terminated.
107-
* It should never be used again after termination.
108-
* - All extensions on the same application instance MUST return the same session ID.
109-
* - This will allow us to know which of our extensions (eg Q vs Toolkit) are in the
110-
* same VS Code window.
111-
*
112-
* `vscode.env.sessionId` behaves as described aboved, EXCEPT its
113-
* value looks close to a UUID by does not exactly match it (has additional characters).
114-
* As a result we process it through uuidV5 which creates a proper UUID from it.
115-
* uuidV5 is idempotent, so as long as `vscode.env.sessionId` returns the same value,
116-
* we will get the same UUID.
101+
* See spec: https://quip-amazon.com/9gqrAqwO5FCE
117102
*/
118-
export const getSessionId = once(() => uuidV5(vscode.env.sessionId, sessionIdNonce))
119-
/**
120-
* This is an arbitrary nonce that is used in creating a v5 UUID for Session ID. We only
121-
* have this since the spec requires it.
122-
* - This should ONLY be used by {@link getSessionId}.
123-
* - This value MUST NOT change during runtime, otherwise {@link getSessionId} will lose its
124-
* idempotency. But, if there was a reason to change the value in a PR, it would not be an issue.
125-
* - This is exported only for testing.
126-
*/
127-
export const sessionIdNonce = '44cfdb20-b30b-4585-a66c-9f48f24f99b5' as const
103+
export const getSessionId = once(() => SessionId.getSessionId())
104+
105+
/** IMPORTANT: Use {@link getSessionId()} only. This is exported just for testing. */
106+
export class SessionId {
107+
public static getSessionId(): string {
108+
// This implementation does not work in web
109+
if (!isWeb()) {
110+
return this._getSessionId()
111+
}
112+
// A best effort at a sessionId just for web mode
113+
return this._getVscSessionId()
114+
}
115+
116+
/**
117+
* This implementation assumes that the `globalThis` is shared between extensions in the same
118+
* Extension Host, so we can share a global variable that way.
119+
*
120+
* This does not seem to work on web mode since the `globalThis` is not shared due to WebWorker design
121+
*/
122+
private static _getSessionId() {
123+
const g = globalThis as any
124+
if (g.amzn_sessionId === undefined || !isUuid(g.amzn_sessionId)) {
125+
g.amzn_sessionId = uuidV4()
126+
}
127+
return g.amzn_sessionId
128+
}
129+
130+
/**
131+
* `vscode.env.sessionId` looks close to a UUID by does not exactly match it (has additional characters).
132+
* As a result we process it through uuidV5 which creates a proper UUID from it.
133+
* uuidV5 is idempotent, so as long as `vscode.env.sessionId` returns the same value,
134+
* we will get the same UUID.
135+
*
136+
* We were initially using this implementation for all session ids, but it has some caveats:
137+
* - If the extension host crashes, sesionId stays the same since the parent VSC process defines it and that does not crash.
138+
* We wanted it to generate a new sessionId on ext host crash.
139+
* - This value may not be reliable, see the following sessionId in telemetry, it contains many events
140+
* all from different client ids: `sessionId: cabea8e7-a8a1-5e51-a60e-07218f4a5937`
141+
*/
142+
private static _getVscSessionId() {
143+
return uuidV5(vscode.env.sessionId, this.sessionIdNonce)
144+
}
145+
/**
146+
* This is an arbitrary nonce that is used in creating a v5 UUID for Session ID. We only
147+
* have this since the spec requires it.
148+
* - This should ONLY be used by {@link getSessionId}.
149+
* - This value MUST NOT change during runtime, otherwise {@link getSessionId} will lose its
150+
* idempotency. But, if there was a reason to change the value in a PR, it would not be an issue.
151+
* - This is exported only for testing.
152+
*/
153+
private static readonly sessionIdNonce = '44cfdb20-b30b-4585-a66c-9f48f24f99b5'
154+
}
128155

129156
/**
130157
* Calculates the clientId for the current profile. This calculation is performed once

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
getSessionId,
1313
getUserAgent,
1414
platformPair,
15-
sessionIdNonce,
15+
SessionId,
1616
telemetryClientIdEnvKey,
1717
TelemetryConfig,
1818
} from '../../../shared/telemetry/util'
@@ -112,14 +112,18 @@ describe('TelemetryConfig', function () {
112112

113113
describe('getSessionId', function () {
114114
it('returns a stable UUID', function () {
115-
const result = getSessionId()
115+
const result = SessionId.getSessionId()
116116

117117
assert.deepStrictEqual(isUuid(result), true)
118-
assert.deepStrictEqual(getSessionId(), result, 'Subsequent call did not return the same UUID')
118+
assert.deepStrictEqual(SessionId.getSessionId(), result, 'Subsequent call did not return the same UUID')
119119
})
120120

121-
it('nonce is the same as always', function () {
122-
assert.deepStrictEqual(sessionIdNonce, '44cfdb20-b30b-4585-a66c-9f48f24f99b5')
121+
it('overwrites something that does not look like a UUID', function () {
122+
;(globalThis as any).amzn_sessionId = 'notAUUID'
123+
const result = SessionId.getSessionId()
124+
125+
assert.deepStrictEqual(isUuid(result), true)
126+
assert.deepStrictEqual(SessionId.getSessionId(), result, 'Subsequent call did not return the same UUID')
123127
})
124128
})
125129

0 commit comments

Comments
 (0)