Skip to content

Commit 940d76e

Browse files
authored
telemetry(amazonq): didSelectProfile
## Problem when calling `switchProfile`, emit didSelectProfile with corresponding user intent 1. user -> when user click on menu `switch profile` action 2. auth -> when user log in and prompted to profile selection page 3. plugin -> when switchProfile is invoked by plugin (us) on users' behalf (this will happen when users start a new IDE instance and we want to auto-switch user's selection in other IDE instance etc)
1 parent 0d7ec02 commit 940d76e

File tree

9 files changed

+154
-55
lines changed

9 files changed

+154
-55
lines changed

packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,13 @@ describe('RegionProfileManager', function () {
9393
describe('switch and get profile', function () {
9494
it('should switch if connection is IdC', async function () {
9595
await setupConnection('idc')
96-
await sut.switchRegionProfile(profileFoo)
96+
await sut.switchRegionProfile(profileFoo, 'user')
9797
assert.deepStrictEqual(sut.activeRegionProfile, profileFoo)
9898
})
9999

100100
it('should do nothing and return undefined if connection is builder id', async function () {
101101
await setupConnection('builderId')
102-
await sut.switchRegionProfile(profileFoo)
102+
await sut.switchRegionProfile(profileFoo, 'user')
103103
assert.deepStrictEqual(sut.activeRegionProfile, undefined)
104104
})
105105
})
@@ -115,7 +115,7 @@ describe('RegionProfileManager', function () {
115115

116116
it(`builder id should always use default profile IAD`, async function () {
117117
await setupConnection('builderId')
118-
await sut.switchRegionProfile(profileFoo)
118+
await sut.switchRegionProfile(profileFoo, 'user')
119119
assert.deepStrictEqual(sut.activeRegionProfile, undefined)
120120
const conn = authUtil.conn
121121
if (!conn) {
@@ -127,12 +127,15 @@ describe('RegionProfileManager', function () {
127127

128128
it(`idc should return correct endpoint corresponding to profile region`, async function () {
129129
await setupConnection('idc')
130-
await sut.switchRegionProfile({
131-
name: 'foo',
132-
region: 'eu-central-1',
133-
arn: 'foo arn',
134-
description: 'foo description',
135-
})
130+
await sut.switchRegionProfile(
131+
{
132+
name: 'foo',
133+
region: 'eu-central-1',
134+
arn: 'foo arn',
135+
description: 'foo description',
136+
},
137+
'user'
138+
)
136139
assert.ok(sut.activeRegionProfile)
137140
assert.deepStrictEqual(sut.clientConfig, {
138141
region: 'eu-central-1',
@@ -142,12 +145,15 @@ describe('RegionProfileManager', function () {
142145

143146
it(`idc should throw if corresponding endpoint is not defined`, async function () {
144147
await setupConnection('idc')
145-
await sut.switchRegionProfile({
146-
name: 'foo',
147-
region: 'unknown region',
148-
arn: 'foo arn',
149-
description: 'foo description',
150-
})
148+
await sut.switchRegionProfile(
149+
{
150+
name: 'foo',
151+
region: 'unknown region',
152+
arn: 'foo arn',
153+
description: 'foo description',
154+
},
155+
'user'
156+
)
151157

152158
assert.throws(() => {
153159
sut.clientConfig
@@ -158,7 +164,7 @@ describe('RegionProfileManager', function () {
158164
describe('persistence', function () {
159165
it('persistSelectedRegionProfile', async function () {
160166
await setupConnection('idc')
161-
await sut.switchRegionProfile(profileFoo)
167+
await sut.switchRegionProfile(profileFoo, 'user')
162168
assert.deepStrictEqual(sut.activeRegionProfile, profileFoo)
163169
const conn = authUtil.conn
164170
if (!conn) {
@@ -199,7 +205,7 @@ describe('RegionProfileManager', function () {
199205
it('should reset activeProfile and global state', async function () {
200206
// setup
201207
await setupConnection('idc')
202-
await sut.switchRegionProfile(profileFoo)
208+
await sut.switchRegionProfile(profileFoo, 'user')
203209
assert.deepStrictEqual(sut.activeRegionProfile, profileFoo)
204210
const conn = authUtil.conn
205211
if (!conn) {
@@ -230,7 +236,7 @@ describe('RegionProfileManager', function () {
230236
describe('createQClient', function () {
231237
it(`should configure the endpoint and region correspondingly`, async function () {
232238
await setupConnection('idc')
233-
await sut.switchRegionProfile(profileFoo)
239+
await sut.switchRegionProfile(profileFoo, 'user')
234240
assert.deepStrictEqual(sut.activeRegionProfile, profileFoo)
235241
const conn = authUtil.conn as SsoConnection
236242

packages/core/src/amazonq/webview/generators/webViewContent.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,7 @@ export class WebViewContentGenerator {
8989
// 1. profile count >= 2
9090
// 2. not default (fallback) which has empty arn
9191
let regionProfile: RegionProfile | undefined = AuthUtil.instance.regionProfileManager.activeRegionProfile
92-
if (
93-
(regionProfile && AuthUtil.instance.regionProfileManager.isDefault(regionProfile)) ||
94-
AuthUtil.instance.regionProfileManager.profiles.length === 1
95-
) {
92+
if (AuthUtil.instance.regionProfileManager.profiles.length === 1) {
9693
regionProfile = undefined
9794
}
9895

packages/core/src/codewhisperer/region/regionProfileManager.ts

Lines changed: 68 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,9 @@ import { getLogger } from '../../shared/logger/logger'
2626
import { pageableToCollection } from '../../shared/utilities/collectionUtils'
2727
import { parse } from '@aws-sdk/util-arn-parser'
2828
import { isAwsError, ToolkitError } from '../../shared/errors'
29+
import { telemetry } from '../../shared/telemetry/telemetry'
2930
import { localize } from '../../shared/utilities/vsCodeUtils'
3031

31-
const defaultProfile: RegionProfile = {
32-
name: 'default',
33-
region: 'us-east-1',
34-
arn: '',
35-
description: 'defaultProfile when listAvailableProfiles fails',
36-
}
37-
3832
// TODO: is there a better way to manage all endpoint strings in one place?
3933
export const defaultServiceConfig: CodeWhispererConfig = {
4034
region: 'us-east-1',
@@ -48,6 +42,14 @@ const endpoints = createConstantMap({
4842
'eu-central-1': 'https://rts.prod-eu-central-1.codewhisperer.ai.aws.dev/',
4943
})
5044

45+
/**
46+
* 'user' -> users change the profile through Q menu
47+
* 'auth' -> users change the profile through webview profile selector page
48+
* 'update' -> plugin auto select the profile on users' behalf as there is only 1 profile
49+
* 'reload' -> on plugin restart, plugin will try to reload previous selected profile
50+
*/
51+
export type ProfileSwitchIntent = 'user' | 'auth' | 'update' | 'reload'
52+
5153
export class RegionProfileManager {
5254
private static logger = getLogger()
5355
private _activeRegionProfile: RegionProfile | undefined
@@ -103,6 +105,8 @@ export class RegionProfileManager {
103105
constructor(private readonly connectionProvider: () => Connection | undefined) {}
104106

105107
async listRegionProfile(): Promise<RegionProfile[]> {
108+
this._profiles = []
109+
106110
const conn = this.connectionProvider()
107111
if (conn === undefined || !isSsoConnection(conn)) {
108112
return []
@@ -145,7 +149,7 @@ export class RegionProfileManager {
145149
return availableProfiles
146150
}
147151

148-
async switchRegionProfile(regionProfile: RegionProfile | undefined) {
152+
async switchRegionProfile(regionProfile: RegionProfile | undefined, source: ProfileSwitchIntent) {
149153
const conn = this.connectionProvider()
150154
if (conn === undefined || !isIdcSsoConnection(conn)) {
151155
return
@@ -155,6 +159,9 @@ export class RegionProfileManager {
155159
return
156160
}
157161

162+
// TODO: make it typesafe
163+
const ssoConn = this.connectionProvider() as SsoConnection
164+
158165
// only prompt to users when users switch from A profile to B profile
159166
if (this.activeRegionProfile !== undefined && regionProfile !== undefined) {
160167
const response = await showConfirmationMessage({
@@ -169,10 +176,35 @@ export class RegionProfileManager {
169176
})
170177

171178
if (!response) {
179+
telemetry.amazonq_didSelectProfile.emit({
180+
source: source,
181+
amazonQProfileRegion: this.activeRegionProfile?.region ?? 'not-set',
182+
ssoRegion: ssoConn.ssoRegion,
183+
result: 'Cancelled',
184+
credentialStartUrl: ssoConn.startUrl,
185+
profileCount: this.profiles.length,
186+
})
172187
return
173188
}
174189
}
175190

191+
if (source === 'reload' || source === 'update') {
192+
telemetry.amazonq_profileState.emit({
193+
source: source,
194+
amazonQProfileRegion: regionProfile?.region ?? 'not-set',
195+
result: 'Succeeded',
196+
})
197+
} else {
198+
telemetry.amazonq_didSelectProfile.emit({
199+
source: source,
200+
amazonQProfileRegion: regionProfile?.region ?? 'not-set',
201+
ssoRegion: ssoConn.ssoRegion,
202+
result: 'Succeeded',
203+
credentialStartUrl: ssoConn.startUrl,
204+
profileCount: this.profiles.length,
205+
})
206+
}
207+
176208
await this._switchRegionProfile(regionProfile)
177209
}
178210

@@ -181,7 +213,7 @@ export class RegionProfileManager {
181213

182214
this._onDidChangeRegionProfile.fire(regionProfile)
183215
// dont show if it's a default (fallback)
184-
if (regionProfile && !this.isDefault(regionProfile) && this.profiles.length > 1) {
216+
if (regionProfile && this.profiles.length > 1) {
185217
void vscode.window.showInformationMessage(`You are using the ${regionProfile.name} profile for Q.`).then()
186218
}
187219

@@ -203,10 +235,32 @@ export class RegionProfileManager {
203235
return
204236
}
205237
// cross-validation
206-
const profiles = this.listRegionProfile()
207-
const r = (await profiles).find((it) => it.arn === previousSelected)
238+
let profiles: RegionProfile[] = []
239+
try {
240+
profiles = await this.listRegionProfile()
241+
} catch (e) {
242+
telemetry.amazonq_profileState.emit({
243+
source: 'reload',
244+
amazonQProfileRegion: 'not-set',
245+
reason: (e as Error).message,
246+
result: 'Failed',
247+
})
208248

209-
await this.switchRegionProfile(r)
249+
return
250+
}
251+
252+
const r = profiles.find((it) => it.arn === previousSelected)
253+
if (!r) {
254+
telemetry.amazonq_profileState.emit({
255+
source: 'reload',
256+
amazonQProfileRegion: 'not-set',
257+
reason: 'profile could not be selected',
258+
result: 'Failed',
259+
})
260+
return
261+
}
262+
263+
await this.switchRegionProfile(r, 'reload')
210264
}
211265

212266
private loadPersistedRegionProfle(): { [label: string]: string } {
@@ -223,7 +277,7 @@ export class RegionProfileManager {
223277
const conn = this.connectionProvider()
224278

225279
// default has empty arn and shouldn't be persisted because it's just a fallback
226-
if (!conn || this.activeRegionProfile === undefined || this.isDefault(this.activeRegionProfile)) {
280+
if (!conn || this.activeRegionProfile === undefined) {
227281
return
228282
}
229283

@@ -238,14 +292,6 @@ export class RegionProfileManager {
238292
await globals.globalState.update('aws.amazonq.regionProfiles', previousPersistedState)
239293
}
240294

241-
isDefault(profile: RegionProfile): boolean {
242-
return (
243-
profile.arn === defaultProfile.arn &&
244-
profile.name === defaultProfile.name &&
245-
profile.region === defaultProfile.region
246-
)
247-
}
248-
249295
async generateQuickPickItem(): Promise<DataQuickPickItem<string>[]> {
250296
const selected = this.activeRegionProfile
251297
let profiles: RegionProfile[] = []
@@ -264,7 +310,7 @@ export class RegionProfileManager {
264310
const quickPickItems: DataQuickPickItem<string>[] = profiles.map((it) => {
265311
const label = it.name
266312
const onClick = async () => {
267-
await this.switchRegionProfile(it)
313+
await this.switchRegionProfile(it, 'user')
268314
}
269315
const data = it.arn
270316
const description = it.region

packages/core/src/codewhisperer/ui/codeWhispererNodes.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,7 @@ export function createSelectCustomization(): DataQuickPickItem<'selectCustomizat
139139
}
140140

141141
export function createSelectRegionProfileNode(): DataQuickPickItem<'selectRegionProfile'> {
142-
let selectedRegionProfile = AuthUtil.instance.regionProfileManager.activeRegionProfile
143-
// default shouldn't be shown as it's saying ListAvailableProfiles fail and we fallback to IAD
144-
if (selectedRegionProfile && AuthUtil.instance.regionProfileManager.isDefault(selectedRegionProfile)) {
145-
selectedRegionProfile = undefined
146-
}
142+
const selectedRegionProfile = AuthUtil.instance.regionProfileManager.activeRegionProfile
147143

148144
const label = 'Change Profile'
149145
const icon = getIcon('vscode-arrow-swap')

packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import { ToolkitError } from '../../../../shared/errors'
2323
import { withTelemetryContext } from '../../../../shared/telemetry/util'
2424
import { builderIdStartUrl } from '../../../../auth/sso/constants'
2525
import { RegionProfile } from '../../../../codewhisperer/models/model'
26+
import { telemetry } from '../../../../shared/telemetry/telemetry'
27+
import { ProfileSwitchIntent } from '../../../../codewhisperer/region/regionProfileManager'
2628

2729
const className = 'AmazonQLoginWebview'
2830
export class AmazonQLoginWebview extends CommonAuthWebview {
@@ -214,12 +216,22 @@ export class AmazonQLoginWebview extends CommonAuthWebview {
214216
try {
215217
return await AuthUtil.instance.regionProfileManager.listRegionProfile()
216218
} catch (e) {
219+
const conn = AuthUtil.instance.conn as SsoConnection | undefined
220+
telemetry.amazonq_didSelectProfile.emit({
221+
source: 'auth',
222+
amazonQProfileRegion: AuthUtil.instance.regionProfileManager.activeRegionProfile?.region ?? 'not-set',
223+
ssoRegion: conn?.ssoRegion,
224+
result: 'Failed',
225+
credentialStartUrl: conn?.startUrl,
226+
reason: (e as Error).message,
227+
})
228+
217229
return (e as Error).message
218230
}
219231
}
220232

221-
override selectRegionProfile(profile: RegionProfile) {
222-
return AuthUtil.instance.regionProfileManager.switchRegionProfile(profile)
233+
override selectRegionProfile(profile: RegionProfile, source: ProfileSwitchIntent) {
234+
return AuthUtil.instance.regionProfileManager.switchRegionProfile(profile, source)
223235
}
224236

225237
private setupConnectionEventEmitter(): void {

packages/core/src/login/webview/vue/backend.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { AuthSSOServer } from '../../../auth/sso/server'
3333
import { getLogger } from '../../../shared/logger/logger'
3434
import { isValidUrl } from '../../../shared/utilities/uriUtils'
3535
import { RegionProfile } from '../../../codewhisperer/models/model'
36+
import { ProfileSwitchIntent } from '../../../codewhisperer/region/regionProfileManager'
3637

3738
export abstract class CommonAuthWebview extends VueWebview {
3839
private readonly className = 'CommonAuthWebview'
@@ -209,7 +210,7 @@ export abstract class CommonAuthWebview extends VueWebview {
209210

210211
abstract listRegionProfiles(): Promise<RegionProfile[] | string>
211212

212-
abstract selectRegionProfile(profile: RegionProfile): Promise<void>
213+
abstract selectRegionProfile(profile: RegionProfile, source: ProfileSwitchIntent): Promise<void>
213214

214215
/**
215216
* Emit stored metric metadata. Does not reset the stored metric metadata, because it

packages/core/src/login/webview/vue/regionProfileSelector.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export default defineComponent({
138138
onClickContinue() {
139139
if (this.availableRegionProfiles[this.selectedRegionProfileIndex] !== undefined) {
140140
const selectedProfile = this.availableRegionProfiles[this.selectedRegionProfileIndex]
141-
client.selectRegionProfile(selectedProfile)
141+
client.selectRegionProfile(selectedProfile, 'auth')
142142
} else {
143143
// TODO: handle error
144144
}
@@ -156,7 +156,7 @@ export default defineComponent({
156156
this.availableRegionProfiles = r
157157
// auto select and bypass this profile view if profile count === 1
158158
if (this.availableRegionProfiles.length === 1) {
159-
await client.selectRegionProfile(this.availableRegionProfiles[0])
159+
await client.selectRegionProfile(this.availableRegionProfiles[0], 'update')
160160
}
161161
}
162162
},

packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { AuthError, AuthFlowState } from '../types'
2222
import { setContext } from '../../../../shared/vscode/setContext'
2323
import { builderIdStartUrl } from '../../../../auth/sso/constants'
2424
import { RegionProfile } from '../../../../codewhisperer/models/model'
25+
import { ProfileSwitchIntent } from '../../../../codewhisperer/region/regionProfileManager'
2526

2627
export class ToolkitLoginWebview extends CommonAuthWebview {
2728
public override id: string = 'aws.toolkit.AmazonCommonAuth'
@@ -182,7 +183,7 @@ export class ToolkitLoginWebview extends CommonAuthWebview {
182183
throw new Error('Method not implemented')
183184
}
184185

185-
override selectRegionProfile(profile: RegionProfile): Promise<void> {
186+
override selectRegionProfile(profile: RegionProfile, source: ProfileSwitchIntent): Promise<void> {
186187
throw new Error('Method not implemented')
187188
}
188189
}

0 commit comments

Comments
 (0)