Skip to content

Commit 85e90cd

Browse files
Merge master into feature/emr
2 parents 2defe39 + aac385c commit 85e90cd

File tree

5 files changed

+92
-61
lines changed

5 files changed

+92
-61
lines changed

packages/core/src/notifications/activation.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { NotificationsNode } from './panelNode'
1010
import { RuleEngine, getRuleContext } from './rules'
1111
import globals from '../shared/extensionGlobals'
1212
import { AuthState } from './types'
13+
import { getLogger } from '../shared/logger/logger'
1314

1415
/** Time in MS to poll for emergency notifications */
1516
const emergencyPollTime = 1000 * 10 * 60
@@ -44,4 +45,6 @@ export async function activate(
4445
const ruleContext = await getRuleContext(context, await authStateFn())
4546
await controller.pollForEmergencies(new RuleEngine(ruleContext))
4647
}, emergencyPollTime)
48+
49+
getLogger('notifications').debug('Activated in-IDE notifications polling module')
4750
}

packages/core/src/notifications/controller.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ import * as vscode from 'vscode'
77
import { ToolkitError } from '../shared/errors'
88
import globals from '../shared/extensionGlobals'
99
import { globalKey } from '../shared/globalState'
10-
import { NotificationsState, NotificationsStateConstructor, NotificationType, ToolkitNotification } from './types'
10+
import {
11+
getNotificationTelemetryId,
12+
NotificationsState,
13+
NotificationsStateConstructor,
14+
NotificationType,
15+
ToolkitNotification,
16+
} from './types'
1117
import { HttpResourceFetcher } from '../shared/resourcefetcher/httpResourceFetcher'
1218
import { getLogger } from '../shared/logger/logger'
1319
import { NotificationsNode } from './panelNode'
@@ -17,6 +23,7 @@ import { TreeNode } from '../shared/treeview/resourceTreeDataProvider'
1723
import { withRetries } from '../shared/utilities/functionUtils'
1824
import { FileResourceFetcher } from '../shared/resourcefetcher/fileResourceFetcher'
1925
import { isAmazonQ } from '../shared/extensionUtilities'
26+
import { telemetry } from '../shared/telemetry/telemetry'
2027

2128
/**
2229
* Handles fetching and maintaining the state of in-IDE notifications.
@@ -203,7 +210,10 @@ function registerDismissCommand() {
203210
/** See {@link NotificationsNode} for more info. */
204211
const notification = item.command?.arguments[0] as ToolkitNotification
205212

206-
await NotificationsController.instance.dismissNotification(notification.id)
213+
await telemetry.ui_click.run(async (span) => {
214+
span.record({ elementId: `${getNotificationTelemetryId(notification)}:DISMISS` })
215+
await NotificationsController.instance.dismissNotification(notification.id)
216+
})
207217
} else {
208218
getLogger('notifications').error(`${name}: Cannot dismiss notification: item is not a vscode.TreeItem`)
209219
}

packages/core/src/notifications/panelNode.ts

Lines changed: 65 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ import { ResourceTreeDataProvider, TreeNode } from '../shared/treeview/resourceT
88
import { Command, Commands } from '../shared/vscode/commands2'
99
import { getIcon } from '../shared/icons'
1010
import { contextKey, setContext } from '../shared/vscode/setContext'
11-
import { NotificationType, ToolkitNotification } from './types'
11+
import { NotificationType, ToolkitNotification, getNotificationTelemetryId } from './types'
1212
import { ToolkitError } from '../shared/errors'
1313
import { isAmazonQ } from '../shared/extensionUtilities'
1414
import { getLogger } from '../shared/logger/logger'
1515
import { registerToolView } from '../awsexplorer/activationShared'
1616
import { readonlyDocument } from '../shared/utilities/textDocumentUtilities'
1717
import { openUrl } from '../shared/utilities/vsCodeUtils'
18+
import { telemetry } from '../shared/telemetry/telemetry'
1819

1920
/**
2021
* Controls the "Notifications" side panel/tree in each extension. It takes purely UX actions
@@ -27,25 +28,24 @@ export class NotificationsNode implements TreeNode {
2728
public startUpNotifications: ToolkitNotification[] = []
2829
public emergencyNotifications: ToolkitNotification[] = []
2930

31+
/** Command executed when a notification item is clicked on in the panel. */
3032
private readonly openNotificationCmd: Command
3133
private readonly focusCmdStr: string
3234
private readonly showContextStr: contextKey
3335
private readonly startUpNodeContext: string
3436
private readonly emergencyNodeContext: string
3537

36-
private readonly onDidChangeTreeItemEmitter = new vscode.EventEmitter<void>()
37-
private readonly onDidChangeChildrenEmitter = new vscode.EventEmitter<void>()
38-
private readonly onDidChangeVisibilityEmitter = new vscode.EventEmitter<void>()
39-
public readonly onDidChangeTreeItem = this.onDidChangeTreeItemEmitter.event
40-
public readonly onDidChangeChildren = this.onDidChangeChildrenEmitter.event
41-
public readonly onDidChangeVisibility = this.onDidChangeVisibilityEmitter.event
42-
4338
static #instance: NotificationsNode
4439

4540
private constructor() {
4641
this.openNotificationCmd = Commands.register(
4742
isAmazonQ() ? '_aws.amazonq.notifications.open' : '_aws.toolkit.notifications.open',
48-
async (n: ToolkitNotification) => this.openNotification(n)
43+
(n: ToolkitNotification) => {
44+
return telemetry.ui_click.run((span) => {
45+
span.record({ elementId: getNotificationTelemetryId(n) })
46+
return this.openNotification(n)
47+
})
48+
}
4949
)
5050

5151
if (isAmazonQ()) {
@@ -73,12 +73,6 @@ export class NotificationsNode implements TreeNode {
7373
const hasNotifications = this.startUpNotifications.length > 0 || this.emergencyNotifications.length > 0
7474
void setContext(this.showContextStr, hasNotifications)
7575

76-
this.onDidChangeChildrenEmitter.fire()
77-
this.provider?.refresh()
78-
}
79-
80-
public refreshRootNode() {
81-
this.onDidChangeTreeItemEmitter.fire()
8276
this.provider?.refresh()
8377
}
8478

@@ -129,28 +123,34 @@ export class NotificationsNode implements TreeNode {
129123
* Fired when a notification is clicked on in the panel. It will run any rendering
130124
* instructions included in the notification. See {@link ToolkitNotification.uiRenderInstructions}.
131125
*/
132-
public async openNotification(notification: ToolkitNotification) {
126+
private async openNotification(notification: ToolkitNotification) {
133127
switch (notification.uiRenderInstructions.onClick.type) {
134128
case 'modal':
135129
// Render blocking modal
136130
getLogger('notifications').verbose(`rendering modal for notificaiton: ${notification.id} ...`)
137-
await this.showInformationWindow(notification, 'modal')
131+
await this.showInformationWindow(notification, 'modal', false)
138132
break
139133
case 'openUrl':
134+
// Show open url option
140135
if (!notification.uiRenderInstructions.onClick.url) {
141136
throw new ToolkitError('No url provided for onclick open url')
142137
}
143-
// Show open url option
144138
getLogger('notifications').verbose(`opening url for notification: ${notification.id} ...`)
145-
await openUrl(vscode.Uri.parse(notification.uiRenderInstructions.onClick.url))
139+
await openUrl(
140+
vscode.Uri.parse(notification.uiRenderInstructions.onClick.url),
141+
getNotificationTelemetryId(notification)
142+
)
146143
break
147144
case 'openTextDocument':
148145
// Display read-only txt document
149146
getLogger('notifications').verbose(`showing txt document for notification: ${notification.id} ...`)
150-
await readonlyDocument.show(
151-
notification.uiRenderInstructions.content['en-US'].description,
152-
`Notification: ${notification.id}`
153-
)
147+
await telemetry.toolkit_invokeAction.run(async () => {
148+
telemetry.record({ source: getNotificationTelemetryId(notification), action: 'openTxt' })
149+
await readonlyDocument.show(
150+
notification.uiRenderInstructions.content['en-US'].description,
151+
`Notification: ${notification.id}`
152+
)
153+
})
154154
break
155155
}
156156
}
@@ -160,57 +160,65 @@ export class NotificationsNode implements TreeNode {
160160
* Can be either a blocking modal or a bottom-right corner toast
161161
* Handles the button click actions based on the button type.
162162
*/
163-
public async showInformationWindow(notification: ToolkitNotification, type: string = 'toast') {
163+
private showInformationWindow(notification: ToolkitNotification, type: string = 'toast', passive: boolean = false) {
164164
const isModal = type === 'modal'
165165

166-
// modal has to have defined actions(buttons)
166+
// modal has to have defined actions (buttons)
167167
const buttons = notification.uiRenderInstructions.actions ?? []
168168
const buttonLabels = buttons.map((actions) => actions.displayText['en-US'])
169169
const detail = notification.uiRenderInstructions.content['en-US'].description
170170

171-
// we use toastPreview to display as titlefor toast, since detail won't be shown
171+
// we use toastPreview to display as title for toast, since detail won't be shown
172172
const title = isModal
173173
? notification.uiRenderInstructions.content['en-US'].title
174174
: (notification.uiRenderInstructions.content['en-US'].toastPreview ??
175175
notification.uiRenderInstructions.content['en-US'].title)
176176

177-
const selectedText = await vscode.window.showInformationMessage(
178-
title,
179-
{ modal: isModal, detail },
180-
...buttonLabels
181-
)
177+
telemetry.toolkit_showNotification.emit({
178+
id: getNotificationTelemetryId(notification),
179+
passive,
180+
component: 'editor',
181+
result: 'Succeeded',
182+
})
182183

183-
if (selectedText) {
184-
const selectedButton = buttons.find((actions) => actions.displayText['en-US'] === selectedText)
185-
// Different button options
186-
if (selectedButton) {
187-
switch (selectedButton.type) {
188-
case 'openTxt':
189-
await readonlyDocument.show(
190-
notification.uiRenderInstructions.content['en-US'].description,
191-
`Notification: ${notification.id}`
192-
)
193-
break
194-
case 'updateAndReload':
195-
await this.updateAndReload(notification.displayIf.extensionId)
196-
break
197-
case 'openUrl':
198-
if (selectedButton.url) {
199-
await openUrl(vscode.Uri.parse(selectedButton.url))
200-
} else {
201-
throw new ToolkitError('url not provided')
184+
return vscode.window
185+
.showInformationMessage(title, { modal: isModal, detail }, ...buttonLabels)
186+
.then((response) => {
187+
return telemetry.toolkit_invokeAction.run(async (span) => {
188+
span.record({ source: getNotificationTelemetryId(notification), action: response ?? 'OK' })
189+
if (response) {
190+
const selectedButton = buttons.find((actions) => actions.displayText['en-US'] === response)
191+
// Different button options
192+
if (selectedButton) {
193+
switch (selectedButton.type) {
194+
case 'openTxt':
195+
await readonlyDocument.show(
196+
notification.uiRenderInstructions.content['en-US'].description,
197+
`Notification: ${notification.id}`
198+
)
199+
break
200+
case 'updateAndReload':
201+
await this.updateAndReload(notification.displayIf.extensionId)
202+
break
203+
case 'openUrl':
204+
if (selectedButton.url) {
205+
await openUrl(vscode.Uri.parse(selectedButton.url))
206+
} else {
207+
throw new ToolkitError('url not provided')
208+
}
209+
break
210+
default:
211+
throw new ToolkitError('button action not defined')
212+
}
202213
}
203-
break
204-
default:
205-
throw new ToolkitError('button action not defined')
206-
}
207-
}
208-
}
214+
}
215+
})
216+
})
209217
}
210218

211219
public async onReceiveNotifications(notifications: ToolkitNotification[]) {
212220
for (const notification of notifications) {
213-
void this.showInformationWindow(notification, notification.uiRenderInstructions.onRecieve)
221+
void this.showInformationWindow(notification, notification.uiRenderInstructions.onRecieve, true)
214222
}
215223
}
216224

packages/core/src/notifications/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,7 @@ export interface RuleContext {
138138

139139
/** Type expected by things that build (or help build) {@link RuleContext} */
140140
export type AuthState = Omit<AuthUserState, 'source'>
141+
142+
export function getNotificationTelemetryId(n: ToolkitNotification): string {
143+
return `TARGETED_NOTIFICATION:${n.id}`
144+
}

packages/core/src/test/notifications/controller.test.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,19 @@ import assert from 'assert'
99
import sinon from 'sinon'
1010
import globals from '../../shared/extensionGlobals'
1111
import { randomUUID } from '../../shared'
12-
import { installFakeClock } from '../testUtil'
12+
import { assertTelemetry, installFakeClock } from '../testUtil'
1313
import {
1414
NotificationFetcher,
1515
NotificationsController,
1616
RemoteFetcher,
1717
ResourceResponse,
1818
} from '../../notifications/controller'
19-
import { NotificationData, NotificationType, ToolkitNotification } from '../../notifications/types'
19+
import {
20+
NotificationData,
21+
NotificationType,
22+
ToolkitNotification,
23+
getNotificationTelemetryId,
24+
} from '../../notifications/types'
2025
import { HttpResourceFetcher } from '../../shared/resourcefetcher/httpResourceFetcher'
2126
import { NotificationsNode } from '../../notifications/panelNode'
2227
import { RuleEngine } from '../../notifications/rules'
@@ -482,6 +487,7 @@ describe('Notifications Controller', function () {
482487

483488
assert.equal(onReceiveSpy.callCount, 1)
484489
assert.deepStrictEqual(onReceiveSpy.args[0][0], [content.notifications[0]])
490+
assertTelemetry('toolkit_showNotification', { id: getNotificationTelemetryId(content.notifications[0]) })
485491

486492
onReceiveSpy.restore()
487493
})

0 commit comments

Comments
 (0)