Skip to content

Commit 72e4f5b

Browse files
committed
feat(notifications): setup and activation
- Gated behind dev setting (`aws.dev.notifications`) - Setup the notifications panel and begin polling in each of the extensions. - Refactor auth state code of each extension so that we can re-use that information for the notification rule engine.
1 parent 77bd8a7 commit 72e4f5b

File tree

14 files changed

+208
-124
lines changed

14 files changed

+208
-124
lines changed

packages/amazonq/src/extension.ts

Lines changed: 42 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,14 @@ import {
4646
maybeShowMinVscodeWarning,
4747
isSageMaker,
4848
} from 'aws-core-vscode/shared'
49-
import { ExtStartUpSources, telemetry } from 'aws-core-vscode/telemetry'
49+
import { AuthUserState, ExtStartUpSources, telemetry } from 'aws-core-vscode/telemetry'
5050
import { VSCODE_EXTENSION_ID } from 'aws-core-vscode/utils'
5151
import { join } from 'path'
5252
import * as semver from 'semver'
5353
import * as vscode from 'vscode'
5454
import { registerCommands } from './commands'
5555
import { focusAmazonQPanel } from 'aws-core-vscode/codewhispererChat'
56+
import { activate as activateNotifications } from 'aws-core-vscode/notifications'
5657

5758
export const amazonQContextPrefix = 'amazonq'
5859

@@ -162,52 +163,46 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is
162163
}, 1000)
163164
}
164165

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))
166+
const authState = await getAuthState()
167+
telemetry.auth_userState.emit({
168+
passive: true,
169+
source: AuthUtils.ExtensionUse.instance.sourceForTelemetry(),
170+
...authState,
171+
})
172+
173+
await activateNotifications(context, authState, getAuthState)
174+
}
175+
176+
async function getAuthState(): Promise<Omit<AuthUserState, 'source'>> {
177+
let authState: AuthState = 'disconnected'
178+
try {
179+
// May call connection validate functions that try to refresh the token.
180+
// This could result in network errors.
181+
authState = (await AuthUtil.instance.getChatAuthState(false)).codewhispererChat
182+
} catch (err) {
183+
if (
184+
isNetworkError(err) &&
185+
AuthUtil.instance.conn &&
186+
AuthUtil.instance.auth.getConnectionState(AuthUtil.instance.conn) === 'valid'
187+
) {
188+
authState = 'connectedWithNetworkError'
189+
} else {
190+
throw err
191+
}
192+
}
193+
const currConn = AuthUtil.instance.conn
194+
if (currConn !== undefined && !(isAnySsoConnection(currConn) || isSageMaker())) {
195+
getLogger().error(`Current Amazon Q connection is not SSO, type is: %s`, currConn?.type)
196+
}
197+
198+
return {
199+
authStatus:
200+
authState === 'connected' || authState === 'expired' || authState === 'connectedWithNetworkError'
201+
? authState
202+
: 'notConnected',
203+
authEnabledConnections: AuthUtils.getAuthFormIdsFromConnection(currConn).join(','),
204+
...(await getTelemetryMetadataForConn(currConn)),
205+
}
211206
}
212207

213208
export async function deactivateCommon() {

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: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,24 +38,27 @@ import { activate as activateDev } from './dev/activation'
3838
import { activate as activateApplicationComposer } from './applicationcomposer/activation'
3939
import { activate as activateRedshift } from './awsService/redshift/activation'
4040
import { activate as activateIamPolicyChecks } from './awsService/accessanalyzer/activation'
41+
import { activate as activateNotifications } from './notifications/activate'
4142
import { SchemaService } from './shared/schemas'
4243
import { AwsResourceManager } from './dynamicResources/awsResourceManager'
4344
import globals from './shared/extensionGlobals'
4445
import { Experiments, Settings, showSettingsFailedMsg } from './shared/settings'
4546
import { isReleaseVersion } from './shared/vscode/env'
46-
import { telemetry } from './shared/telemetry/telemetry'
47+
import { AuthStatus, AuthUserState, telemetry } from './shared/telemetry/telemetry'
4748
import { Auth, SessionSeparationPrompt } from './auth/auth'
49+
import { getTelemetryMetadataForConn } from './auth/connection'
4850
import { registerSubmitFeedback } from './feedback/vue/submitFeedback'
49-
import { activateCommon, deactivateCommon, emitUserState } from './extension'
51+
import { activateCommon, deactivateCommon } from './extension'
5052
import { learnMoreAmazonQCommand, qExtensionPageCommand, dismissQTree } from './amazonq/explorer/amazonQChildrenNodes'
5153
import { AuthUtil, codeWhispererCoreScopes, isPreviousQUser } from './codewhisperer/util/authUtil'
5254
import { installAmazonQExtension } from './codewhisperer/commands/basicCommands'
5355
import { isExtensionInstalled, VSCODE_EXTENSION_ID } from './shared/utilities'
54-
import { ExtensionUse, initializeCredentialsProviderManager } from './auth/utils'
56+
import { ExtensionUse, getAuthFormIdsFromConnection, initializeCredentialsProviderManager } from './auth/utils'
5557
import { ExtStartUpSources } from './shared/telemetry'
5658
import { activate as activateThreatComposerEditor } from './threatComposer/activation'
5759
import { isSsoConnection, hasScopes } from './auth/connection'
5860
import { CrashMonitoring, setContext } from './shared'
61+
import { AuthFormId } from './login/webview/vue/types'
5962

6063
let localize: nls.LocalizeFunc
6164

@@ -231,7 +234,14 @@ export async function activate(context: vscode.ExtensionContext) {
231234
globals.telemetry.assertPassiveTelemetry(globals.didReload)
232235
}
233236

234-
await emitUserState()
237+
const authState = await getAuthState()
238+
telemetry.auth_userState.emit({
239+
passive: true,
240+
source: ExtensionUse.instance.sourceForTelemetry(),
241+
...authState,
242+
})
243+
244+
await activateNotifications(context, authState, getAuthState)
235245
} catch (error) {
236246
const stacktrace = (error as Error).stack?.split('\n')
237247
// truncate if the stacktrace is unusually long
@@ -322,3 +332,36 @@ function recordToolkitInitialization(activationStartedOn: number, settingsValid:
322332
logger?.error(err as Error)
323333
}
324334
}
335+
336+
async function getAuthState(): Promise<Omit<AuthUserState, 'source'>> {
337+
let authStatus: AuthStatus = 'notConnected'
338+
const enabledConnections: Set<AuthFormId> = new Set()
339+
const enabledScopes: Set<string> = new Set()
340+
if (Auth.instance.hasConnections) {
341+
authStatus = 'expired'
342+
;(await Auth.instance.listConnections()).forEach((conn) => {
343+
const state = Auth.instance.getConnectionState(conn)
344+
if (state === 'valid') {
345+
authStatus = 'connected'
346+
}
347+
348+
getAuthFormIdsFromConnection(conn).forEach((id) => enabledConnections.add(id))
349+
if (isSsoConnection(conn)) {
350+
conn.scopes?.forEach((s) => enabledScopes.add(s))
351+
}
352+
})
353+
}
354+
355+
// There may be other SSO connections in toolkit, but there is no use case for
356+
// displaying registration info for non-active connections at this time.
357+
const activeConn = Auth.instance.activeConnection
358+
if (activeConn?.type === 'sso') {
359+
telemetry.record(await getTelemetryMetadataForConn(activeConn))
360+
}
361+
362+
return {
363+
authStatus,
364+
authEnabledConnections: [...enabledConnections].sort().join(','),
365+
authScopes: [...enabledScopes].sort().join(','),
366+
}
367+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import * as vscode from 'vscode'
7+
import { DevSettings } from '../shared/settings'
8+
import { DevFetcher, NotificationsController } from './controller'
9+
import { NotificationsNode } from './panelNode'
10+
import { RuleEngine, getRuleContext } from './rules'
11+
import globals from '../shared/extensionGlobals'
12+
import { AuthState } from './types'
13+
14+
/** Time in MS to poll for emergency notifications */
15+
const emergencyPollTime = 1000 * 10 * 60
16+
17+
/**
18+
* Activate the in-IDE notifications module and begin receiving notifications.
19+
*
20+
* @param context extension context
21+
* @param initialState initial auth state
22+
* @param authStateFn fn to get current auth state
23+
*/
24+
export async function activate(
25+
context: vscode.ExtensionContext,
26+
initialState: AuthState,
27+
authStateFn: () => Promise<AuthState>
28+
) {
29+
// TODO: Currently gated behind feature-flag.
30+
if (!DevSettings.instance.get('notifications', false)) {
31+
return
32+
}
33+
34+
const panelNode = NotificationsNode.instance
35+
panelNode.registerView(context)
36+
37+
const controller = new NotificationsController(panelNode)
38+
const engine = new RuleEngine(await getRuleContext(context, initialState))
39+
40+
void controller.pollForStartUp(engine)
41+
void controller.pollForEmergencies(engine)
42+
43+
globals.clock.setInterval(async () => {
44+
const ruleContext = await getRuleContext(context, await authStateFn())
45+
await controller.pollForEmergencies(new RuleEngine(ruleContext))
46+
}, emergencyPollTime)
47+
}

packages/core/src/notifications/index.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,4 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
export { RuleContext } from './types'
7-
export { NotificationsController } from './controller'
8-
export { RuleEngine } from './rules'
9-
export { registerProvider, NotificationsNode } from './panelNode'
6+
export { activate } from './activate'

0 commit comments

Comments
 (0)