Skip to content

Commit f6a8141

Browse files
authored
telemetry(amazonq): didSelectProfile (#2001)
amazonq_didSelectProfile All IDEs must emit the "amazonq_didSelectProfile" event under the following conditions: Immediately after a user has successfully logged in. [Auth flow] When a user explicitly changes their profile. [Modal Dialog flow] amazonq_profileState IDEs will auto select or show failure to select profile for users who have already authenticated. We want to cover profile state for this users. Hence this metric amazonq_profileState is emitted on load/reload/update/restart of after with auth_userState. ide startup, plugins will reload previous selected profile and validate it auto-select if users only have 1 profile
1 parent 28efe39 commit f6a8141

File tree

10 files changed

+242
-31
lines changed

10 files changed

+242
-31
lines changed

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QLoginWebview.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import software.aws.toolkits.jetbrains.core.webview.BrowserState
3434
import software.aws.toolkits.jetbrains.core.webview.LoginBrowser
3535
import software.aws.toolkits.jetbrains.core.webview.WebviewResourceHandlerFactory
3636
import software.aws.toolkits.jetbrains.isDeveloperMode
37+
import software.aws.toolkits.jetbrains.services.amazonq.profile.QProfileSwitchIntent
3738
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile
3839
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
3940
import software.aws.toolkits.jetbrains.services.amazonq.util.createBrowser
@@ -212,7 +213,7 @@ class QWebviewBrowser(val project: Project, private val parentDisposable: Dispos
212213
QRegionProfileManager.getInstance().switchProfile(
213214
project,
214215
QRegionProfile(profileName = message.profileName, arn = message.arn),
215-
passive = false
216+
intent = QProfileSwitchIntent.Auth
216217
)
217218
}
218219
}

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/startup/AmazonQStartupActivity.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
1919
import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
2020
import software.aws.toolkits.jetbrains.core.gettingstarted.emitUserState
2121
import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService
22+
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
2223
import software.aws.toolkits.jetbrains.services.amazonq.project.ProjectContextController
2324
import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindow
2425
import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindowFactory
@@ -52,6 +53,9 @@ class AmazonQStartupActivity : ProjectActivity {
5253
CodeWhispererExplorerActionManager.getInstance().setIsFirstRestartAfterQInstall(false)
5354
}
5455
}
56+
57+
QRegionProfileManager.getInstance().validateProfile(project)
58+
5559
startLsp(project)
5660
if (runOnce.get()) return
5761
emitUserState(project)

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindowFactory.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import com.intellij.util.ui.components.BorderLayoutPanel
1515
import software.aws.toolkits.core.utils.debug
1616
import software.aws.toolkits.core.utils.getLogger
1717
import software.aws.toolkits.core.utils.warn
18+
import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection
1819
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection
1920
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
2021
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManagerListener
@@ -27,6 +28,7 @@ import software.aws.toolkits.jetbrains.core.webview.BrowserState
2728
import software.aws.toolkits.jetbrains.services.amazonq.QWebviewPanel
2829
import software.aws.toolkits.jetbrains.services.amazonq.RefreshQChatPanelButtonPressedListener
2930
import software.aws.toolkits.jetbrains.services.amazonq.gettingstarted.openMeetQPage
31+
import software.aws.toolkits.jetbrains.services.amazonq.profile.QProfileSwitchIntent
3032
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile
3133
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
3234
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileSelectedListener
@@ -35,6 +37,8 @@ import software.aws.toolkits.jetbrains.utils.isQExpired
3537
import software.aws.toolkits.jetbrains.utils.isQWebviewsAvailable
3638
import software.aws.toolkits.resources.message
3739
import software.aws.toolkits.telemetry.FeatureId
40+
import software.aws.toolkits.telemetry.MetricResult
41+
import software.aws.toolkits.telemetry.Telemetry
3842
import java.awt.event.ComponentAdapter
3943
import java.awt.event.ComponentEvent
4044

@@ -70,6 +74,14 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
7074
QRegionProfileManager.getInstance().listRegionProfiles(project)
7175
} catch (e: Exception) {
7276
LOG.warn { "Failed to call listRegionProfiles API" }
77+
Telemetry.amazonq.didSelectProfile.use { span ->
78+
span.source(QProfileSwitchIntent.Auth.value)
79+
.amazonQProfileRegion(QRegionProfileManager.getInstance().activeProfile(project)?.region ?: "not-set")
80+
.ssoRegion((qConn as? AwsBearerTokenConnection)?.region)
81+
.credentialStartUrl((qConn as? AwsBearerTokenConnection)?.startUrl)
82+
.result(MetricResult.Failed)
83+
.reason(e.message)
84+
}
7385
}
7486
}
7587
prepareChatContent(project, qPanel)
@@ -100,7 +112,7 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
100112
project.messageBus.connect(toolWindow.disposable).subscribe(
101113
QRegionProfileSelectedListener.TOPIC,
102114
object : QRegionProfileSelectedListener {
103-
override fun onProfileSelected(project: Project, profile: QRegionProfile) {
115+
override fun onProfileSelected(project: Project, profile: QRegionProfile?) {
104116
if (project.isDisposed) return
105117
AmazonQToolWindow.getInstance(project).disposeAndRecreate()
106118
prepareChatContent(project, qPanel)

plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/QRegionProfileManagerTest.kt

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,14 @@ import software.aws.toolkits.jetbrains.core.credentials.sono.Q_SCOPES
3636
import software.aws.toolkits.jetbrains.core.region.MockRegionProviderRule
3737
import software.aws.toolkits.jetbrains.services.amazonq.profile.QEndpoints
3838
import software.aws.toolkits.jetbrains.services.amazonq.profile.QProfileState
39+
import software.aws.toolkits.jetbrains.services.amazonq.profile.QProfileSwitchIntent
3940
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile
4041
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
4142
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileSelectedListener
4243
import software.aws.toolkits.jetbrains.utils.xmlElement
4344
import java.net.URI
4445
import java.util.function.Consumer
46+
import kotlin.test.fail
4547

4648
// TODO: should use junit5
4749
class QRegionProfileManagerTest {
@@ -81,16 +83,16 @@ class QRegionProfileManagerTest {
8183

8284
@Test
8385
fun `switchProfile should switch the current connection(project) to the selected profile`() {
84-
sut.switchProfile(project, QRegionProfile(arn = "arn", profileName = "foo_profile"), true)
86+
sut.switchProfile(project, QRegionProfile(arn = "arn", profileName = "foo_profile"), QProfileSwitchIntent.User)
8587
assertThat(sut.activeProfile(project)).isEqualTo(QRegionProfile(arn = "arn", profileName = "foo_profile"))
8688

87-
sut.switchProfile(project, QRegionProfile(arn = "another_arn", profileName = "bar_profile"), true)
89+
sut.switchProfile(project, QRegionProfile(arn = "another_arn", profileName = "bar_profile"), QProfileSwitchIntent.User)
8890
assertThat(sut.activeProfile(project)).isEqualTo(QRegionProfile(arn = "another_arn", profileName = "bar_profile"))
8991
}
9092

9193
@Test
9294
fun `switchProfile should return null if user is not connected`() {
93-
sut.switchProfile(project, QRegionProfile(arn = "arn", profileName = "foo_profile"), true)
95+
sut.switchProfile(project, QRegionProfile(arn = "arn", profileName = "foo_profile"), QProfileSwitchIntent.User)
9496
assertThat(sut.activeProfile(project)).isEqualTo(QRegionProfile(arn = "arn", profileName = "foo_profile"))
9597

9698
ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance())?.let {
@@ -109,16 +111,16 @@ class QRegionProfileManagerTest {
109111
project.messageBus.connect(disposableRule.disposable).subscribe(
110112
QRegionProfileSelectedListener.TOPIC,
111113
object : QRegionProfileSelectedListener {
112-
override fun onProfileSelected(project: Project, profile: QRegionProfile) {
114+
override fun onProfileSelected(project: Project, profile: QRegionProfile?) {
113115
cnt += 1
114116
}
115117
}
116118
)
117119

118120
assertThat(cnt).isEqualTo(0)
119-
sut.switchProfile(project, QRegionProfile(arn = "arn", profileName = "foo_profile"), false)
121+
sut.switchProfile(project, QRegionProfile(arn = "arn", profileName = "foo_profile"), QProfileSwitchIntent.Reload)
120122
assertThat(cnt).isEqualTo(1)
121-
sut.switchProfile(project, QRegionProfile(arn = "another_arn", profileName = "BAR_PROFILE"), false)
123+
sut.switchProfile(project, QRegionProfile(arn = "another_arn", profileName = "BAR_PROFILE"), QProfileSwitchIntent.Reload)
122124
assertThat(cnt).isEqualTo(2)
123125
}
124126

@@ -153,13 +155,46 @@ class QRegionProfileManagerTest {
153155
assertThat(r).contains(QRegionProfile("BAR", "bar"))
154156
}
155157

158+
@Test
159+
fun `validateProfile should cross validate selected profile with latest API response for current project and remove it if its not longer accessible`() {
160+
val client = clientRule.create<CodeWhispererRuntimeClient>()
161+
val mockResponse: SdkIterable<Profile> = SdkIterable<Profile> {
162+
listOf(
163+
Profile.builder().profileName("foo").arn("foo-arn-v2").build(),
164+
Profile.builder().profileName("bar").arn("bar-arn").build(),
165+
).toMutableList().iterator()
166+
}
167+
val iterable: ListAvailableProfilesIterable = mock {
168+
on { it.profiles() } doReturn mockResponse
169+
}
170+
client.stub {
171+
onGeneric { listAvailableProfilesPaginator(any<Consumer<ListAvailableProfilesRequest.Builder>>()) } doReturn iterable
172+
}
173+
174+
val activeConn =
175+
ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance()) ?: fail("connection shouldn't be null")
176+
val anotherConn = authRule.createConnection(ManagedSsoProfile(ssoRegion = "us-east-1", startUrl = "anotherUrl", scopes = Q_SCOPES))
177+
val fooProfile = QRegionProfile("foo", "foo-arn")
178+
val barProfile = QRegionProfile("bar", "bar-arn")
179+
val state = QProfileState().apply {
180+
this.connectionIdToActiveProfile[activeConn.id] = fooProfile
181+
this.connectionIdToActiveProfile[anotherConn.id] = barProfile
182+
}
183+
sut.loadState(state)
184+
assertThat(sut.activeProfile(project)).isEqualTo(fooProfile)
185+
186+
sut.validateProfile(project)
187+
assertThat(sut.activeProfile(project)).isNull()
188+
assertThat(sut.state.connectionIdToActiveProfile).isEqualTo(mapOf(anotherConn.id to barProfile))
189+
}
190+
156191
@Test
157192
fun `clientSettings should return the region Q profile specify`() {
158193
MockClientManager.useRealImplementations(disposableRule.disposable)
159194
sut.switchProfile(
160195
project,
161196
QRegionProfile(arn = "arn:aws:codewhisperer:eu-central-1:123456789012:profile/FOO_PROFILE", profileName = "FOO_PROFILE"),
162-
true
197+
QProfileSwitchIntent.User
163198
)
164199
assertThat(
165200
sut.activeProfile(project)
@@ -168,7 +203,11 @@ class QRegionProfileManagerTest {
168203
val settings = sut.getQClientSettings(project)
169204
assertThat(settings.region.id).isEqualTo(Region.EU_CENTRAL_1.id())
170205

171-
sut.switchProfile(project, QRegionProfile(arn = "arn:aws:codewhisperer:us-east-1:123456789012:profile/BAR_PROFILE", profileName = "BAR_PROFILE"), true)
206+
sut.switchProfile(
207+
project,
208+
QRegionProfile(arn = "arn:aws:codewhisperer:us-east-1:123456789012:profile/BAR_PROFILE", profileName = "BAR_PROFILE"),
209+
QProfileSwitchIntent.User
210+
)
172211
assertThat(
173212
sut.activeProfile(project)
174213
).isEqualTo(QRegionProfile(arn = "arn:aws:codewhisperer:us-east-1:123456789012:profile/BAR_PROFILE", profileName = "BAR_PROFILE"))
@@ -184,7 +223,7 @@ class QRegionProfileManagerTest {
184223
sut.switchProfile(
185224
project,
186225
QRegionProfile(arn = "arn:aws:codewhisperer:eu-central-1:123456789012:profile/FOO_PROFILE", profileName = "FOO_PROFILE"),
187-
true
226+
QProfileSwitchIntent.User
188227
)
189228
assertThat(
190229
sut.activeProfile(project)
@@ -198,7 +237,11 @@ class QRegionProfileManagerTest {
198237
client.serviceClientConfiguration().endpointOverride().get()
199238
).isEqualTo(URI.create(QEndpoints.getQEndpointWithRegion(Region.EU_CENTRAL_1.id())))
200239

201-
sut.switchProfile(project, QRegionProfile(arn = "arn:aws:codewhisperer:us-east-1:123456789012:profile/BAR_PROFILE", profileName = "BAR_PROFILE"), true)
240+
sut.switchProfile(
241+
project,
242+
QRegionProfile(arn = "arn:aws:codewhisperer:us-east-1:123456789012:profile/BAR_PROFILE", profileName = "BAR_PROFILE"),
243+
QProfileSwitchIntent.User
244+
)
202245
assertThat(
203246
sut.activeProfile(project)
204247
).isEqualTo(QRegionProfile(arn = "arn:aws:codewhisperer:us-east-1:123456789012:profile/BAR_PROFILE", profileName = "BAR_PROFILE"))

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/actions/QSwitchProfilesAction.kt

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,22 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
package software.aws.toolkits.jetbrains.services.amazonq.actions
5+
56
import com.intellij.icons.AllIcons
67
import com.intellij.openapi.actionSystem.ActionUpdateThread
78
import com.intellij.openapi.actionSystem.AnAction
89
import com.intellij.openapi.actionSystem.AnActionEvent
910
import com.intellij.openapi.application.ApplicationManager
1011
import com.intellij.openapi.project.DumbAware
12+
import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection
13+
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
14+
import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
15+
import software.aws.toolkits.jetbrains.services.amazonq.profile.QProfileSwitchIntent
1116
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileDialog
1217
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
1318
import software.aws.toolkits.resources.AmazonQBundle.message
19+
import software.aws.toolkits.telemetry.MetricResult
20+
import software.aws.toolkits.telemetry.Telemetry
1421

1522
class QSwitchProfilesAction : AnAction(message("action.q.switchProfiles.text")), DumbAware {
1623

@@ -19,10 +26,24 @@ class QSwitchProfilesAction : AnAction(message("action.q.switchProfiles.text")),
1926
override fun update(e: AnActionEvent) {
2027
e.presentation.icon = AllIcons.Actions.SwapPanels
2128
}
29+
2230
override fun actionPerformed(e: AnActionEvent) {
2331
val project = e.project ?: return
2432
ApplicationManager.getApplication().executeOnPooledThread {
25-
val profiles = QRegionProfileManager.getInstance().listRegionProfiles(project)
33+
val profiles = try {
34+
QRegionProfileManager.getInstance().listRegionProfiles(project)
35+
} catch (e: Exception) {
36+
val conn = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance()) as? AwsBearerTokenConnection
37+
Telemetry.amazonq.didSelectProfile.use { span ->
38+
span.source(QProfileSwitchIntent.User.value)
39+
.amazonQProfileRegion(QRegionProfileManager.getInstance().activeProfile(project)?.region ?: "not-set")
40+
.ssoRegion(conn?.region)
41+
.credentialStartUrl(conn?.startUrl)
42+
.result(MetricResult.Failed)
43+
.reason(e.message)
44+
}
45+
throw e
46+
}
2647
?: error("Attempted to fetch profiles while there does not exist")
2748
val selectedProfile = QRegionProfileManager.getInstance().activeProfile(project) ?: profiles[0]
2849
ApplicationManager.getApplication().invokeLater {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.services.amazonq.profile
5+
6+
/**
7+
* 'user' -> users change the profile through Q menu
8+
* 'auth' -> users change the profile through webview profile selector page
9+
* 'update' -> plugin auto select the profile on users' behalf as there is only 1 profile
10+
* 'reload' -> on plugin restart, plugin will try to reload previous selected profile
11+
*/
12+
enum class QProfileSwitchIntent(val value: String) {
13+
User("user"),
14+
Auth("auth"),
15+
Update("update"),
16+
Reload("reload"), ;
17+
18+
override fun toString() = value
19+
}

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QRegionProfileDialog.kt

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@ import com.intellij.openapi.ui.DialogWrapper
1010
import com.intellij.ui.dsl.builder.BottomGap
1111
import com.intellij.ui.dsl.builder.bind
1212
import com.intellij.ui.dsl.builder.panel
13+
import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection
14+
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
15+
import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
1316
import software.aws.toolkits.jetbrains.core.help.HelpIds
1417
import software.aws.toolkits.resources.AmazonQBundle.message
18+
import software.aws.toolkits.telemetry.MetricResult
19+
import software.aws.toolkits.telemetry.Telemetry
1520
import javax.swing.JComponent
1621

1722
class QRegionProfileDialog(
@@ -69,8 +74,24 @@ class QRegionProfileDialog(
6974
override fun doOKAction() {
7075
panel.apply()
7176
if (selectedOption != selectedProfile) {
72-
QRegionProfileManager.getInstance().switchProfile(project, selectedOption, passive = false)
77+
QRegionProfileManager.getInstance().switchProfile(project, selectedOption, intent = QProfileSwitchIntent.User)
7378
}
7479
close(OK_EXIT_CODE)
7580
}
81+
82+
override fun doCancelAction() {
83+
super.doCancelAction()
84+
val profileManager = QRegionProfileManager.getInstance()
85+
val conn = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance()) as? AwsBearerTokenConnection
86+
Telemetry.amazonq.didSelectProfile.use { span ->
87+
span.source(QProfileSwitchIntent.User.value)
88+
.amazonQProfileRegion(profileManager.activeProfile(project)?.region ?: "not-set")
89+
.profileCount(profiles.size)
90+
.ssoRegion(conn?.region)
91+
.credentialStartUrl(conn?.startUrl)
92+
.result(MetricResult.Cancelled)
93+
}
94+
95+
close(CANCEL_EXIT_CODE)
96+
}
7697
}

0 commit comments

Comments
 (0)