Skip to content

Commit 2c356e3

Browse files
authored
feat(notifications): setup and activation (#5932)
- Gated behind dev setting (`aws.dev.notifications`) - Setup the notifications panel and begin polling in each of the extensions. - Currently the code collects a bunch of information about the state of auth for telemetry (auth_userState). Refactor this code in each extension so that we can re-use that information for the notification rule engine. - Change emitting auth_userState to node only for amazonq, since notifications is node-only. Decided to punt this work for web because spending effort to make it web-compatible may be in vain if the design of web metrics is different. --- <!--- REMINDER: Ensure that your PR meets the guidelines in CONTRIBUTING.md --> License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 89375c4 commit 2c356e3

File tree

15 files changed

+221
-143
lines changed

15 files changed

+221
-143
lines changed

packages/amazonq/src/extension.ts

Lines changed: 3 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,8 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import {
7-
AuthUtils,
8-
CredentialsStore,
9-
LoginManager,
10-
getTelemetryMetadataForConn,
11-
initializeAuth,
12-
isAnySsoConnection,
13-
} from 'aws-core-vscode/auth'
14-
import {
15-
AuthState,
16-
AuthUtil,
17-
activate as activateCodeWhisperer,
18-
shutdown as shutdownCodeWhisperer,
19-
} from 'aws-core-vscode/codewhisperer'
6+
import { AuthUtils, CredentialsStore, LoginManager, initializeAuth } from 'aws-core-vscode/auth'
7+
import { activate as activateCodeWhisperer, shutdown as shutdownCodeWhisperer } from 'aws-core-vscode/codewhisperer'
208
import { makeEndpointsProvider, registerGenericCommands } from 'aws-core-vscode'
219
import { CommonAuthWebview } from 'aws-core-vscode/login'
2210
import {
@@ -38,15 +26,13 @@ import {
3826
globals,
3927
initialize,
4028
initializeComputeRegion,
41-
isNetworkError,
4229
messages,
4330
placeholder,
4431
setContext,
4532
setupUninstallHandler,
4633
maybeShowMinVscodeWarning,
47-
isSageMaker,
4834
} from 'aws-core-vscode/shared'
49-
import { ExtStartUpSources, telemetry } from 'aws-core-vscode/telemetry'
35+
import { ExtStartUpSources } from 'aws-core-vscode/telemetry'
5036
import { VSCODE_EXTENSION_ID } from 'aws-core-vscode/utils'
5137
import { join } from 'path'
5238
import * as semver from 'semver'
@@ -161,53 +147,6 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is
161147
void focusAmazonQPanel.execute(placeholder, 'firstStartUp')
162148
}, 1000)
163149
}
164-
165-
await telemetry.auth_userState
166-
.run(async () => {
167-
telemetry.record({ passive: true })
168-
169-
const firstUse = AuthUtils.ExtensionUse.instance.isFirstUse()
170-
const wasUpdated = AuthUtils.ExtensionUse.instance.wasUpdated()
171-
172-
if (firstUse) {
173-
telemetry.record({ source: ExtStartUpSources.firstStartUp })
174-
} else if (wasUpdated) {
175-
telemetry.record({ source: ExtStartUpSources.update })
176-
} else {
177-
telemetry.record({ source: ExtStartUpSources.reload })
178-
}
179-
180-
let authState: AuthState = 'disconnected'
181-
try {
182-
// May call connection validate functions that try to refresh the token.
183-
// This could result in network errors.
184-
authState = (await AuthUtil.instance.getChatAuthState(false)).codewhispererChat
185-
} catch (err) {
186-
if (
187-
isNetworkError(err) &&
188-
AuthUtil.instance.conn &&
189-
AuthUtil.instance.auth.getConnectionState(AuthUtil.instance.conn) === 'valid'
190-
) {
191-
authState = 'connectedWithNetworkError'
192-
} else {
193-
throw err
194-
}
195-
}
196-
const currConn = AuthUtil.instance.conn
197-
if (currConn !== undefined && !(isAnySsoConnection(currConn) || isSageMaker())) {
198-
getLogger().error(`Current Amazon Q connection is not SSO, type is: %s`, currConn?.type)
199-
}
200-
201-
telemetry.record({
202-
authStatus:
203-
authState === 'connected' || authState === 'expired' || authState === 'connectedWithNetworkError'
204-
? authState
205-
: 'notConnected',
206-
authEnabledConnections: AuthUtils.getAuthFormIdsFromConnection(currConn).join(','),
207-
...(await getTelemetryMetadataForConn(currConn)),
208-
})
209-
})
210-
.catch((err) => getLogger().error('Error collecting telemetry for auth_userState: %s', err))
211150
}
212151

213152
export async function deactivateCommon() {

packages/amazonq/src/extensionNode.ts

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,20 @@ import * as vscode from 'vscode'
77
import { activateAmazonQCommon, amazonQContextPrefix, deactivateCommon } from './extension'
88
import { DefaultAmazonQAppInitContext } from 'aws-core-vscode/amazonq'
99
import { activate as activateQGumby } from 'aws-core-vscode/amazonqGumby'
10-
import { ExtContext, globals, CrashMonitoring } from 'aws-core-vscode/shared'
10+
import { ExtContext, globals, CrashMonitoring, getLogger, isNetworkError, isSageMaker } from 'aws-core-vscode/shared'
1111
import { filetypes, SchemaService } from 'aws-core-vscode/sharedNode'
1212
import { updateDevMode } from 'aws-core-vscode/dev'
1313
import { CommonAuthViewProvider } from 'aws-core-vscode/login'
1414
import { isExtensionActive, VSCODE_EXTENSION_ID } from 'aws-core-vscode/utils'
1515
import { registerSubmitFeedback } from 'aws-core-vscode/feedback'
1616
import { DevOptions } from 'aws-core-vscode/dev'
17-
import { Auth } from 'aws-core-vscode/auth'
17+
import { Auth, AuthUtils, getTelemetryMetadataForConn, isAnySsoConnection } from 'aws-core-vscode/auth'
1818
import api from './api'
1919
import { activate as activateCWChat } from './app/chat/activation'
2020
import { beta } from 'aws-core-vscode/dev'
21+
import { activate as activateNotifications } from 'aws-core-vscode/notifications'
22+
import { AuthState, AuthUtil } from 'aws-core-vscode/codewhisperer'
23+
import { telemetry, AuthUserState } from 'aws-core-vscode/telemetry'
2124

2225
export async function activate(context: vscode.ExtensionContext) {
2326
// IMPORTANT: No other code should be added to this function. Place it in one of the following 2 functions where appropriate.
@@ -61,6 +64,50 @@ async function activateAmazonQNode(context: vscode.ExtensionContext) {
6164

6265
await setupDevMode(context)
6366
await beta.activate(context)
67+
68+
// TODO: Should probably emit for web as well.
69+
// Will the web metric look the same?
70+
const authState = await getAuthState()
71+
telemetry.auth_userState.emit({
72+
passive: true,
73+
result: 'Succeeded',
74+
source: AuthUtils.ExtensionUse.instance.sourceForTelemetry(),
75+
...authState,
76+
})
77+
78+
await activateNotifications(context, authState, getAuthState)
79+
}
80+
81+
async function getAuthState(): Promise<Omit<AuthUserState, 'source'>> {
82+
let authState: AuthState = 'disconnected'
83+
try {
84+
// May call connection validate functions that try to refresh the token.
85+
// This could result in network errors.
86+
authState = (await AuthUtil.instance.getChatAuthState(false)).codewhispererChat
87+
} catch (err) {
88+
if (
89+
isNetworkError(err) &&
90+
AuthUtil.instance.conn &&
91+
AuthUtil.instance.auth.getConnectionState(AuthUtil.instance.conn) === 'valid'
92+
) {
93+
authState = 'connectedWithNetworkError'
94+
} else {
95+
throw err
96+
}
97+
}
98+
const currConn = AuthUtil.instance.conn
99+
if (currConn !== undefined && !(isAnySsoConnection(currConn) || isSageMaker())) {
100+
getLogger().error(`Current Amazon Q connection is not SSO, type is: %s`, currConn?.type)
101+
}
102+
103+
return {
104+
authStatus:
105+
authState === 'connected' || authState === 'expired' || authState === 'connectedWithNetworkError'
106+
? authState
107+
: 'notConnected',
108+
authEnabledConnections: AuthUtils.getAuthFormIdsFromConnection(currConn).join(','),
109+
...(await getTelemetryMetadataForConn(currConn)),
110+
}
64111
}
65112

66113
/**

packages/core/src/auth/utils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import { EcsCredentialsProvider } from './providers/ecsCredentialsProvider'
5959
import { EnvVarsCredentialsProvider } from './providers/envVarsCredentialsProvider'
6060
import { showMessageWithUrl } from '../shared/utilities/messages'
6161
import { credentialHelpUrl } from '../shared/constants'
62+
import { ExtStartUpSource } from '../shared/telemetry/util'
6263

6364
// iam-only excludes Builder ID and IAM Identity Center from the list of valid connections
6465
// TODO: Understand if "iam" should include these from the list at all
@@ -734,6 +735,19 @@ export class ExtensionUse {
734735
return this.wasExtensionUpdated
735736
}
736737

738+
/**
739+
* Returns a {@link ExtStartUpSource} based on the current state of the extension.
740+
*/
741+
sourceForTelemetry(): ExtStartUpSource {
742+
if (this.isFirstUse()) {
743+
return ExtStartUpSources.firstStartUp
744+
} else if (this.wasUpdated()) {
745+
return ExtStartUpSources.update
746+
} else {
747+
return ExtStartUpSources.reload
748+
}
749+
}
750+
737751
private updateMemento(key: 'isExtensionFirstUse' | 'lastExtensionVersion', val: any) {
738752
globals.globalState.tryUpdate(key, val)
739753
}

packages/core/src/awsexplorer/activationShared.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,6 @@ export function registerToolView(viewNode: ToolView, context: vscode.ExtensionCo
2727
telemetry.cdk_appExpanded.emit()
2828
}
2929
})
30+
31+
return toolView
3032
}

packages/core/src/extension.ts

Lines changed: 1 addition & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { Commands } from './shared/vscode/commands2'
1919
import { endpointsFileUrl, githubCreateIssueUrl, githubUrl } from './shared/constants'
2020
import { getIdeProperties, aboutExtension, isCloud9, getDocUrl } from './shared/extensionUtilities'
2121
import { logAndShowError, logAndShowWebviewError } from './shared/utilities/logAndShowUtils'
22-
import { AuthStatus, telemetry } from './shared/telemetry/telemetry'
22+
import { telemetry } from './shared/telemetry/telemetry'
2323
import { openUrl } from './shared/utilities/vsCodeUtils'
2424
import { activateViewsShared } from './awsexplorer/activationShared'
2525
import fs from './shared/fs/fs'
@@ -45,11 +45,6 @@ import { UriHandler } from './shared/vscode/uriHandler'
4545
import { disableAwsSdkWarning } from './shared/awsClientBuilder'
4646
import { FileResourceFetcher } from './shared/resourcefetcher/fileResourceFetcher'
4747
import { ResourceFetcher } from './shared/resourcefetcher/resourcefetcher'
48-
import { ExtStartUpSources } from './shared/telemetry/util'
49-
import { ExtensionUse, getAuthFormIdsFromConnection } from './auth/utils'
50-
import { Auth } from './auth'
51-
import { AuthFormId } from './login/webview/vue/types'
52-
import { getTelemetryMetadataForConn, isSsoConnection } from './auth/connection'
5348
import { registerCommands } from './commands'
5449

5550
// In web mode everything must be in a single file, so things like the endpoints file will not be available.
@@ -258,51 +253,3 @@ function wrapWithProgressForCloud9(channel: vscode.OutputChannel): (typeof vscod
258253
})
259254
}
260255
}
261-
262-
export async function emitUserState() {
263-
await telemetry.auth_userState.run(async () => {
264-
telemetry.record({ passive: true })
265-
266-
const firstUse = ExtensionUse.instance.isFirstUse()
267-
const wasUpdated = ExtensionUse.instance.wasUpdated()
268-
269-
if (firstUse) {
270-
telemetry.record({ source: ExtStartUpSources.firstStartUp })
271-
} else if (wasUpdated) {
272-
telemetry.record({ source: ExtStartUpSources.update })
273-
} else {
274-
telemetry.record({ source: ExtStartUpSources.reload })
275-
}
276-
277-
let authStatus: AuthStatus = 'notConnected'
278-
const enabledConnections: Set<AuthFormId> = new Set()
279-
const enabledScopes: Set<string> = new Set()
280-
if (Auth.instance.hasConnections) {
281-
authStatus = 'expired'
282-
;(await Auth.instance.listConnections()).forEach((conn) => {
283-
const state = Auth.instance.getConnectionState(conn)
284-
if (state === 'valid') {
285-
authStatus = 'connected'
286-
}
287-
288-
getAuthFormIdsFromConnection(conn).forEach((id) => enabledConnections.add(id))
289-
if (isSsoConnection(conn)) {
290-
conn.scopes?.forEach((s) => enabledScopes.add(s))
291-
}
292-
})
293-
}
294-
295-
// There may be other SSO connections in toolkit, but there is no use case for
296-
// displaying registration info for non-active connections at this time.
297-
const activeConn = Auth.instance.activeConnection
298-
if (activeConn?.type === 'sso') {
299-
telemetry.record(await getTelemetryMetadataForConn(activeConn))
300-
}
301-
302-
telemetry.record({
303-
authStatus,
304-
authEnabledConnections: [...enabledConnections].join(','),
305-
authScopes: [...enabledScopes].join(','),
306-
})
307-
})
308-
}

packages/core/src/extensionNode.ts

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,24 +39,27 @@ import * as beta from './dev/beta'
3939
import { activate as activateApplicationComposer } from './applicationcomposer/activation'
4040
import { activate as activateRedshift } from './awsService/redshift/activation'
4141
import { activate as activateIamPolicyChecks } from './awsService/accessanalyzer/activation'
42+
import { activate as activateNotifications } from './notifications/activation'
4243
import { SchemaService } from './shared/schemas'
4344
import { AwsResourceManager } from './dynamicResources/awsResourceManager'
4445
import globals from './shared/extensionGlobals'
4546
import { Experiments, Settings, showSettingsFailedMsg } from './shared/settings'
4647
import { isReleaseVersion } from './shared/vscode/env'
47-
import { telemetry } from './shared/telemetry/telemetry'
48+
import { AuthStatus, AuthUserState, telemetry } from './shared/telemetry/telemetry'
4849
import { Auth, SessionSeparationPrompt } from './auth/auth'
50+
import { getTelemetryMetadataForConn } from './auth/connection'
4951
import { registerSubmitFeedback } from './feedback/vue/submitFeedback'
50-
import { activateCommon, deactivateCommon, emitUserState } from './extension'
52+
import { activateCommon, deactivateCommon } from './extension'
5153
import { learnMoreAmazonQCommand, qExtensionPageCommand, dismissQTree } from './amazonq/explorer/amazonQChildrenNodes'
5254
import { AuthUtil, codeWhispererCoreScopes, isPreviousQUser } from './codewhisperer/util/authUtil'
5355
import { installAmazonQExtension } from './codewhisperer/commands/basicCommands'
5456
import { isExtensionInstalled, VSCODE_EXTENSION_ID } from './shared/utilities'
55-
import { ExtensionUse, initializeCredentialsProviderManager } from './auth/utils'
57+
import { ExtensionUse, getAuthFormIdsFromConnection, initializeCredentialsProviderManager } from './auth/utils'
5658
import { ExtStartUpSources } from './shared/telemetry'
5759
import { activate as activateThreatComposerEditor } from './threatComposer/activation'
5860
import { isSsoConnection, hasScopes } from './auth/connection'
5961
import { CrashMonitoring, setContext } from './shared'
62+
import { AuthFormId } from './login/webview/vue/types'
6063

6164
let localize: nls.LocalizeFunc
6265

@@ -233,7 +236,17 @@ export async function activate(context: vscode.ExtensionContext) {
233236
globals.telemetry.assertPassiveTelemetry(globals.didReload)
234237
}
235238

236-
await emitUserState()
239+
// TODO: Should probably emit for web as well.
240+
// Will the web metric look the same?
241+
const authState = await getAuthState()
242+
telemetry.auth_userState.emit({
243+
passive: true,
244+
result: 'Succeeded',
245+
source: ExtensionUse.instance.sourceForTelemetry(),
246+
...authState,
247+
})
248+
249+
await activateNotifications(context, authState, getAuthState)
237250
} catch (error) {
238251
const stacktrace = (error as Error).stack?.split('\n')
239252
// truncate if the stacktrace is unusually long
@@ -324,3 +337,36 @@ function recordToolkitInitialization(activationStartedOn: number, settingsValid:
324337
logger?.error(err as Error)
325338
}
326339
}
340+
341+
async function getAuthState(): Promise<Omit<AuthUserState, 'source'>> {
342+
let authStatus: AuthStatus = 'notConnected'
343+
const enabledConnections: Set<AuthFormId> = new Set()
344+
const enabledScopes: Set<string> = new Set()
345+
if (Auth.instance.hasConnections) {
346+
authStatus = 'expired'
347+
;(await Auth.instance.listConnections()).forEach((conn) => {
348+
const state = Auth.instance.getConnectionState(conn)
349+
if (state === 'valid') {
350+
authStatus = 'connected'
351+
}
352+
353+
getAuthFormIdsFromConnection(conn).forEach((id) => enabledConnections.add(id))
354+
if (isSsoConnection(conn)) {
355+
conn.scopes?.forEach((s) => enabledScopes.add(s))
356+
}
357+
})
358+
}
359+
360+
// There may be other SSO connections in toolkit, but there is no use case for
361+
// displaying registration info for non-active connections at this time.
362+
const activeConn = Auth.instance.activeConnection
363+
if (activeConn?.type === 'sso') {
364+
telemetry.record(await getTelemetryMetadataForConn(activeConn))
365+
}
366+
367+
return {
368+
authStatus,
369+
authEnabledConnections: [...enabledConnections].sort().join(','),
370+
authScopes: [...enabledScopes].sort().join(','),
371+
}
372+
}

0 commit comments

Comments
 (0)