Skip to content

Commit 30aa78e

Browse files
committed
telemetry: add notifications telemetry
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
1 parent ab0664b commit 30aa78e

File tree

5 files changed

+93
-60
lines changed

5 files changed

+93
-60
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 & 1 deletion
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.
@@ -202,6 +209,10 @@ function registerDismissCommand() {
202209
// The command used to build the TreeNode contains the notification as an argument.
203210
/** See {@link NotificationsNode} for more info. */
204211
const notification = item.command?.arguments[0] as ToolkitNotification
212+
telemetry.ui_click.emit({
213+
elementId: `${getNotificationTelemetryId(notification)}:DISMISS`,
214+
result: 'Succeeded',
215+
})
205216

206217
await NotificationsController.instance.dismissNotification(notification.id)
207218
} else {

packages/core/src/notifications/panelNode.ts

Lines changed: 66 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,25 @@ 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+
telemetry.ui_click.emit({
45+
elementId: getNotificationTelemetryId(n),
46+
result: 'Succeeded',
47+
})
48+
return this.openNotification(n)
49+
}
4950
)
5051

5152
if (isAmazonQ()) {
@@ -73,12 +74,6 @@ export class NotificationsNode implements TreeNode {
7374
const hasNotifications = this.startUpNotifications.length > 0 || this.emergencyNotifications.length > 0
7475
void setContext(this.showContextStr, hasNotifications)
7576

76-
this.onDidChangeChildrenEmitter.fire()
77-
this.provider?.refresh()
78-
}
79-
80-
public refreshRootNode() {
81-
this.onDidChangeTreeItemEmitter.fire()
8277
this.provider?.refresh()
8378
}
8479

@@ -129,28 +124,34 @@ export class NotificationsNode implements TreeNode {
129124
* Fired when a notification is clicked on in the panel. It will run any rendering
130125
* instructions included in the notification. See {@link ToolkitNotification.uiRenderInstructions}.
131126
*/
132-
public async openNotification(notification: ToolkitNotification) {
127+
private async openNotification(notification: ToolkitNotification) {
133128
switch (notification.uiRenderInstructions.onClick.type) {
134129
case 'modal':
135130
// Render blocking modal
136131
getLogger('notifications').verbose(`rendering modal for notificaiton: ${notification.id} ...`)
137-
await this.showInformationWindow(notification, 'modal')
132+
await this.showInformationWindow(notification, 'modal', false)
138133
break
139134
case 'openUrl':
135+
// Show open url option
140136
if (!notification.uiRenderInstructions.onClick.url) {
141137
throw new ToolkitError('No url provided for onclick open url')
142138
}
143-
// Show open url option
144139
getLogger('notifications').verbose(`opening url for notification: ${notification.id} ...`)
145-
await openUrl(vscode.Uri.parse(notification.uiRenderInstructions.onClick.url))
140+
await openUrl(
141+
vscode.Uri.parse(notification.uiRenderInstructions.onClick.url),
142+
getNotificationTelemetryId(notification)
143+
)
146144
break
147145
case 'openTextDocument':
148146
// Display read-only txt document
149147
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-
)
148+
await telemetry.toolkit_invokeAction.run(async () => {
149+
telemetry.record({ source: getNotificationTelemetryId(notification), action: 'openTxt' })
150+
await readonlyDocument.show(
151+
notification.uiRenderInstructions.content['en-US'].description,
152+
`Notification: ${notification.id}`
153+
)
154+
})
154155
break
155156
}
156157
}
@@ -160,57 +161,65 @@ export class NotificationsNode implements TreeNode {
160161
* Can be either a blocking modal or a bottom-right corner toast
161162
* Handles the button click actions based on the button type.
162163
*/
163-
public async showInformationWindow(notification: ToolkitNotification, type: string = 'toast') {
164+
private showInformationWindow(notification: ToolkitNotification, type: string = 'toast', passive: boolean = false) {
164165
const isModal = type === 'modal'
165166

166-
// modal has to have defined actions(buttons)
167+
// modal has to have defined actions (buttons)
167168
const buttons = notification.uiRenderInstructions.actions ?? []
168169
const buttonLabels = buttons.map((actions) => actions.displayText['en-US'])
169170
const detail = notification.uiRenderInstructions.content['en-US'].description
170171

171-
// we use toastPreview to display as titlefor toast, since detail won't be shown
172+
// we use toastPreview to display as title for toast, since detail won't be shown
172173
const title = isModal
173174
? notification.uiRenderInstructions.content['en-US'].title
174175
: (notification.uiRenderInstructions.content['en-US'].toastPreview ??
175176
notification.uiRenderInstructions.content['en-US'].title)
176177

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

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

211220
public async onReceiveNotifications(notifications: ToolkitNotification[]) {
212221
for (const notification of notifications) {
213-
void this.showInformationWindow(notification, notification.uiRenderInstructions.onRecieve)
222+
void this.showInformationWindow(notification, notification.uiRenderInstructions.onRecieve, true)
214223
}
215224
}
216225

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)