Skip to content

Commit

Permalink
feat(amazonq): initial standalone telemetry (aws#4719)
Browse files Browse the repository at this point in the history
Implemented telemetry:
-auth_userState
   -Describes the state of auth after activation.
- toolkit_showNotification
   -The user was shown a notification about Amazon Q being auto-installed or about the extension and if they'd like to install it.
- toolkit_invokeAction
   -The user installed, learned more, or dismissed the previously mentioned notification.
- ui_click
  -amazonq_switchToQSignIn
- sources on various codewhisperer commands

Other stuff:
- Collapse amazon q commands into one set (registered from amazon q)
- ExtensionUse.wasUpdated util func.

Follow up items:

Moving the definitions to common is a prioritized work item and will occur after this PR
Login page telemetry

commits:

* add initial telemetry definitions

* add notification telemetry

* add telemetry for opening login page

* collapse switch to Q commands

* fix close page telemetry, docstring, export telemetry from core

* add wasUpdated util function

* add auth_userState metric to amazonq

* update toolkit_invokeAction emissions

* reloaded -> reload

* add start up source telemetry enum

* rename dismiss command, remove from package.json

* add various fixes for existing telemtry

* fix tests

* resolve feedback

- ExtStartUpSources enum -> const
- fix compositeKey arg for _aws.toolkit.amazonq.dismiss
  • Loading branch information
hayemaxi authored Apr 18, 2024
1 parent 724affe commit b899e82
Show file tree
Hide file tree
Showing 22 changed files with 324 additions and 121 deletions.
34 changes: 32 additions & 2 deletions packages/amazonq/src/extensionShared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
shutdown as codewhispererShutdown,
amazonQDismissedKey,
refreshToolkitQState,
AuthUtil,
} from 'aws-core-vscode/codewhisperer'
import {
ExtContext,
Expand All @@ -25,13 +26,14 @@ import {
globals,
RegionProvider,
} from 'aws-core-vscode/shared'
import { initializeAuth, CredentialsStore, LoginManager, ExtensionUse } from 'aws-core-vscode/auth'
import { initializeAuth, CredentialsStore, LoginManager, AuthUtils } from 'aws-core-vscode/auth'
import { makeEndpointsProvider, registerCommands } from 'aws-core-vscode'
import { activate as activateCWChat } from 'aws-core-vscode/amazonq'
import { activate as activateQGumby } from 'aws-core-vscode/amazonqGumby'
import { CommonAuthViewProvider } from 'aws-core-vscode/login'
import { isExtensionActive, VSCODE_EXTENSION_ID } from 'aws-core-vscode/utils'
import { registerSubmitFeedback } from 'aws-core-vscode/feedback'
import { telemetry, ExtStartUpSources } from 'aws-core-vscode/telemetry'

export async function activateShared(context: vscode.ExtensionContext) {
const contextPrefix = 'amazonq'
Expand Down Expand Up @@ -121,9 +123,37 @@ export async function activateShared(context: vscode.ExtensionContext) {
// enable auto suggestions on activation
await CodeSuggestionsState.instance.setSuggestionsEnabled(true)

if (ExtensionUse.instance.isFirstUse()) {
if (AuthUtils.ExtensionUse.instance.isFirstUse()) {
await vscode.commands.executeCommand('workbench.view.extension.amazonq')
}

await telemetry.auth_userState.run(async () => {
telemetry.record({ passive: true })

const firstUse = AuthUtils.ExtensionUse.instance.isFirstUse()
const wasUpdated = AuthUtils.ExtensionUse.instance.wasUpdated()

if (firstUse) {
telemetry.record({ source: ExtStartUpSources.firstStartUp })
} else if (wasUpdated) {
telemetry.record({ source: ExtStartUpSources.update })
} else {
telemetry.record({ source: ExtStartUpSources.reload })
}

const authState = (await AuthUtil.instance.getChatAuthState()).codewhispererChat
const authKinds: AuthUtils.AuthSimpleId[] = []
if (await AuthUtils.hasBuilderId('codewhisperer')) {
authKinds.push('builderIdCodeWhisperer')
}
if (await AuthUtils.hasSso('codewhisperer')) {
authKinds.push('identityCenterCodeWhisperer')
}
telemetry.record({
authStatus: authState === 'connected' || authState === 'expired' ? authState : 'notConnected',
enabledAuthConnections: authKinds.join(','),
})
})
}

export async function deactivateShared() {
Expand Down
12 changes: 2 additions & 10 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"./amazonqGumby": "./dist/src/amazonqGumby/index.js",
"./login": "./dist/src/login/webview/index.js",
"./utils": "./dist/src/shared/utilities/index.js",
"./feedback": "./dist/src/feedback/index.js"
"./feedback": "./dist/src/feedback/index.js",
"./telemetry": "./dist/src/shared/telemetry/index.js"
},
"contributes": {
"configuration": {
Expand Down Expand Up @@ -1176,10 +1177,6 @@
{
"command": "aws.toolkit.amazonq.extensionpage",
"when": "false"
},
{
"command": "aws.toolkit.amazonq.dismiss",
"when": "false"
}
],
"editor/title": [
Expand Down Expand Up @@ -3459,11 +3456,6 @@
"title": "Open Amazon Q Extension",
"category": "%AWS.title%"
},
{
"command": "aws.toolkit.amazonq.dismiss",
"title": "Dismiss Q Notification",
"category": "%AWS.title%"
},
{
"command": "aws.dev.openMenu",
"title": "Open Developer Menu",
Expand Down
5 changes: 2 additions & 3 deletions packages/core/src/amazonq/auth/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
* SPDX-License-Identifier: Apache-2.0
*/

import * as vscode from 'vscode'
import { reconnect } from '../../codewhisperer/commands/basicCommands'
import { amazonQChatSource } from '../../codewhisperer/commands/types'
import { recordTelemetryChatRunCommand } from '../../codewhispererChat/controllers/chat/telemetryHelper'
import { placeholder } from '../../shared/vscode/commands2'
import { switchToAmazonQSignInCommand } from '../explorer/commonNodes'
import { AuthFollowUpType } from './model'

export class AuthController {
Expand All @@ -27,8 +27,7 @@ export class AuthController {
}

private handleFullAuth() {
void vscode.commands.executeCommand('setContext', 'aws.amazonq.showLoginView', true)
void vscode.commands.executeCommand('aws.amazonq.AmazonCommonAuth.focus')
void switchToAmazonQSignInCommand.execute('amazonQChat')
}

private handleReAuth() {
Expand Down
23 changes: 17 additions & 6 deletions packages/core/src/amazonq/explorer/amazonQChildrenNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { VSCODE_EXTENSION_ID } from '../../shared/extensions'
import { globals } from '../../shared'
import { amazonQDismissedKey } from '../../codewhisperer/models/constants'
import { _switchToAmazonQ } from './commonNodes'
import { ExtStartUpSources, telemetry } from '../../shared/telemetry'
import { ExtensionUse } from '../../auth/utils'

const localize = nls.loadMessageBundle()

Expand All @@ -25,12 +27,21 @@ export const qExtensionPageCommand = Commands.declare('aws.toolkit.amazonq.exten
void vscode.env.openExternal(vscode.Uri.parse(`vscode:extension/${VSCODE_EXTENSION_ID.amazonq}`))
})

export const dismissQTree = Commands.declare('aws.toolkit.amazonq.dismiss', () => async () => {
await globals.context.globalState.update(amazonQDismissedKey, true)
await vscode.commands.executeCommand('setContext', amazonQDismissedKey, true)
})
export const dismissQTree = Commands.declare(
{ id: '_aws.toolkit.amazonq.dismiss', compositeKey: { 0: 'source' } },
() => async (source: string) => {
await telemetry.toolkit_invokeAction.run(async () => {
telemetry.record({
source: ExtensionUse.instance.isFirstUse() ? ExtStartUpSources.firstStartUp : ExtStartUpSources.none,
})

await globals.context.globalState.update(amazonQDismissedKey, true)
await vscode.commands.executeCommand('setContext', amazonQDismissedKey, true)

export const toolkitSwitchToAmazonQCommand = Commands.declare('_aws.toolkit.amazonq.focusView', () => _switchToAmazonQ)
telemetry.record({ action: 'dismissQExplorerTree' })
})
}
)

// Learn more button of Amazon Q now opens the Amazon Q marketplace page.
export const createLearnMoreNode = () =>
Expand All @@ -48,7 +59,7 @@ export function createInstallQNode() {
}

export function createDismissNode() {
return dismissQTree.build().asTreeNode({
return dismissQTree.build(cwTreeNodeSource).asTreeNode({
label: 'Dismiss', // TODO: localize
iconPath: getIcon('vscode-close'),
})
Expand Down
13 changes: 3 additions & 10 deletions packages/core/src/amazonq/explorer/amazonQTreeNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,7 @@ import * as vscode from 'vscode'
import { createFreeTierLimitMet, createReconnect } from '../../codewhisperer/ui/codeWhispererNodes'
import { ResourceTreeDataProvider, TreeNode } from '../../shared/treeview/resourceTreeDataProvider'
import { AuthState, AuthUtil, isPreviousQUser } from '../../codewhisperer/util/authUtil'
import {
createLearnMoreNode,
createInstallQNode,
createDismissNode,
toolkitSwitchToAmazonQCommand,
} from './amazonQChildrenNodes'
import { createLearnMoreNode, createInstallQNode, createDismissNode } from './amazonQChildrenNodes'
import { Command, Commands } from '../../shared/vscode/commands2'
import { listCodeWhispererCommands } from '../../codewhisperer/ui/statusBarMenu'
import { getIcon } from '../../shared/icons'
Expand Down Expand Up @@ -89,13 +84,11 @@ export class AmazonQNode implements TreeNode {
}

if (AmazonQNode.amazonQState !== 'connected') {
return [createSignIn('tree', toolkitSwitchToAmazonQCommand), createLearnMoreNode()]
return [createSignIn('tree'), createLearnMoreNode()]
}

return [
vsCodeState.isFreeTierLimitReached
? createFreeTierLimitMet('tree')
: switchToAmazonQNode('tree', toolkitSwitchToAmazonQCommand),
vsCodeState.isFreeTierLimitReached ? createFreeTierLimitMet('tree') : switchToAmazonQNode('tree'),
createNewMenuButton(),
]
}
Expand Down
61 changes: 35 additions & 26 deletions packages/core/src/amazonq/explorer/commonNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,28 @@

import * as vscode from 'vscode'
import * as nls from 'vscode-nls'
import { Command, DeclaredCommand } from '../../shared/vscode/commands2'
import { Command, Commands } from '../../shared/vscode/commands2'
import { codicon, getIcon } from '../../shared/icons'
import { telemetry } from '../../shared/telemetry/telemetry'
import { DataQuickPickItem } from '../../shared/ui/pickerPrompter'
import { TreeNode } from '../../shared/treeview/resourceTreeDataProvider'
import { CodeWhispererSource } from '../../codewhisperer/commands/types'

const localize = nls.loadMessageBundle()

/**
* Do not call this function directly, use the necessary equivalent command registered by the extensions:
* - switchToAmazonQCommand, _aws.amazonq.focusView (for Amazon Q code)
* - toolkitSwitchToAmazonQCommand, _aws.toolkit.amazonq.focusView (for Toolkit code)
* Do not call this function directly, use the necessary equivalent commands below,
* which areregistered by the Amazon Q extension.
* - switchToAmazonQCommand
* - switchToAmazonQSignInCommand
*/
export async function _switchToAmazonQ(signIn: boolean = false) {
export async function _switchToAmazonQ(signIn: boolean) {
if (signIn) {
await vscode.commands.executeCommand('setContext', 'aws.amazonq.showLoginView', true)
telemetry.ui_click.emit({
elementId: 'amazonq_switchToQSignIn',
passive: false,
})
} else {
telemetry.ui_click.emit({
elementId: 'amazonq_switchToQChat',
Expand All @@ -34,6 +40,18 @@ export async function _switchToAmazonQ(signIn: boolean = false) {
await vscode.commands.executeCommand('aws.amazonq.AmazonCommonAuth.focus')
}

export const switchToAmazonQCommand = Commands.declare(
{ id: '_aws.amazonq.focusView', compositeKey: { 0: 'source' } },
() =>
(source: CodeWhispererSource, signIn: boolean = false) =>
_switchToAmazonQ(false)
)

export const switchToAmazonQSignInCommand = Commands.declare(
{ id: '_aws.amazonq.signIn.focusView', compositeKey: { 0: 'source' } },
() => (source: CodeWhispererSource) => _switchToAmazonQ(true)
)

/**
* Common nodes that can be used by mutliple UIs, e.g. status bar menu, explorer tree, etc.
* Individual extensions may register their own commands for the nodes, so it must be passed in.
Expand All @@ -42,19 +60,13 @@ export async function _switchToAmazonQ(signIn: boolean = false) {
* and only use the one registered in Amazon Q.
*/

export function switchToAmazonQNode(
type: 'item',
cmd: DeclaredCommand<typeof _switchToAmazonQ>
): DataQuickPickItem<'openChatPanel'>
export function switchToAmazonQNode(type: 'tree', cmd: DeclaredCommand<typeof _switchToAmazonQ>): TreeNode<Command>
export function switchToAmazonQNode(
type: 'item' | 'tree',
cmd: DeclaredCommand<typeof _switchToAmazonQ>
): DataQuickPickItem<'openChatPanel'> | TreeNode<Command>
export function switchToAmazonQNode(type: 'item' | 'tree', cmd: DeclaredCommand<typeof _switchToAmazonQ>): any {
export function switchToAmazonQNode(type: 'item'): DataQuickPickItem<'openChatPanel'>
export function switchToAmazonQNode(type: 'tree'): TreeNode<Command>
export function switchToAmazonQNode(type: 'item' | 'tree'): DataQuickPickItem<'openChatPanel'> | TreeNode<Command>
export function switchToAmazonQNode(type: 'item' | 'tree'): any {
switch (type) {
case 'tree':
return cmd.build().asTreeNode({
return switchToAmazonQCommand.build('codewhispererTreeNode').asTreeNode({
label: 'Open Chat Panel',
iconPath: getIcon('vscode-comment'),
contextValue: 'awsToAmazonQChatNode',
Expand All @@ -64,32 +76,29 @@ export function switchToAmazonQNode(type: 'item' | 'tree', cmd: DeclaredCommand<
data: 'openChatPanel',
label: 'Open Chat Panel',
iconPath: getIcon('vscode-comment'),
onClick: () => cmd.execute(),
onClick: () => switchToAmazonQCommand.execute('codewhispererQuickPick'),
} as DataQuickPickItem<'openChatPanel'>
}
}

export function createSignIn(type: 'item', cmd: DeclaredCommand<typeof _switchToAmazonQ>): DataQuickPickItem<'signIn'>
export function createSignIn(type: 'tree', cmd: DeclaredCommand<typeof _switchToAmazonQ>): TreeNode<Command>
export function createSignIn(
type: 'item' | 'tree',
cmd: DeclaredCommand<typeof _switchToAmazonQ>
): DataQuickPickItem<'signIn'> | TreeNode<Command>
export function createSignIn(type: 'item' | 'tree', cmd: DeclaredCommand<typeof _switchToAmazonQ>): any {
export function createSignIn(type: 'item'): DataQuickPickItem<'signIn'>
export function createSignIn(type: 'tree'): TreeNode<Command>
export function createSignIn(type: 'item' | 'tree'): DataQuickPickItem<'signIn'> | TreeNode<Command>
export function createSignIn(type: 'item' | 'tree'): any {
const label = localize('AWS.codewhisperer.signInNode.label', 'Sign in to get started')
const icon = getIcon('vscode-account')

switch (type) {
case 'tree':
return cmd.build(true).asTreeNode({
return switchToAmazonQSignInCommand.build('codewhispererTreeNode').asTreeNode({
label: label,
iconPath: icon,
})
case 'item':
return {
data: 'signIn',
label: codicon`${icon} ${label}`,
onClick: () => cmd.execute(true),
onClick: () => switchToAmazonQSignInCommand.execute('codewhispererQuickPick'),
} as DataQuickPickItem<'signIn'>
}
}
2 changes: 1 addition & 1 deletion packages/core/src/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ import { randomUUID } from '../common/crypto'

interface AuthService {
/**
* Lists all connections known to the Toolkit.
* Lists all connections known to the extension.
*/
listConnections(): Promise<Connection[]>

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ export { Connection, AwsConnection } from './connection'
export { Auth } from './auth'
export { CredentialsStore } from './credentials/store'
export { LoginManager } from './deprecated/loginManager'
export { ExtensionUse } from './utils'
export * as AuthUtils from './utils'
3 changes: 2 additions & 1 deletion packages/core/src/auth/ui/vue/show.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import { debounce } from 'lodash'
import { submitFeedback } from '../../../feedback/vue/submitFeedback'
import { InvalidGrantException } from '@aws-sdk/client-sso-oidc'
import { isWeb } from '../../../common/webUtils'
import { ExtStartUpSources } from '../../../shared/telemetry'

export class AuthWebview extends VueWebview {
public static readonly sourcePath: string = 'src/auth/ui/vue/index.js'
Expand Down Expand Up @@ -842,7 +843,7 @@ export async function emitWebviewClosed(authWebview: ClassToInterfaceType<AuthWe
result = 'Cancelled'
}

if (source === 'firstStartup' && numConnectionsAdded === 0) {
if (source === ExtStartUpSources.firstStartUp && numConnectionsAdded === 0) {
if (numConnectionsInitial > 0) {
// This is the users first startup of the extension and no new connections were added, but they already had connections setup on their
// system which we discovered. We consider this a success even though they added no new connections.
Expand Down
Loading

0 comments on commit b899e82

Please sign in to comment.