Skip to content

Commit 16ccd1b

Browse files
andrewyuqaws-toolkit-automationjustinmk3
authored
CodeWhisperer: ab testing feature (#4109)
* CodeWhisperer: Add OptOut field to GenerateCompletions API (#3989) Add OptOut field to GenerateCompletions API * CodeWhisperer: Add feature fetching component (#4047) * CodeWhisperer: Add feature fetching component 1. Add a component to fetch feature assignments every 30 mins, calling ListFeatureEvaluations API and cache values in memory. 2. Add a field in UserTriggerDecision codewhispererFeatureEvaluations to record all the feature configs in a string, which can be queried on Kibana * remove unused function * implment a fetchFeatureConfigs command to resolve cicular dependency * add a test for getFeatureConfigsTelemetry * Add UserContext, suggestionCount and clientId into STE (#4108) * use const instead of let (#4115) --------- Co-authored-by: aws-toolkit-automation <[email protected]> Co-authored-by: Justin M. Keyes <[email protected]>
1 parent af655f6 commit 16ccd1b

22 files changed

+376
-125
lines changed

src/codewhisperer/activation.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
applySecurityFix,
4040
signoutCodeWhisperer,
4141
showManageCwConnections,
42+
fetchFeatureConfigsCmd,
4243
} from './commands/basicCommands'
4344
import { sleep } from '../shared/utilities/timeoutUtils'
4445
import { ReferenceLogViewProvider } from './service/referenceLogViewProvider'
@@ -206,6 +207,8 @@ export async function activate(context: ExtContext): Promise<void> {
206207
selectCustomizationPrompt.register(),
207208
// notify new customizations
208209
notifyNewCustomizationsCmd.register(),
210+
// fetch feature configs
211+
fetchFeatureConfigsCmd.register(),
209212
/**
210213
* On recommendation acceptance
211214
*/

src/codewhisperer/client/codewhisperer.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ import { AWSError, Credentials, Service } from 'aws-sdk'
77
import globals from '../../shared/extensionGlobals'
88
import * as CodeWhispererClient from './codewhispererclient'
99
import * as CodeWhispererUserClient from './codewhispereruserclient'
10-
import { ListAvailableCustomizationsResponse, SendTelemetryEventRequest } from './codewhispereruserclient'
10+
import {
11+
ListAvailableCustomizationsResponse,
12+
ListFeatureEvaluationsRequest,
13+
ListFeatureEvaluationsResponse,
14+
SendTelemetryEventRequest,
15+
} from './codewhispereruserclient'
1116
import * as CodeWhispererConstants from '../models/constants'
1217
import { ServiceOptions } from '../../shared/awsClientBuilder'
1318
import { hasVendedIamCredentials } from '../../auth/auth'
@@ -22,6 +27,9 @@ import { session } from '../util/codeWhispererSession'
2227
import { getLogger } from '../../shared/logger'
2328
import { indent } from '../../shared/utilities/textUtilities'
2429
import { keepAliveHeader } from './agent'
30+
import { getOptOutPreference } from '../util/commonUtil'
31+
import * as os from 'os'
32+
import { getClientId } from '../../shared/telemetry/util'
2533

2634
export type ProgrammingLanguage = Readonly<
2735
CodeWhispererClient.ProgrammingLanguage | CodeWhispererUserClient.ProgrammingLanguage
@@ -219,17 +227,46 @@ export class DefaultCodeWhispererClient {
219227
}
220228

221229
public async sendTelemetryEvent(request: SendTelemetryEventRequest) {
222-
const requestWithOptOut: SendTelemetryEventRequest = {
230+
const requestWithCommonFields: SendTelemetryEventRequest = {
223231
...request,
224-
optOutPreference: globals.telemetry.telemetryEnabled ? 'OPTIN' : 'OPTOUT',
232+
optOutPreference: getOptOutPreference(),
233+
userContext: {
234+
ideCategory: 'VSCODE',
235+
operatingSystem: this.getOperatingSystem(),
236+
product: 'CodeWhisperer',
237+
clientId: await getClientId(globals.context.globalState),
238+
},
225239
}
226240
if (!AuthUtil.instance.isValidEnterpriseSsoInUse() && !globals.telemetry.telemetryEnabled) {
227241
return
228242
}
229-
const response = await (await this.createUserSdkClient()).sendTelemetryEvent(requestWithOptOut).promise()
243+
const response = await (await this.createUserSdkClient()).sendTelemetryEvent(requestWithCommonFields).promise()
230244
getLogger().debug(`codewhisperer: sendTelemetryEvent requestID: ${response.$response.requestId}`)
231245
}
232246

247+
public async listFeatureEvaluations(): Promise<ListFeatureEvaluationsResponse> {
248+
const request: ListFeatureEvaluationsRequest = {
249+
userContext: {
250+
ideCategory: 'VSCODE',
251+
operatingSystem: this.getOperatingSystem(),
252+
product: 'CodeWhisperer',
253+
clientId: await getClientId(globals.context.globalState),
254+
},
255+
}
256+
return (await this.createUserSdkClient()).listFeatureEvaluations(request).promise()
257+
}
258+
259+
private getOperatingSystem(): string {
260+
const osId = os.platform() // 'darwin', 'win32', 'linux', etc.
261+
if (osId === 'darwin') {
262+
return 'MAC'
263+
} else if (osId === 'win32') {
264+
return 'WINDOWS'
265+
} else {
266+
return 'LINUX'
267+
}
268+
}
269+
233270
/**
234271
* @description Use this function to start the transformation job.
235272
* @param request

src/codewhisperer/client/user-service-2.json

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@
5353
"output": { "shape": "CreateUploadUrlResponse" },
5454
"errors": [
5555
{ "shape": "ThrottlingException" },
56+
{ "shape": "ConflictException" },
57+
{ "shape": "ResourceNotFoundException" },
5658
{ "shape": "InternalServerException" },
5759
{ "shape": "ValidationException" },
5860
{ "shape": "AccessDeniedException" }
@@ -116,6 +118,8 @@
116118
"output": { "shape": "GetTaskAssistCodeGenerationResponse" },
117119
"errors": [
118120
{ "shape": "ThrottlingException" },
121+
{ "shape": "ConflictException" },
122+
{ "shape": "ResourceNotFoundException" },
119123
{ "shape": "InternalServerException" },
120124
{ "shape": "ValidationException" },
121125
{ "shape": "AccessDeniedException" }
@@ -241,6 +245,8 @@
241245
"output": { "shape": "StartTaskAssistCodeGenerationResponse" },
242246
"errors": [
243247
{ "shape": "ThrottlingException" },
248+
{ "shape": "ConflictException" },
249+
{ "shape": "ResourceNotFoundException" },
244250
{ "shape": "InternalServerException" },
245251
{ "shape": "ValidationException" },
246252
{ "shape": "AccessDeniedException" }
@@ -474,6 +480,7 @@
474480
"contentMd5": { "shape": "CreateUploadUrlRequestContentMd5String" },
475481
"contentChecksum": { "shape": "CreateUploadUrlRequestContentChecksumString" },
476482
"contentChecksumType": { "shape": "ContentChecksumType" },
483+
"contentLength": { "shape": "CreateUploadUrlRequestContentLengthLong" },
477484
"artifactType": { "shape": "ArtifactType" },
478485
"uploadIntent": { "shape": "UploadIntent" },
479486
"uploadContext": { "shape": "UploadContext" }
@@ -485,6 +492,11 @@
485492
"min": 1,
486493
"sensitive": true
487494
},
495+
"CreateUploadUrlRequestContentLengthLong": {
496+
"type": "long",
497+
"box": true,
498+
"min": 1
499+
},
488500
"CreateUploadUrlRequestContentMd5String": {
489501
"type": "string",
490502
"max": 128,
@@ -521,7 +533,7 @@
521533
"type": "string",
522534
"max": 950,
523535
"min": 0,
524-
"pattern": "$|^arn:[-.a-z0-9]{1,63}:codewhisperer:([-.a-z0-9]{0,63}:){2}([a-zA-Z0-9-_:/]){1,1023}"
536+
"pattern": "arn:[-.a-z0-9]{1,63}:codewhisperer:([-.a-z0-9]{0,63}:){2}([a-zA-Z0-9-_:/]){1,1023}"
525537
},
526538
"CustomizationName": {
527539
"type": "string",
@@ -955,7 +967,8 @@
955967
"PreSignedUrl": {
956968
"type": "string",
957969
"max": 2048,
958-
"min": 1
970+
"min": 1,
971+
"sensitive": true
959972
},
960973
"PrimitiveInteger": { "type": "integer" },
961974
"ProgrammingLanguage": {
@@ -1442,7 +1455,8 @@
14421455
"members": {
14431456
"ideCategory": { "shape": "IdeCategory" },
14441457
"operatingSystem": { "shape": "OperatingSystem" },
1445-
"product": { "shape": "UserContextProductString" }
1458+
"product": { "shape": "UserContextProductString" },
1459+
"clientId": { "shape": "UUID" }
14461460
}
14471461
},
14481462
"UserContextProductString": {
@@ -1518,7 +1532,8 @@
15181532
"recommendationLatencyMilliseconds": { "shape": "Double" },
15191533
"timestamp": { "shape": "Timestamp" },
15201534
"suggestionReferenceCount": { "shape": "PrimitiveInteger" },
1521-
"generatedLine": { "shape": "PrimitiveInteger" }
1535+
"generatedLine": { "shape": "PrimitiveInteger" },
1536+
"numberOfRecommendations": { "shape": "PrimitiveInteger" }
15221537
}
15231538
},
15241539
"ValidationException": {

src/codewhisperer/commands/basicCommands.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { FileSystemCommon } from '../../srcShared/fs'
3232
import { Mutable } from '../../shared/utilities/tsUtils'
3333
import { CodeWhispererSource } from './types'
3434
import { showManageConnections } from '../../auth/ui/vue/show'
35+
import { FeatureConfigProvider } from '../service/featureConfigProvider'
3536

3637
export const toggleCodeSuggestions = Commands.declare(
3738
{ id: 'aws.codeWhisperer.toggleCodeSuggestion', compositeKey: { 1: 'source' } },
@@ -246,6 +247,13 @@ export const notifyNewCustomizationsCmd = Commands.declare(
246247
}
247248
)
248249

250+
export const fetchFeatureConfigsCmd = Commands.declare(
251+
{ id: 'aws.codeWhisperer.fetchFeatureConfigs', logging: false },
252+
() => () => {
253+
FeatureConfigProvider.instance.fetchFeatureConfigs()
254+
}
255+
)
256+
249257
export const applySecurityFix = Commands.declare(
250258
'aws.codeWhisperer.applySecurityFix',
251259
() => async (issue: CodeScanIssue, filePath: string, source: Component) => {

src/codewhisperer/service/completionProvider.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import * as CodeWhispererConstants from '../models/constants'
88
import { runtimeLanguageContext } from '../util/runtimeLanguageContext'
99
import { Recommendation } from '../client/codewhisperer'
1010
import { LicenseUtil } from '../util/licenseUtil'
11-
import { TelemetryHelper } from '../util/telemetryHelper'
1211
import { RecommendationHandler } from './recommendationHandler'
1312
import { session } from '../util/codeWhispererSession'
1413
/**
@@ -60,7 +59,7 @@ export function getCompletionItem(
6059
recommendation,
6160
RecommendationHandler.instance.requestId,
6261
session.sessionId,
63-
TelemetryHelper.instance.triggerType,
62+
session.triggerType,
6463
session.getCompletionType(recommendationIndex),
6564
languageContext.language,
6665
references,
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { FeatureValue } from '../client/codewhispereruserclient'
7+
import { codeWhispererClient as client } from '../client/codewhisperer'
8+
import { AuthUtil } from '../util/authUtil'
9+
import { getLogger } from '../../shared/logger'
10+
11+
export class FeatureContext {
12+
constructor(public name: string, public variation: string, public value: FeatureValue) {}
13+
}
14+
15+
const testFeatureName = 'testFeature'
16+
const featureConfigPollIntervalInMs = 30 * 60 * 1000 // 30 mins
17+
18+
// TODO: add real feature later
19+
export const featureDefinitions = new Map([
20+
[testFeatureName, new FeatureContext(testFeatureName, 'CONTROL', { stringValue: 'testValue' })],
21+
])
22+
23+
export class FeatureConfigProvider {
24+
private featureConfigs = new Map<string, FeatureContext>()
25+
26+
static #instance: FeatureConfigProvider
27+
28+
constructor() {
29+
this.fetchFeatureConfigs()
30+
31+
setInterval(this.fetchFeatureConfigs.bind(this), featureConfigPollIntervalInMs)
32+
}
33+
34+
public static get instance() {
35+
return (this.#instance ??= new this())
36+
}
37+
38+
async fetchFeatureConfigs(): Promise<void> {
39+
if (AuthUtil.instance.isConnectionExpired()) {
40+
return
41+
}
42+
43+
getLogger().debug('CodeWhisperer: Fetching feature configs')
44+
try {
45+
const response = await client.listFeatureEvaluations()
46+
47+
// Overwrite feature configs from server response
48+
response.featureEvaluations.forEach(evaluation => {
49+
this.featureConfigs.set(
50+
evaluation.feature,
51+
new FeatureContext(evaluation.feature, evaluation.variation, evaluation.value)
52+
)
53+
})
54+
} catch (e) {
55+
getLogger().debug('CodeWhisperer: Error when fetching feature configs', e)
56+
}
57+
getLogger().debug(`CodeWhisperer: Current feature configs: ${this.getFeatureConfigsTelemetry()}`)
58+
}
59+
60+
// Sample format: "{testFeature: CONTROL}""
61+
getFeatureConfigsTelemetry(): string {
62+
return `{${Array.from(this.featureConfigs.entries())
63+
.map(([name, context]) => `${name}: ${context.variation}`)
64+
.join(', ')}}`
65+
}
66+
67+
// TODO: for all feature variations, define a contract that can be enforced upon the implementation of
68+
// the business logic.
69+
// When we align on a new feature config, client-side will implement specific business logic to utilize
70+
// these values by:
71+
// 1) Add an entry in featureDefinitions, which is <feature_name> to <feature_context>.
72+
// 2) Add a function with name `getXXX`, where XXX refers to the feature name.
73+
// 3) Specify the return type: One of the return type string/boolean/Long/Double should be used here.
74+
// 4) Specify the key for the `getFeatureValueForKey` helper function which is the feature name.
75+
// 5) Specify the corresponding type value getter for the `FeatureValue` class. For example,
76+
// if the return type is Long, then the corresponding type value getter is `longValue()`.
77+
// 6) Add a test case for this feature.
78+
// 7) In case `getXXX()` returns undefined, it should be treated as a default/control group.
79+
getTestFeature(): string | undefined {
80+
return this.getFeatureValueForKey(testFeatureName).stringValue
81+
}
82+
83+
// Get the feature value for the given key.
84+
// In case of a misconfiguration, it will return a default feature value of Boolean true.
85+
private getFeatureValueForKey(name: string): FeatureValue {
86+
return this.featureConfigs.get(name)?.value ?? featureDefinitions.get(name)?.value ?? { boolValue: true }
87+
}
88+
}

src/codewhisperer/service/inlineCompletionItemProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export class CWInlineCompletionItemProvider implements vscode.InlineCompletionIt
114114
truncatedSuggestion,
115115
this.requestId,
116116
session.sessionId,
117-
TelemetryHelper.instance.triggerType,
117+
session.triggerType,
118118
session.getCompletionType(index),
119119
runtimeLanguageContext.getLanguageContext(document.languageId).language,
120120
r.references,

src/codewhisperer/service/recommendationHandler.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const lock = new AsyncLock({ maxPending: 1 })
6767

6868
export class RecommendationHandler {
6969
public lastInvocationTime: number
70+
// TODO: remove this requestId
7071
public requestId: string
7172
private nextToken: string
7273
private cancellationToken: vscode.CancellationTokenSource
@@ -192,9 +193,9 @@ export class RecommendationHandler {
192193
} else {
193194
session.requestContext = {
194195
request: {
195-
fileContext: session.requestContext.request.fileContext,
196+
...session.requestContext.request,
197+
// Putting nextToken assignment in the end so it overwrites the existing nextToken
196198
nextToken: this.nextToken,
197-
supplementalContexts: session.requestContext.request.supplementalContexts,
198199
},
199200
supplementalMetadata: session.requestContext.supplementalMetadata,
200201
}
@@ -207,7 +208,10 @@ export class RecommendationHandler {
207208
// set start pos for non pagination call or first pagination call
208209
if (!pagination || (pagination && page === 0)) {
209210
session.startPos = editor.selection.active
211+
session.startCursorOffset = editor.document.offsetAt(session.startPos)
210212
session.leftContextOfCurrentLine = EditorContext.getLeftContext(editor, session.startPos.line)
213+
session.triggerType = triggerType
214+
session.autoTriggerType = autoTriggerType
211215

212216
/**
213217
* Validate request
@@ -248,7 +252,6 @@ export class RecommendationHandler {
248252
recommendations = (resp && resp.completions) || []
249253
}
250254
invocationResult = 'Succeeded'
251-
TelemetryHelper.instance.triggerType = triggerType
252255
requestId = resp?.$response && resp?.$response?.requestId
253256
nextToken = resp?.nextToken ? resp?.nextToken : ''
254257
sessionId = resp?.$response?.httpResponse?.headers['x-amzn-sessionid']
@@ -343,11 +346,8 @@ export class RecommendationHandler {
343346
requestId,
344347
sessionId,
345348
session.recommendations.length + recommendations.length - 1,
346-
triggerType,
347-
autoTriggerType,
348349
invocationResult,
349350
latency,
350-
session.startPos.line,
351351
session.language,
352352
session.taskType,
353353
reason,
@@ -690,7 +690,7 @@ export class RecommendationHandler {
690690
telemetry.codewhisperer_perceivedLatency.emit({
691691
codewhispererRequestId: this.requestId,
692692
codewhispererSessionId: session.sessionId,
693-
codewhispererTriggerType: TelemetryHelper.instance.triggerType,
693+
codewhispererTriggerType: session.triggerType,
694694
codewhispererCompletionType: session.getCompletionType(0),
695695
codewhispererLanguage: languageContext.language,
696696
duration: performance.now() - this.lastInvocationTime,

0 commit comments

Comments
 (0)