Skip to content

Commit 1822fb6

Browse files
authored
telemetry: add UA environment variable to SAM CLI invocations (#3170)
## Problem SAM CLI doesn't know which products are executing it on the user's behalf ## Solution Add an environment variable containing additional context
1 parent 98f6b77 commit 1822fb6

File tree

8 files changed

+84
-14
lines changed

8 files changed

+84
-14
lines changed

src/shared/awsClientBuilder.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,9 @@
66
import { Request, AWSError, Credentials } from 'aws-sdk'
77
import { CredentialsOptions } from 'aws-sdk/lib/credentials'
88
import { ServiceConfigurationOptions } from 'aws-sdk/lib/service'
9-
import { env, version } from 'vscode'
109
import { AwsContext } from './awsContext'
11-
import globals from './extensionGlobals'
1210
import { DevSettings } from './settings'
13-
import { getClientId } from './telemetry/util'
14-
import { extensionVersion } from './vscode/env'
11+
import { getUserAgent } from './telemetry/util'
1512

1613
// These are not on the public API but are very useful for logging purposes.
1714
// Tests guard against the possibility that these values change unexpectedly.
@@ -129,9 +126,7 @@ export class DefaultAWSClientBuilder implements AWSClientBuilder {
129126
}
130127

131128
if (userAgent && !opt.customUserAgent) {
132-
const platformName = env.appName.replace(/\s/g, '-')
133-
const clientId = await getClientId(globals.context.globalState)
134-
opt.customUserAgent = `AWS-Toolkit-For-VSCode/${extensionVersion} ${platformName}/${version} ClientId/${clientId}`
129+
opt.customUserAgent = await getUserAgent({ includeClientId: true })
135130
}
136131

137132
const apiConfig = (opt as { apiConfig?: { metadata?: Record<string, string> } } | undefined)?.apiConfig

src/shared/sam/cli/samCliInvoker.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import * as logger from '../../logger'
77
import { ChildProcess, ChildProcessResult } from '../../utilities/childProcess'
88
import {
9+
addTelemetryEnvVar,
910
makeRequiredSamCliProcessInvokeOptions,
1011
SamCliProcessInvokeOptions,
1112
SamCliProcessInvoker,
@@ -47,7 +48,7 @@ export class DefaultSamCliProcessInvoker implements SamCliProcessInvoker {
4748
const samCommand = sam.path ? sam.path : 'sam'
4849
this.childProcess = new ChildProcess(samCommand, invokeOptions.arguments, {
4950
logging: options?.logging ? 'yes' : 'no',
50-
spawnOptions: invokeOptions.spawnOptions,
51+
spawnOptions: await addTelemetryEnvVar(options?.spawnOptions),
5152
})
5253

5354
getLogger('channel').info(localize('AWS.running.command', 'Command: {0}', `${this.childProcess}`))

src/shared/sam/cli/samCliInvokerUtils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import { SpawnOptions } from 'child_process'
77
import { getLogger } from '../../logger'
8+
import { getUserAgent } from '../../telemetry/util'
89
import { ChildProcessResult, ChildProcessOptions } from '../../utilities/childProcess'
910

1011
export interface SamCliProcessInvokeOptions {
@@ -99,3 +100,13 @@ function startsWithEscapeSequence(text: string, sequences = acceptedSequences):
99100
function startsWithError(text: string): boolean {
100101
return text.startsWith('Error:')
101102
}
103+
104+
export async function addTelemetryEnvVar(options: SpawnOptions | undefined): Promise<SpawnOptions> {
105+
return {
106+
...options,
107+
env: {
108+
SAM_CLI_TELEMETRY_FROM_IDE: await getUserAgent({ includeClientId: false }),
109+
...options?.env,
110+
},
111+
}
112+
}

src/shared/sam/cli/samCliLocalInvoke.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { removeAnsi } from '../../utilities/textUtilities'
1414
import * as vscode from 'vscode'
1515
import globals from '../../extensionGlobals'
1616
import { SamCliSettings } from './samCliSettings'
17+
import { addTelemetryEnvVar } from './samCliInvokerUtils'
1718

1819
const localize = nls.loadMessageBundle()
1920

@@ -58,7 +59,9 @@ export class DefaultSamLocalInvokeCommand implements SamLocalInvokeCommand {
5859
) {}
5960

6061
public async invoke({ options, ...params }: SamLocalInvokeCommandArgs): Promise<ChildProcess> {
61-
const childProcess = new ChildProcess(params.command, params.args, { spawnOptions: options })
62+
const childProcess = new ChildProcess(params.command, params.args, {
63+
spawnOptions: await addTelemetryEnvVar(options),
64+
})
6265
getLogger('channel').info('AWS.running.command', 'Command: {0}', `${childProcess}`)
6366
// "sam local invoke", "sam local start-api", etc.
6467
const samCommandName = `sam ${params.args[0]} ${params.args[1]}`

src/shared/sam/sync.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { SamCliInfoInvocation } from './cli/samCliInfo'
3939
import { parse } from 'semver'
4040
import { isAutomation } from '../vscode/env'
4141
import { getOverriddenParameters } from '../../lambda/config/parameterUtils'
42+
import { addTelemetryEnvVar } from './cli/samCliInvokerUtils'
4243

4344
export interface SyncParams {
4445
readonly region: string
@@ -348,10 +349,10 @@ export async function runSamSync(args: SyncParams) {
348349
}
349350

350351
const sam = new ChildProcess(samCliPath, ['sync', ...boundArgs], {
351-
spawnOptions: {
352+
spawnOptions: await addTelemetryEnvVar({
352353
cwd: args.projectRoot.fsPath,
353354
env: await injectCredentials(args.connection),
354-
},
355+
}),
355356
})
356357

357358
await runSyncInTerminal(sam)

src/shared/telemetry/util.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import { Memento } from 'vscode'
6+
import { env, Memento, version } from 'vscode'
77
import { getLogger } from '../logger'
88
import { fromExtensionManifest } from '../settings'
99
import { shared } from '../utilities/functionUtils'
10-
import { isAutomation } from '../vscode/env'
10+
import { extensionVersion, isAutomation } from '../vscode/env'
1111
import { v4 as uuidv4 } from 'uuid'
1212
import { addTypeName } from '../utilities/typeConstructors'
13+
import globals from '../extensionGlobals'
1314

1415
const legacySettingsTelemetryValueDisable = 'Disable'
1516
const legacySettingsTelemetryValueEnable = 'Enable'
@@ -59,3 +60,23 @@ export const getClientId = shared(
5960
}
6061
}
6162
)
63+
64+
/**
65+
* Returns a string that should be used as the extension's user agent.
66+
*
67+
* Omits the `ClientId` pair by default.
68+
*/
69+
export async function getUserAgent(
70+
opt?: { includeClientId?: boolean },
71+
globalState = globals.context.globalState
72+
): Promise<string> {
73+
const platformName = env.appName.replace(/\s/g, '-')
74+
const pairs = [`AWS-Toolkit-For-VSCode/${extensionVersion}`, `${platformName}/${version}`]
75+
76+
if (opt?.includeClientId) {
77+
const clientId = await getClientId(globalState)
78+
pairs.push(`ClientId/${clientId}`)
79+
}
80+
81+
return pairs.join(' ')
82+
}

src/test/shared/sam/cli/samCliInvokerUtils.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import * as assert from 'assert'
77
import {
8+
addTelemetryEnvVar,
89
collectAcceptedErrorMessages,
910
logAndThrowIfUnexpectedExitCode,
1011
makeUnexpectedExitCodeError,
@@ -81,3 +82,20 @@ describe('collectAcceptedErrorMessages()', async () => {
8182
assert(!result.includes(prependEscapeCode('[100m This is not an accepted escape sequence')))
8283
})
8384
})
85+
86+
describe('addTelemetryEnvVar', async function () {
87+
it('adds a new variable, preserving the existing contents', async function () {
88+
const result = await addTelemetryEnvVar({
89+
cwd: '/foo',
90+
env: { AWS_REGION: 'us-east-1' },
91+
})
92+
93+
assert.deepStrictEqual(result, {
94+
cwd: '/foo',
95+
env: {
96+
SAM_CLI_TELEMETRY_FROM_IDE: result.env?.['SAM_CLI_TELEMETRY_FROM_IDE'],
97+
AWS_REGION: 'us-east-1',
98+
},
99+
})
100+
})
101+
})

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
import * as assert from 'assert'
77
import { Memento, ConfigurationTarget } from 'vscode'
88
import { Settings } from '../../../shared/settings'
9-
import { convertLegacy, getClientId, TelemetryConfig } from '../../../shared/telemetry/util'
9+
import { convertLegacy, getClientId, getUserAgent, TelemetryConfig } from '../../../shared/telemetry/util'
10+
import { extensionVersion } from '../../../shared/vscode/env'
1011
import { FakeMemento } from '../../fakeExtensionContext'
1112

1213
describe('TelemetryConfig', function () {
@@ -157,3 +158,22 @@ describe('getClientId', function () {
157158
assert.strictEqual(clientId, '11111111-1111-1111-1111-111111111111')
158159
})
159160
})
161+
162+
describe('getUserAgent', function () {
163+
it('includes product name and version', async function () {
164+
const userAgent = await getUserAgent()
165+
const lastPair = userAgent.split(' ')[0]
166+
assert.ok(lastPair?.startsWith(`AWS-Toolkit-For-VSCode/${extensionVersion}`))
167+
})
168+
169+
it('omits `ClientId` by default', async function () {
170+
const userAgent = await getUserAgent()
171+
assert.ok(!userAgent.includes('ClientId'))
172+
})
173+
174+
it('includes `ClientId` at the end if opted in', async function () {
175+
const userAgent = await getUserAgent({ includeClientId: true })
176+
const lastPair = userAgent.split(' ').pop()
177+
assert.ok(lastPair?.startsWith('ClientId/'))
178+
})
179+
})

0 commit comments

Comments
 (0)