diff --git a/packages/core/src/notifications/activation.ts b/packages/core/src/notifications/activation.ts index fd1db8b1b72..a563880d46f 100644 --- a/packages/core/src/notifications/activation.ts +++ b/packages/core/src/notifications/activation.ts @@ -10,6 +10,7 @@ import { NotificationsNode } from './panelNode' import { RuleEngine, getRuleContext } from './rules' import globals from '../shared/extensionGlobals' import { AuthState } from './types' +import { getLogger } from '../shared/logger/logger' /** Time in MS to poll for emergency notifications */ const emergencyPollTime = 1000 * 10 * 60 @@ -44,4 +45,6 @@ export async function activate( const ruleContext = await getRuleContext(context, await authStateFn()) await controller.pollForEmergencies(new RuleEngine(ruleContext)) }, emergencyPollTime) + + getLogger('notifications').debug('Activated in-IDE notifications polling module') } diff --git a/packages/core/src/notifications/controller.ts b/packages/core/src/notifications/controller.ts index 04bb05cb8ab..3c820c35da3 100644 --- a/packages/core/src/notifications/controller.ts +++ b/packages/core/src/notifications/controller.ts @@ -7,7 +7,13 @@ import * as vscode from 'vscode' import { ToolkitError } from '../shared/errors' import globals from '../shared/extensionGlobals' import { globalKey } from '../shared/globalState' -import { NotificationsState, NotificationsStateConstructor, NotificationType, ToolkitNotification } from './types' +import { + getNotificationTelemetryId, + NotificationsState, + NotificationsStateConstructor, + NotificationType, + ToolkitNotification, +} from './types' import { HttpResourceFetcher } from '../shared/resourcefetcher/httpResourceFetcher' import { getLogger } from '../shared/logger/logger' import { NotificationsNode } from './panelNode' @@ -17,6 +23,7 @@ import { TreeNode } from '../shared/treeview/resourceTreeDataProvider' import { withRetries } from '../shared/utilities/functionUtils' import { FileResourceFetcher } from '../shared/resourcefetcher/fileResourceFetcher' import { isAmazonQ } from '../shared/extensionUtilities' +import { telemetry } from '../shared/telemetry/telemetry' /** * Handles fetching and maintaining the state of in-IDE notifications. @@ -202,6 +209,10 @@ function registerDismissCommand() { // The command used to build the TreeNode contains the notification as an argument. /** See {@link NotificationsNode} for more info. */ const notification = item.command?.arguments[0] as ToolkitNotification + telemetry.ui_click.emit({ + elementId: `${getNotificationTelemetryId(notification)}:DISMISS`, + result: 'Succeeded', + }) await NotificationsController.instance.dismissNotification(notification.id) } else { diff --git a/packages/core/src/notifications/panelNode.ts b/packages/core/src/notifications/panelNode.ts index 118f7da1706..97b33d58895 100644 --- a/packages/core/src/notifications/panelNode.ts +++ b/packages/core/src/notifications/panelNode.ts @@ -8,13 +8,14 @@ import { ResourceTreeDataProvider, TreeNode } from '../shared/treeview/resourceT import { Command, Commands } from '../shared/vscode/commands2' import { getIcon } from '../shared/icons' import { contextKey, setContext } from '../shared/vscode/setContext' -import { NotificationType, ToolkitNotification } from './types' +import { NotificationType, ToolkitNotification, getNotificationTelemetryId } from './types' import { ToolkitError } from '../shared/errors' import { isAmazonQ } from '../shared/extensionUtilities' import { getLogger } from '../shared/logger/logger' import { registerToolView } from '../awsexplorer/activationShared' import { readonlyDocument } from '../shared/utilities/textDocumentUtilities' import { openUrl } from '../shared/utilities/vsCodeUtils' +import { telemetry } from '../shared/telemetry/telemetry' /** * Controls the "Notifications" side panel/tree in each extension. It takes purely UX actions @@ -27,25 +28,25 @@ export class NotificationsNode implements TreeNode { public startUpNotifications: ToolkitNotification[] = [] public emergencyNotifications: ToolkitNotification[] = [] + /** Command executed when a notification item is clicked on in the panel. */ private readonly openNotificationCmd: Command private readonly focusCmdStr: string private readonly showContextStr: contextKey private readonly startUpNodeContext: string private readonly emergencyNodeContext: string - private readonly onDidChangeTreeItemEmitter = new vscode.EventEmitter() - private readonly onDidChangeChildrenEmitter = new vscode.EventEmitter() - private readonly onDidChangeVisibilityEmitter = new vscode.EventEmitter() - public readonly onDidChangeTreeItem = this.onDidChangeTreeItemEmitter.event - public readonly onDidChangeChildren = this.onDidChangeChildrenEmitter.event - public readonly onDidChangeVisibility = this.onDidChangeVisibilityEmitter.event - static #instance: NotificationsNode private constructor() { this.openNotificationCmd = Commands.register( isAmazonQ() ? '_aws.amazonq.notifications.open' : '_aws.toolkit.notifications.open', - async (n: ToolkitNotification) => this.openNotification(n) + (n: ToolkitNotification) => { + telemetry.ui_click.emit({ + elementId: getNotificationTelemetryId(n), + result: 'Succeeded', + }) + return this.openNotification(n) + } ) if (isAmazonQ()) { @@ -73,12 +74,6 @@ export class NotificationsNode implements TreeNode { const hasNotifications = this.startUpNotifications.length > 0 || this.emergencyNotifications.length > 0 void setContext(this.showContextStr, hasNotifications) - this.onDidChangeChildrenEmitter.fire() - this.provider?.refresh() - } - - public refreshRootNode() { - this.onDidChangeTreeItemEmitter.fire() this.provider?.refresh() } @@ -129,28 +124,34 @@ export class NotificationsNode implements TreeNode { * Fired when a notification is clicked on in the panel. It will run any rendering * instructions included in the notification. See {@link ToolkitNotification.uiRenderInstructions}. */ - public async openNotification(notification: ToolkitNotification) { + private async openNotification(notification: ToolkitNotification) { switch (notification.uiRenderInstructions.onClick.type) { case 'modal': // Render blocking modal getLogger('notifications').verbose(`rendering modal for notificaiton: ${notification.id} ...`) - await this.showInformationWindow(notification, 'modal') + await this.showInformationWindow(notification, 'modal', false) break case 'openUrl': + // Show open url option if (!notification.uiRenderInstructions.onClick.url) { throw new ToolkitError('No url provided for onclick open url') } - // Show open url option getLogger('notifications').verbose(`opening url for notification: ${notification.id} ...`) - await openUrl(vscode.Uri.parse(notification.uiRenderInstructions.onClick.url)) + await openUrl( + vscode.Uri.parse(notification.uiRenderInstructions.onClick.url), + getNotificationTelemetryId(notification) + ) break case 'openTextDocument': // Display read-only txt document getLogger('notifications').verbose(`showing txt document for notification: ${notification.id} ...`) - await readonlyDocument.show( - notification.uiRenderInstructions.content['en-US'].description, - `Notification: ${notification.id}` - ) + await telemetry.toolkit_invokeAction.run(async () => { + telemetry.record({ source: getNotificationTelemetryId(notification), action: 'openTxt' }) + await readonlyDocument.show( + notification.uiRenderInstructions.content['en-US'].description, + `Notification: ${notification.id}` + ) + }) break } } @@ -160,57 +161,65 @@ export class NotificationsNode implements TreeNode { * Can be either a blocking modal or a bottom-right corner toast * Handles the button click actions based on the button type. */ - public async showInformationWindow(notification: ToolkitNotification, type: string = 'toast') { + private showInformationWindow(notification: ToolkitNotification, type: string = 'toast', passive: boolean = false) { const isModal = type === 'modal' - // modal has to have defined actions(buttons) + // modal has to have defined actions (buttons) const buttons = notification.uiRenderInstructions.actions ?? [] const buttonLabels = buttons.map((actions) => actions.displayText['en-US']) const detail = notification.uiRenderInstructions.content['en-US'].description - // we use toastPreview to display as titlefor toast, since detail won't be shown + // we use toastPreview to display as title for toast, since detail won't be shown const title = isModal ? notification.uiRenderInstructions.content['en-US'].title : (notification.uiRenderInstructions.content['en-US'].toastPreview ?? notification.uiRenderInstructions.content['en-US'].title) - const selectedText = await vscode.window.showInformationMessage( - title, - { modal: isModal, detail }, - ...buttonLabels - ) + telemetry.toolkit_showNotification.emit({ + id: getNotificationTelemetryId(notification), + passive, + component: 'editor', + result: 'Succeeded', + }) - if (selectedText) { - const selectedButton = buttons.find((actions) => actions.displayText['en-US'] === selectedText) - // Different button options - if (selectedButton) { - switch (selectedButton.type) { - case 'openTxt': - await readonlyDocument.show( - notification.uiRenderInstructions.content['en-US'].description, - `Notification: ${notification.id}` - ) - break - case 'updateAndReload': - await this.updateAndReload(notification.displayIf.extensionId) - break - case 'openUrl': - if (selectedButton.url) { - await openUrl(vscode.Uri.parse(selectedButton.url)) - } else { - throw new ToolkitError('url not provided') + return vscode.window + .showInformationMessage(title, { modal: isModal, detail }, ...buttonLabels) + .then((response) => { + return telemetry.toolkit_invokeAction.run(async (span) => { + span.record({ source: getNotificationTelemetryId(notification), action: response ?? 'OK' }) + if (response) { + const selectedButton = buttons.find((actions) => actions.displayText['en-US'] === response) + // Different button options + if (selectedButton) { + switch (selectedButton.type) { + case 'openTxt': + await readonlyDocument.show( + notification.uiRenderInstructions.content['en-US'].description, + `Notification: ${notification.id}` + ) + break + case 'updateAndReload': + await this.updateAndReload(notification.displayIf.extensionId) + break + case 'openUrl': + if (selectedButton.url) { + await openUrl(vscode.Uri.parse(selectedButton.url)) + } else { + throw new ToolkitError('url not provided') + } + break + default: + throw new ToolkitError('button action not defined') + } } - break - default: - throw new ToolkitError('button action not defined') - } - } - } + } + }) + }) } public async onReceiveNotifications(notifications: ToolkitNotification[]) { for (const notification of notifications) { - void this.showInformationWindow(notification, notification.uiRenderInstructions.onRecieve) + void this.showInformationWindow(notification, notification.uiRenderInstructions.onRecieve, true) } } diff --git a/packages/core/src/notifications/types.ts b/packages/core/src/notifications/types.ts index 7c3ac798d2f..12e6876e43d 100644 --- a/packages/core/src/notifications/types.ts +++ b/packages/core/src/notifications/types.ts @@ -138,3 +138,7 @@ export interface RuleContext { /** Type expected by things that build (or help build) {@link RuleContext} */ export type AuthState = Omit + +export function getNotificationTelemetryId(n: ToolkitNotification): string { + return `TARGETED_NOTIFICATION:${n.id}` +} diff --git a/packages/core/src/test/notifications/controller.test.ts b/packages/core/src/test/notifications/controller.test.ts index 3a78c94a746..e1ac14094b8 100644 --- a/packages/core/src/test/notifications/controller.test.ts +++ b/packages/core/src/test/notifications/controller.test.ts @@ -9,14 +9,19 @@ import assert from 'assert' import sinon from 'sinon' import globals from '../../shared/extensionGlobals' import { randomUUID } from '../../shared' -import { installFakeClock } from '../testUtil' +import { assertTelemetry, installFakeClock } from '../testUtil' import { NotificationFetcher, NotificationsController, RemoteFetcher, ResourceResponse, } from '../../notifications/controller' -import { NotificationData, NotificationType, ToolkitNotification } from '../../notifications/types' +import { + NotificationData, + NotificationType, + ToolkitNotification, + getNotificationTelemetryId, +} from '../../notifications/types' import { HttpResourceFetcher } from '../../shared/resourcefetcher/httpResourceFetcher' import { NotificationsNode } from '../../notifications/panelNode' import { RuleEngine } from '../../notifications/rules' @@ -482,6 +487,7 @@ describe('Notifications Controller', function () { assert.equal(onReceiveSpy.callCount, 1) assert.deepStrictEqual(onReceiveSpy.args[0][0], [content.notifications[0]]) + assertTelemetry('toolkit_showNotification', { id: getNotificationTelemetryId(content.notifications[0]) }) onReceiveSpy.restore() })