Skip to content

Commit aac385c

Browse files
authored
telemetry: add notifications telemetry (#5956)
I explored sending telemetry if the user hides or unhides the notification view panel. However, there is no VSC hook to detect this and other ways (e.g. checking visible property) are not reliable. So, we cannot track this. - toolkit_showNotification - for displaying notifications on receive or when clicked on - toolkit_invokeAction - invoking an action from a notification, e.g. clicking on a button in the notification or opening a text document - ui_click for clicking on the notifications node or dismiss button --- <!--- 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 c6a13a8 commit aac385c

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)