diff --git a/packages/amazonq/src/extension.ts b/packages/amazonq/src/extension.ts index 599aa9264a4..add823b5a9e 100644 --- a/packages/amazonq/src/extension.ts +++ b/packages/amazonq/src/extension.ts @@ -3,20 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { - AuthUtils, - CredentialsStore, - LoginManager, - getTelemetryMetadataForConn, - initializeAuth, - isAnySsoConnection, -} from 'aws-core-vscode/auth' -import { - AuthState, - AuthUtil, - activate as activateCodeWhisperer, - shutdown as shutdownCodeWhisperer, -} from 'aws-core-vscode/codewhisperer' +import { AuthUtils, CredentialsStore, LoginManager, initializeAuth } from 'aws-core-vscode/auth' +import { activate as activateCodeWhisperer, shutdown as shutdownCodeWhisperer } from 'aws-core-vscode/codewhisperer' import { makeEndpointsProvider, registerGenericCommands } from 'aws-core-vscode' import { CommonAuthWebview } from 'aws-core-vscode/login' import { @@ -38,15 +26,13 @@ import { globals, initialize, initializeComputeRegion, - isNetworkError, messages, placeholder, setContext, setupUninstallHandler, maybeShowMinVscodeWarning, - isSageMaker, } from 'aws-core-vscode/shared' -import { ExtStartUpSources, telemetry } from 'aws-core-vscode/telemetry' +import { ExtStartUpSources } from 'aws-core-vscode/telemetry' import { VSCODE_EXTENSION_ID } from 'aws-core-vscode/utils' import { join } from 'path' import * as semver from 'semver' @@ -161,53 +147,6 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is void focusAmazonQPanel.execute(placeholder, 'firstStartUp') }, 1000) } - - await telemetry.auth_userState - .run(async () => { - telemetry.record({ passive: true }) - - const firstUse = AuthUtils.ExtensionUse.instance.isFirstUse() - const wasUpdated = AuthUtils.ExtensionUse.instance.wasUpdated() - - if (firstUse) { - telemetry.record({ source: ExtStartUpSources.firstStartUp }) - } else if (wasUpdated) { - telemetry.record({ source: ExtStartUpSources.update }) - } else { - telemetry.record({ source: ExtStartUpSources.reload }) - } - - let authState: AuthState = 'disconnected' - try { - // May call connection validate functions that try to refresh the token. - // This could result in network errors. - authState = (await AuthUtil.instance.getChatAuthState(false)).codewhispererChat - } catch (err) { - if ( - isNetworkError(err) && - AuthUtil.instance.conn && - AuthUtil.instance.auth.getConnectionState(AuthUtil.instance.conn) === 'valid' - ) { - authState = 'connectedWithNetworkError' - } else { - throw err - } - } - const currConn = AuthUtil.instance.conn - if (currConn !== undefined && !(isAnySsoConnection(currConn) || isSageMaker())) { - getLogger().error(`Current Amazon Q connection is not SSO, type is: %s`, currConn?.type) - } - - telemetry.record({ - authStatus: - authState === 'connected' || authState === 'expired' || authState === 'connectedWithNetworkError' - ? authState - : 'notConnected', - authEnabledConnections: AuthUtils.getAuthFormIdsFromConnection(currConn).join(','), - ...(await getTelemetryMetadataForConn(currConn)), - }) - }) - .catch((err) => getLogger().error('Error collecting telemetry for auth_userState: %s', err)) } export async function deactivateCommon() { diff --git a/packages/amazonq/src/extensionNode.ts b/packages/amazonq/src/extensionNode.ts index d21e9535383..efb118e570c 100644 --- a/packages/amazonq/src/extensionNode.ts +++ b/packages/amazonq/src/extensionNode.ts @@ -7,17 +7,20 @@ import * as vscode from 'vscode' import { activateAmazonQCommon, amazonQContextPrefix, deactivateCommon } from './extension' import { DefaultAmazonQAppInitContext } from 'aws-core-vscode/amazonq' import { activate as activateQGumby } from 'aws-core-vscode/amazonqGumby' -import { ExtContext, globals, CrashMonitoring } from 'aws-core-vscode/shared' +import { ExtContext, globals, CrashMonitoring, getLogger, isNetworkError, isSageMaker } from 'aws-core-vscode/shared' import { filetypes, SchemaService } from 'aws-core-vscode/sharedNode' import { updateDevMode } from 'aws-core-vscode/dev' import { CommonAuthViewProvider } from 'aws-core-vscode/login' import { isExtensionActive, VSCODE_EXTENSION_ID } from 'aws-core-vscode/utils' import { registerSubmitFeedback } from 'aws-core-vscode/feedback' import { DevOptions } from 'aws-core-vscode/dev' -import { Auth } from 'aws-core-vscode/auth' +import { Auth, AuthUtils, getTelemetryMetadataForConn, isAnySsoConnection } from 'aws-core-vscode/auth' import api from './api' import { activate as activateCWChat } from './app/chat/activation' import { beta } from 'aws-core-vscode/dev' +import { activate as activateNotifications } from 'aws-core-vscode/notifications' +import { AuthState, AuthUtil } from 'aws-core-vscode/codewhisperer' +import { telemetry, AuthUserState } from 'aws-core-vscode/telemetry' export async function activate(context: vscode.ExtensionContext) { // IMPORTANT: No other code should be added to this function. Place it in one of the following 2 functions where appropriate. @@ -61,6 +64,50 @@ async function activateAmazonQNode(context: vscode.ExtensionContext) { await setupDevMode(context) await beta.activate(context) + + // TODO: Should probably emit for web as well. + // Will the web metric look the same? + const authState = await getAuthState() + telemetry.auth_userState.emit({ + passive: true, + result: 'Succeeded', + source: AuthUtils.ExtensionUse.instance.sourceForTelemetry(), + ...authState, + }) + + await activateNotifications(context, authState, getAuthState) +} + +async function getAuthState(): Promise> { + let authState: AuthState = 'disconnected' + try { + // May call connection validate functions that try to refresh the token. + // This could result in network errors. + authState = (await AuthUtil.instance.getChatAuthState(false)).codewhispererChat + } catch (err) { + if ( + isNetworkError(err) && + AuthUtil.instance.conn && + AuthUtil.instance.auth.getConnectionState(AuthUtil.instance.conn) === 'valid' + ) { + authState = 'connectedWithNetworkError' + } else { + throw err + } + } + const currConn = AuthUtil.instance.conn + if (currConn !== undefined && !(isAnySsoConnection(currConn) || isSageMaker())) { + getLogger().error(`Current Amazon Q connection is not SSO, type is: %s`, currConn?.type) + } + + return { + authStatus: + authState === 'connected' || authState === 'expired' || authState === 'connectedWithNetworkError' + ? authState + : 'notConnected', + authEnabledConnections: AuthUtils.getAuthFormIdsFromConnection(currConn).join(','), + ...(await getTelemetryMetadataForConn(currConn)), + } } /** diff --git a/packages/core/src/auth/utils.ts b/packages/core/src/auth/utils.ts index 0cd33ef6360..ea26dc2a3c3 100644 --- a/packages/core/src/auth/utils.ts +++ b/packages/core/src/auth/utils.ts @@ -59,6 +59,7 @@ import { EcsCredentialsProvider } from './providers/ecsCredentialsProvider' import { EnvVarsCredentialsProvider } from './providers/envVarsCredentialsProvider' import { showMessageWithUrl } from '../shared/utilities/messages' import { credentialHelpUrl } from '../shared/constants' +import { ExtStartUpSource } from '../shared/telemetry/util' // iam-only excludes Builder ID and IAM Identity Center from the list of valid connections // TODO: Understand if "iam" should include these from the list at all @@ -734,6 +735,19 @@ export class ExtensionUse { return this.wasExtensionUpdated } + /** + * Returns a {@link ExtStartUpSource} based on the current state of the extension. + */ + sourceForTelemetry(): ExtStartUpSource { + if (this.isFirstUse()) { + return ExtStartUpSources.firstStartUp + } else if (this.wasUpdated()) { + return ExtStartUpSources.update + } else { + return ExtStartUpSources.reload + } + } + private updateMemento(key: 'isExtensionFirstUse' | 'lastExtensionVersion', val: any) { globals.globalState.tryUpdate(key, val) } diff --git a/packages/core/src/awsexplorer/activationShared.ts b/packages/core/src/awsexplorer/activationShared.ts index 6a474ccf2e2..9f488a35c36 100644 --- a/packages/core/src/awsexplorer/activationShared.ts +++ b/packages/core/src/awsexplorer/activationShared.ts @@ -27,4 +27,6 @@ export function registerToolView(viewNode: ToolView, context: vscode.ExtensionCo telemetry.cdk_appExpanded.emit() } }) + + return toolView } diff --git a/packages/core/src/extension.ts b/packages/core/src/extension.ts index 778c90969b2..539178823d1 100644 --- a/packages/core/src/extension.ts +++ b/packages/core/src/extension.ts @@ -19,7 +19,7 @@ import { Commands } from './shared/vscode/commands2' import { endpointsFileUrl, githubCreateIssueUrl, githubUrl } from './shared/constants' import { getIdeProperties, aboutExtension, isCloud9, getDocUrl } from './shared/extensionUtilities' import { logAndShowError, logAndShowWebviewError } from './shared/utilities/logAndShowUtils' -import { AuthStatus, telemetry } from './shared/telemetry/telemetry' +import { telemetry } from './shared/telemetry/telemetry' import { openUrl } from './shared/utilities/vsCodeUtils' import { activateViewsShared } from './awsexplorer/activationShared' import fs from './shared/fs/fs' @@ -45,11 +45,6 @@ import { UriHandler } from './shared/vscode/uriHandler' import { disableAwsSdkWarning } from './shared/awsClientBuilder' import { FileResourceFetcher } from './shared/resourcefetcher/fileResourceFetcher' import { ResourceFetcher } from './shared/resourcefetcher/resourcefetcher' -import { ExtStartUpSources } from './shared/telemetry/util' -import { ExtensionUse, getAuthFormIdsFromConnection } from './auth/utils' -import { Auth } from './auth' -import { AuthFormId } from './login/webview/vue/types' -import { getTelemetryMetadataForConn, isSsoConnection } from './auth/connection' import { registerCommands } from './commands' // 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 }) } } - -export async function emitUserState() { - await telemetry.auth_userState.run(async () => { - telemetry.record({ passive: true }) - - const firstUse = ExtensionUse.instance.isFirstUse() - const wasUpdated = ExtensionUse.instance.wasUpdated() - - if (firstUse) { - telemetry.record({ source: ExtStartUpSources.firstStartUp }) - } else if (wasUpdated) { - telemetry.record({ source: ExtStartUpSources.update }) - } else { - telemetry.record({ source: ExtStartUpSources.reload }) - } - - let authStatus: AuthStatus = 'notConnected' - const enabledConnections: Set = new Set() - const enabledScopes: Set = new Set() - if (Auth.instance.hasConnections) { - authStatus = 'expired' - ;(await Auth.instance.listConnections()).forEach((conn) => { - const state = Auth.instance.getConnectionState(conn) - if (state === 'valid') { - authStatus = 'connected' - } - - getAuthFormIdsFromConnection(conn).forEach((id) => enabledConnections.add(id)) - if (isSsoConnection(conn)) { - conn.scopes?.forEach((s) => enabledScopes.add(s)) - } - }) - } - - // There may be other SSO connections in toolkit, but there is no use case for - // displaying registration info for non-active connections at this time. - const activeConn = Auth.instance.activeConnection - if (activeConn?.type === 'sso') { - telemetry.record(await getTelemetryMetadataForConn(activeConn)) - } - - telemetry.record({ - authStatus, - authEnabledConnections: [...enabledConnections].join(','), - authScopes: [...enabledScopes].join(','), - }) - }) -} diff --git a/packages/core/src/extensionNode.ts b/packages/core/src/extensionNode.ts index b9ac4977b22..cef34e522df 100644 --- a/packages/core/src/extensionNode.ts +++ b/packages/core/src/extensionNode.ts @@ -39,24 +39,27 @@ import * as beta from './dev/beta' import { activate as activateApplicationComposer } from './applicationcomposer/activation' import { activate as activateRedshift } from './awsService/redshift/activation' import { activate as activateIamPolicyChecks } from './awsService/accessanalyzer/activation' +import { activate as activateNotifications } from './notifications/activation' import { SchemaService } from './shared/schemas' import { AwsResourceManager } from './dynamicResources/awsResourceManager' import globals from './shared/extensionGlobals' import { Experiments, Settings, showSettingsFailedMsg } from './shared/settings' import { isReleaseVersion } from './shared/vscode/env' -import { telemetry } from './shared/telemetry/telemetry' +import { AuthStatus, AuthUserState, telemetry } from './shared/telemetry/telemetry' import { Auth, SessionSeparationPrompt } from './auth/auth' +import { getTelemetryMetadataForConn } from './auth/connection' import { registerSubmitFeedback } from './feedback/vue/submitFeedback' -import { activateCommon, deactivateCommon, emitUserState } from './extension' +import { activateCommon, deactivateCommon } from './extension' import { learnMoreAmazonQCommand, qExtensionPageCommand, dismissQTree } from './amazonq/explorer/amazonQChildrenNodes' import { AuthUtil, codeWhispererCoreScopes, isPreviousQUser } from './codewhisperer/util/authUtil' import { installAmazonQExtension } from './codewhisperer/commands/basicCommands' import { isExtensionInstalled, VSCODE_EXTENSION_ID } from './shared/utilities' -import { ExtensionUse, initializeCredentialsProviderManager } from './auth/utils' +import { ExtensionUse, getAuthFormIdsFromConnection, initializeCredentialsProviderManager } from './auth/utils' import { ExtStartUpSources } from './shared/telemetry' import { activate as activateThreatComposerEditor } from './threatComposer/activation' import { isSsoConnection, hasScopes } from './auth/connection' import { CrashMonitoring, setContext } from './shared' +import { AuthFormId } from './login/webview/vue/types' let localize: nls.LocalizeFunc @@ -233,7 +236,17 @@ export async function activate(context: vscode.ExtensionContext) { globals.telemetry.assertPassiveTelemetry(globals.didReload) } - await emitUserState() + // TODO: Should probably emit for web as well. + // Will the web metric look the same? + const authState = await getAuthState() + telemetry.auth_userState.emit({ + passive: true, + result: 'Succeeded', + source: ExtensionUse.instance.sourceForTelemetry(), + ...authState, + }) + + await activateNotifications(context, authState, getAuthState) } catch (error) { const stacktrace = (error as Error).stack?.split('\n') // truncate if the stacktrace is unusually long @@ -324,3 +337,36 @@ function recordToolkitInitialization(activationStartedOn: number, settingsValid: logger?.error(err as Error) } } + +async function getAuthState(): Promise> { + let authStatus: AuthStatus = 'notConnected' + const enabledConnections: Set = new Set() + const enabledScopes: Set = new Set() + if (Auth.instance.hasConnections) { + authStatus = 'expired' + ;(await Auth.instance.listConnections()).forEach((conn) => { + const state = Auth.instance.getConnectionState(conn) + if (state === 'valid') { + authStatus = 'connected' + } + + getAuthFormIdsFromConnection(conn).forEach((id) => enabledConnections.add(id)) + if (isSsoConnection(conn)) { + conn.scopes?.forEach((s) => enabledScopes.add(s)) + } + }) + } + + // There may be other SSO connections in toolkit, but there is no use case for + // displaying registration info for non-active connections at this time. + const activeConn = Auth.instance.activeConnection + if (activeConn?.type === 'sso') { + telemetry.record(await getTelemetryMetadataForConn(activeConn)) + } + + return { + authStatus, + authEnabledConnections: [...enabledConnections].sort().join(','), + authScopes: [...enabledScopes].sort().join(','), + } +} diff --git a/packages/core/src/notifications/activation.ts b/packages/core/src/notifications/activation.ts new file mode 100644 index 00000000000..fd1db8b1b72 --- /dev/null +++ b/packages/core/src/notifications/activation.ts @@ -0,0 +1,47 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { DevSettings } from '../shared/settings' +import { NotificationsController } from './controller' +import { NotificationsNode } from './panelNode' +import { RuleEngine, getRuleContext } from './rules' +import globals from '../shared/extensionGlobals' +import { AuthState } from './types' + +/** Time in MS to poll for emergency notifications */ +const emergencyPollTime = 1000 * 10 * 60 + +/** + * Activate the in-IDE notifications module and begin receiving notifications. + * + * @param context extension context + * @param initialState initial auth state + * @param authStateFn fn to get current auth state + */ +export async function activate( + context: vscode.ExtensionContext, + initialState: AuthState, + authStateFn: () => Promise +) { + // TODO: Currently gated behind feature-flag. + if (!DevSettings.instance.get('notifications', false)) { + return + } + + const panelNode = NotificationsNode.instance + panelNode.registerView(context) + + const controller = new NotificationsController(panelNode) + const engine = new RuleEngine(await getRuleContext(context, initialState)) + + void controller.pollForStartUp(engine) + void controller.pollForEmergencies(engine) + + globals.clock.setInterval(async () => { + const ruleContext = await getRuleContext(context, await authStateFn()) + await controller.pollForEmergencies(new RuleEngine(ruleContext)) + }, emergencyPollTime) +} diff --git a/packages/core/src/notifications/index.ts b/packages/core/src/notifications/index.ts index 3b9963f9985..c61d2937d9b 100644 --- a/packages/core/src/notifications/index.ts +++ b/packages/core/src/notifications/index.ts @@ -3,7 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export { RuleContext } from './types' -export { NotificationsController } from './controller' -export { RuleEngine } from './rules' -export { registerProvider, NotificationsNode } from './panelNode' +export { activate } from './activation' diff --git a/packages/core/src/notifications/panelNode.ts b/packages/core/src/notifications/panelNode.ts index fe7b009ef60..70d2a6d3634 100644 --- a/packages/core/src/notifications/panelNode.ts +++ b/packages/core/src/notifications/panelNode.ts @@ -15,6 +15,7 @@ import { getLogger } from '../shared/logger/logger' import { tempDirPath } from '../shared/filesystemUtilities' import path from 'path' import fs from '../shared/fs/fs' +import { registerToolView } from '../awsexplorer/activationShared' /** * Controls the "Notifications" side panel/tree in each extension. It takes purely UX actions @@ -42,9 +43,7 @@ export class NotificationsNode implements TreeNode { static #instance: NotificationsNode - constructor() { - NotificationsNode.#instance = this - + private constructor() { this.openNotificationCmd = Commands.register( isAmazonQ() ? '_aws.amazonq.notifications.open' : '_aws.toolkit.notifications.open', async (n: ToolkitNotification) => this.openNotification(n) @@ -265,13 +264,25 @@ export class NotificationsNode implements TreeNode { static get instance() { if (this.#instance === undefined) { - throw new ToolkitError('NotificationsNode was accessed before it has been initialized.') + this.#instance = new NotificationsNode() } return this.#instance } -} -export function registerProvider(provider: ResourceTreeDataProvider) { - NotificationsNode.instance.provider = provider + registerProvider(provider: ResourceTreeDataProvider) { + this.provider = provider + } + + registerView(context: vscode.ExtensionContext) { + const view = registerToolView( + { + nodes: [this], + view: isAmazonQ() ? 'aws.amazonq.notifications' : 'aws.toolkit.notifications', + refreshCommands: [(provider: ResourceTreeDataProvider) => this.registerProvider(provider)], + }, + context + ) + view.message = `New feature announcements and emergency notifications for ${isAmazonQ() ? 'Amazon Q' : 'AWS Toolkit'} will appear here.` + } } diff --git a/packages/core/src/notifications/rules.ts b/packages/core/src/notifications/rules.ts index 75b607e4f99..ab661c80930 100644 --- a/packages/core/src/notifications/rules.ts +++ b/packages/core/src/notifications/rules.ts @@ -3,9 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ +import * as vscode from 'vscode' import * as semver from 'semver' import globals from '../shared/extensionGlobals' -import { ConditionalClause, RuleContext, DisplayIf, CriteriaCondition, ToolkitNotification } from './types' +import { ConditionalClause, RuleContext, DisplayIf, CriteriaCondition, ToolkitNotification, AuthState } from './types' +import { getComputeEnvType, getOperatingSystem } from '../shared/telemetry/util' /** * Evaluates if a given version fits into the parameters specified by a notification, e.g: @@ -135,3 +137,18 @@ export class RuleEngine { } } } + +export async function getRuleContext(context: vscode.ExtensionContext, authState: AuthState): Promise { + return { + ideVersion: vscode.version, + extensionVersion: context.extension.packageJSON.version, + os: getOperatingSystem(), + computeEnv: await getComputeEnvType(), + authTypes: authState.authEnabledConnections.split(','), + authRegions: authState.awsRegion ? [authState.awsRegion] : [], + authStates: [authState.authStatus], + authScopes: authState.authScopes ? authState.authScopes?.split(',') : [], + installedExtensions: vscode.extensions.all.map((e) => e.id), + activeExtensions: vscode.extensions.all.filter((e) => e.isActive).map((e) => e.id), + } +} diff --git a/packages/core/src/notifications/types.ts b/packages/core/src/notifications/types.ts index ad3ee5571e5..7c3ac798d2f 100644 --- a/packages/core/src/notifications/types.ts +++ b/packages/core/src/notifications/types.ts @@ -6,6 +6,7 @@ import * as vscode from 'vscode' import { EnvType, OperatingSystem } from '../shared/telemetry/util' import { TypeConstructor } from '../shared/utilities/typeConstructors' +import { AuthUserState } from '../shared/telemetry/telemetry.gen' /** Types of information that we can use to determine whether to show a notification or not. */ export type Criteria = @@ -134,3 +135,6 @@ export interface RuleContext { readonly installedExtensions: string[] readonly activeExtensions: string[] } + +/** Type expected by things that build (or help build) {@link RuleContext} */ +export type AuthState = Omit diff --git a/packages/core/src/shared/index.ts b/packages/core/src/shared/index.ts index 1108f0db97a..c1fb3a259b0 100644 --- a/packages/core/src/shared/index.ts +++ b/packages/core/src/shared/index.ts @@ -13,7 +13,7 @@ export { activate as activateLogger } from './logger/activation' export { activate as activateTelemetry } from './telemetry/activation' export { DefaultAwsContext } from './awsContext' export { DefaultAWSClientBuilder, ServiceOptions } from './awsClientBuilder' -export { Settings } from './settings' +export { Settings, DevSettings } from './settings' export * from './extensionUtilities' export * from './extensionStartup' export { RegionProvider } from './regions/regionProvider' diff --git a/packages/core/src/shared/settings.ts b/packages/core/src/shared/settings.ts index a05410e1fff..ede34991225 100644 --- a/packages/core/src/shared/settings.ts +++ b/packages/core/src/shared/settings.ts @@ -761,6 +761,7 @@ const devSettings = { ssoCacheDirectory: String, autofillStartUrl: String, webAuth: Boolean, + notifications: Boolean, } type ResolvedDevSettings = FromDescriptor type AwsDevSetting = keyof ResolvedDevSettings diff --git a/packages/core/src/shared/telemetry/index.ts b/packages/core/src/shared/telemetry/index.ts index 7e2e615303b..a5cc92b4383 100644 --- a/packages/core/src/shared/telemetry/index.ts +++ b/packages/core/src/shared/telemetry/index.ts @@ -5,4 +5,5 @@ export { activate } from './activation' export { telemetry, AuthStatus, Span } from './telemetry' -export { ExtStartUpSources } from './util' +export { ExtStartUpSources, getComputeEnvType } from './util' +export * from './telemetry.gen' diff --git a/packages/core/src/test/notifications/controller.test.ts b/packages/core/src/test/notifications/controller.test.ts index 1b619c56129..3a78c94a746 100644 --- a/packages/core/src/test/notifications/controller.test.ts +++ b/packages/core/src/test/notifications/controller.test.ts @@ -7,17 +7,22 @@ import * as vscode from 'vscode' import * as FakeTimers from '@sinonjs/fake-timers' import assert from 'assert' import sinon from 'sinon' -import { NotificationsController, NotificationsNode, RuleEngine } from '../../notifications' import globals from '../../shared/extensionGlobals' -import { NotificationData, NotificationType, ToolkitNotification } from '../../notifications/types' import { randomUUID } from '../../shared' import { installFakeClock } from '../testUtil' -import { NotificationFetcher, RemoteFetcher, ResourceResponse } from '../../notifications/controller' +import { + NotificationFetcher, + NotificationsController, + RemoteFetcher, + ResourceResponse, +} from '../../notifications/controller' +import { NotificationData, NotificationType, ToolkitNotification } from '../../notifications/types' import { HttpResourceFetcher } from '../../shared/resourcefetcher/httpResourceFetcher' +import { NotificationsNode } from '../../notifications/panelNode' +import { RuleEngine } from '../../notifications/rules' // one test node to use across different tests -// re-declaration would cause a command conflict -export const panelNode: NotificationsNode = new NotificationsNode() +export const panelNode: NotificationsNode = NotificationsNode.instance describe('Notifications Controller', function () { const ruleEngine: RuleEngine = new RuleEngine({