diff --git a/Composer/packages/client/src/components/TriggerCreationModal/resolveTriggerWidget.tsx b/Composer/packages/client/src/components/TriggerCreationModal/resolveTriggerWidget.tsx index 0c272c1207..a6ee4f15ef 100644 --- a/Composer/packages/client/src/components/TriggerCreationModal/resolveTriggerWidget.tsx +++ b/Composer/packages/client/src/components/TriggerCreationModal/resolveTriggerWidget.tsx @@ -11,6 +11,7 @@ import { LuEditor, inlineModePlaceholder } from '@bfc/code-editor'; import { TriggerFormData, TriggerFormDataErrors } from '../../utils/dialogUtil'; import { isRegExRecognizerType, isLUISnQnARecognizerType, isPVARecognizerType } from '../../utils/dialogValidator'; +import TelemetryClient from '../../telemetry/TelemetryClient'; import { intentStyles } from './styles'; import { validateEventName, validateIntentName, getLuDiagnostics, validateRegExPattern } from './validators'; @@ -108,6 +109,7 @@ export function resolveTriggerWidget( luFeatures: {}, }} placeholder={inlineModePlaceholder} + telemetryClient={TelemetryClient} value={formData.triggerPhrases} onChange={onTriggerPhrasesChange} /> diff --git a/Composer/packages/client/src/pages/knowledge-base/code-editor.tsx b/Composer/packages/client/src/pages/knowledge-base/code-editor.tsx index f61e448c66..8112d9c322 100644 --- a/Composer/packages/client/src/pages/knowledge-base/code-editor.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/code-editor.tsx @@ -110,6 +110,7 @@ const CodeEditor: React.FC = (props) => { path: lspServerPath, }} placeholder={defaultQnAPlaceholder} + telemetryClient={TelemetryClient} value={content} onChange={onChangeContent} onChangeSettings={handleSettingsChange} diff --git a/Composer/packages/client/src/pages/language-understanding/code-editor.tsx b/Composer/packages/client/src/pages/language-understanding/code-editor.tsx index b5f8d343fc..72fd35e5cb 100644 --- a/Composer/packages/client/src/pages/language-understanding/code-editor.tsx +++ b/Composer/packages/client/src/pages/language-understanding/code-editor.tsx @@ -15,6 +15,7 @@ import { LuFile } from '@bfc/shared'; import { localeState, settingsState } from '../../recoilModel/atoms'; import { userSettingsState, dispatcherState, luFilesSelectorFamily } from '../../recoilModel'; +import TelemetryClient from '../../telemetry/TelemetryClient'; import { DiffCodeEditor } from './diff-editor'; @@ -155,6 +156,7 @@ const CodeEditor: React.FC = (props) => { }} luFile={file} luOption={luOption} + telemetryClient={TelemetryClient} value={content} onChange={onChange} onChangeSettings={handleSettingsChange} @@ -174,6 +176,7 @@ const CodeEditor: React.FC = (props) => { options={{ readOnly: true, }} + telemetryClient={TelemetryClient} value={defaultLangContent} onChange={() => {}} /> diff --git a/Composer/packages/client/src/telemetry/AggregateClient.ts b/Composer/packages/client/src/telemetry/AggregateClient.ts new file mode 100644 index 0000000000..fbbd20ad57 --- /dev/null +++ b/Composer/packages/client/src/telemetry/AggregateClient.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export const AggregateClient = (...loggers) => { + const drain = () => { + loggers.forEach((logger) => logger?.drain()); + }; + + const logPageView = (eventName, url, properties) => { + loggers.forEach((logger) => logger.logPageView(eventName, url, properties)); + }; + + const trackEvent = (eventName, properties) => { + loggers.forEach((logger) => logger.trackEvent(eventName, properties)); + }; + + return { + drain, + logPageView, + trackEvent, + }; +}; diff --git a/Composer/packages/client/src/telemetry/TelemetryClient.ts b/Composer/packages/client/src/telemetry/TelemetryClient.ts index 956b0771be..45bfde2c3f 100644 --- a/Composer/packages/client/src/telemetry/TelemetryClient.ts +++ b/Composer/packages/client/src/telemetry/TelemetryClient.ts @@ -3,6 +3,7 @@ import { LogData, TelemetryEventName, TelemetryEvents, TelemetrySettings } from '@bfc/shared'; +import { AggregateClient } from './AggregateClient'; import AppInsightsClient from './AppInsightsClient'; import ConsoleClient from './ConsoleClient'; @@ -44,7 +45,7 @@ export default class TelemetryClient { if (process.env.NODE_ENV !== 'development') { return AppInsightsClient; } else { - return ConsoleClient; + return AggregateClient(AppInsightsClient, ConsoleClient); } } } diff --git a/Composer/packages/client/src/telemetry/useInitializeLogger.ts b/Composer/packages/client/src/telemetry/useInitializeLogger.ts index 0965ca85f9..2bc5af0969 100644 --- a/Composer/packages/client/src/telemetry/useInitializeLogger.ts +++ b/Composer/packages/client/src/telemetry/useInitializeLogger.ts @@ -38,7 +38,12 @@ export const useInitializeLogger = () => { ipcRenderer?.on('session-update', (_event, name) => { switch (name) { case 'session-started': - TelemetryClient.track('SessionStarted', { os: window.navigator.platform }); + TelemetryClient.track('SessionStarted', { + os: window.navigator.platform, + height: screen.height, + width: screen.width, + devicePixelRatio: window.devicePixelRatio, + }); break; case 'session-ended': TelemetryClient.track('SessionEnded'); diff --git a/Composer/packages/electron-server/src/main.ts b/Composer/packages/electron-server/src/main.ts index 553419e4be..962d4ed871 100644 --- a/Composer/packages/electron-server/src/main.ts +++ b/Composer/packages/electron-server/src/main.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import os from 'os'; import { join, resolve } from 'path'; import { AppUpdaterSettings, UserSettings } from '@bfc/shared'; @@ -163,9 +164,14 @@ async function loadServer() { getARMTokenForTenant: OneAuthService.getARMTokenForTenant.bind(OneAuthService), getTenants: OneAuthService.getTenants.bind(OneAuthService), logOut: OneAuthService.signOut.bind(OneAuthService), - machineId, - sessionId, - composerVersion: app.getVersion(), + telemetryData: { + composerVersion: app.getVersion(), + machineId, + sessionId, + architecture: os.arch(), + cpus: os.cpus().length, + memory: os.totalmem(), + }, }); log(`Server started at port: ${serverPort}`); } diff --git a/Composer/packages/lib/code-editor/src/LuEditor.tsx b/Composer/packages/lib/code-editor/src/LuEditor.tsx index 3b327d6f48..cae133ef36 100644 --- a/Composer/packages/lib/code-editor/src/LuEditor.tsx +++ b/Composer/packages/lib/code-editor/src/LuEditor.tsx @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { LuFile } from '@botframework-composer/types'; +import { LuFile, TelemetryClient } from '@botframework-composer/types'; import styled from '@emotion/styled'; import { EditorDidMount, Monaco } from '@monaco-editor/react'; import { FluentTheme, NeutralColors } from '@uifabric/fluent-theme'; @@ -76,6 +76,7 @@ export interface LULSPEditorProps extends BaseEditorProps { } | string; toolbarHidden?: boolean; + telemetryClient: TelemetryClient; onNavigateToLuPage?: (luFileId: string, luSectionId?: string) => void; } @@ -135,6 +136,7 @@ const LuEditor: React.FC = (props) => { onInit: onInitProp, placeholder = defaultPlaceholder, helpURL = LU_HELP, + telemetryClient, ...restProps } = props; const luServer = languageServer || defaultLUServer; @@ -239,7 +241,7 @@ const LuEditor: React.FC = (props) => { if (luEdits?.scrollLine) { editor.revealLineInCenter(luEdits?.scrollLine); } - + telemetryClient.track('LUEditorToolbarEntityDefinitionAdded', { entityType }); editor.focus(); } } @@ -248,11 +250,15 @@ const LuEditor: React.FC = (props) => { ); const insertEntity = useCallback( - (entityName: string) => { + (entityName: string, entityType: string, source: 'toolbar' | 'floatingMenu' = 'toolbar') => { if (editor) { const edits = computeInsertLuEntityEdits(entityName, editor); if (edits) { editor.executeEdits('toolbarMenu', edits); + telemetryClient.track('LUEditorToolbarEntityTagAdded', { + entityType: entityType !== 'prebuilt' ? entityType : entityName, + source, + }); editor.focus(); } } diff --git a/Composer/packages/lib/code-editor/src/lu/InsertEntityButton.tsx b/Composer/packages/lib/code-editor/src/lu/InsertEntityButton.tsx index 714c33175f..2c680ad5d6 100644 --- a/Composer/packages/lib/code-editor/src/lu/InsertEntityButton.tsx +++ b/Composer/packages/lib/code-editor/src/lu/InsertEntityButton.tsx @@ -30,7 +30,7 @@ const buttonStyles = { }; type Props = { - onInsertEntity: (entityName: string) => void; + onInsertEntity: (entityName: string, eventType: string) => void; labelingMenuVisible: boolean; insertEntityDisabled: boolean; tagEntityDisabled: boolean; @@ -47,7 +47,7 @@ export const InsertEntityButton = React.memo((props: Props) => { (_, item?: IContextualMenuItem) => { const entity = item?.data as LuEntity; if (entity) { - onInsertEntity(entity.Name); + onInsertEntity(entity.Name, entity.Type); } }, [onInsertEntity] diff --git a/Composer/packages/lib/code-editor/src/lu/LuEditorToolbar.tsx b/Composer/packages/lib/code-editor/src/lu/LuEditorToolbar.tsx index 3b80248d92..d1ffcaa6b3 100644 --- a/Composer/packages/lib/code-editor/src/lu/LuEditorToolbar.tsx +++ b/Composer/packages/lib/code-editor/src/lu/LuEditorToolbar.tsx @@ -29,7 +29,7 @@ type Props = { luFile?: LuFile; labelingMenuVisible: boolean; onDefineEntity: (entityType: ToolbarLuEntityType, entityName?: string) => void; - onInsertEntity: (entityName: string) => void; + onInsertEntity: (entityName: string, entityType: string) => void; }; export const LuEditorToolbar = React.memo((props: Props) => { diff --git a/Composer/packages/lib/code-editor/src/lu/LuLabelingMenu.tsx b/Composer/packages/lib/code-editor/src/lu/LuLabelingMenu.tsx index ac3849eadd..5ee16f0533 100644 --- a/Composer/packages/lib/code-editor/src/lu/LuLabelingMenu.tsx +++ b/Composer/packages/lib/code-editor/src/lu/LuLabelingMenu.tsx @@ -15,7 +15,7 @@ type Props = { editor: any; luFile?: LuFile; onMenuToggled?: (visible: boolean) => void; - onInsertEntity: (entityName: string) => void; + onInsertEntity: (entityName: string, entityType: string, method: 'toolbar' | 'floatingMenu') => void; }; export const LuLabelingMenu = ({ editor, luFile, onMenuToggled, onInsertEntity }: Props) => { @@ -79,7 +79,7 @@ export const LuLabelingMenu = ({ editor, luFile, onMenuToggled, onInsertEntity } (_, item?: IContextualMenuItem) => { const entity = item?.data as LuEntity; if (entity) { - onInsertEntity(entity.Name); + onInsertEntity(entity.Name, entity.Type, 'floatingMenu'); } setMenuTargetElm(null); }, diff --git a/Composer/packages/server/src/services/telemetry.ts b/Composer/packages/server/src/services/telemetry.ts index eaf9471a86..21f52a2210 100644 --- a/Composer/packages/server/src/services/telemetry.ts +++ b/Composer/packages/server/src/services/telemetry.ts @@ -18,9 +18,11 @@ const getTelemetryContext = () => { const electronContext = useElectronContext(); if (electronContext) { - const { sessionId, machineId, composerVersion } = electronContext; + const { + telemetryData: { composerVersion, sessionId, machineId, architecture, cpus, memory }, + } = electronContext; const { telemetry = {} } = SettingsService.getSettings(); - return { sessionId, userId: machineId, telemetry, composerVersion }; + return { sessionId, userId: machineId, telemetry, composerVersion, architecture, cpus, memory }; } return {}; @@ -38,8 +40,8 @@ if (instrumentationKey) { .setAutoCollectRequests(true); // do not collect the user's machine name AppInsights.defaultClient.context.tags[AppInsights.defaultClient.context.keys.cloudRoleInstance] = ''; - AppInsights.defaultClient.addTelemetryProcessor((envelope: AppInsights.Contracts.Envelope, context): boolean => { - const { sessionId, telemetry, composerVersion, userId } = getTelemetryContext(); + AppInsights.defaultClient.addTelemetryProcessor((envelope: AppInsights.Contracts.Envelope): boolean => { + const { sessionId, telemetry, composerVersion, userId, architecture, cpus, memory } = getTelemetryContext(); if (!telemetry?.allowDataCollection) { return false; @@ -70,6 +72,20 @@ if (instrumentationKey) { data.baseData.properties.composerVersion = composerVersion; } + if (data.baseData.name === 'SessionStarted') { + if (architecture) { + data.baseData.properties.architecture = architecture; + } + + if (cpus !== undefined) { + data.baseData.properties.cpus = cpus; + } + + if (memory !== undefined) { + data.baseData.properties.memory = memory; + } + } + // remove PII for (const property of piiProperties) { if (data.baseData.properties[property] !== undefined) { diff --git a/Composer/packages/server/src/utility/electronContext.ts b/Composer/packages/server/src/utility/electronContext.ts index 1dbdd3da8f..61cdaca2b5 100644 --- a/Composer/packages/server/src/utility/electronContext.ts +++ b/Composer/packages/server/src/utility/electronContext.ts @@ -10,9 +10,14 @@ export type ElectronContext = { getARMTokenForTenant: (tenantId: string) => Promise; getTenants: () => Promise; logOut: () => void; - machineId: string; - sessionId: string; - composerVersion: string; + telemetryData: { + composerVersion: string; + machineId: string; + sessionId: string; + architecture: string; + cpus: number; + memory: number; + }; }; let context; diff --git a/Composer/packages/types/src/telemetry.ts b/Composer/packages/types/src/telemetry.ts index 2e69bf095f..dc0fa1ab10 100644 --- a/Composer/packages/types/src/telemetry.ts +++ b/Composer/packages/types/src/telemetry.ts @@ -59,7 +59,7 @@ type PackageManagerEvents = { }; type SessionEvents = { - SessionStarted: { os: string }; + SessionStarted: { os: string; height: number; width: number; devicePixelRatio: number }; SessionEnded: undefined; NavigateTo: { sectionName: string; url: string }; }; @@ -135,6 +135,11 @@ type LgEditorEvents = { }; }; +type LuEditorEvents = { + LUEditorToolbarEntityTagAdded: { entityType: string; source: 'toolbar' | 'floatingMenu' }; + LUEditorToolbarEntityDefinitionAdded: { entityType: string }; +}; + type WebChatEvents = { WebChatPaneOpened: undefined; WebChatPaneClosed: undefined; @@ -185,7 +190,8 @@ export type TelemetryEvents = ApplicationEvents & AppSettingsEvents & PageView & LgEditorEvents & - WebChatEvents; + WebChatEvents & + LuEditorEvents; export type TelemetryEventName = keyof TelemetryEvents; diff --git a/Composer/packages/ui-plugins/luis/src/LuisIntentEditor.tsx b/Composer/packages/ui-plugins/luis/src/LuisIntentEditor.tsx index da231d65f5..96a27f5486 100644 --- a/Composer/packages/ui-plugins/luis/src/LuisIntentEditor.tsx +++ b/Composer/packages/ui-plugins/luis/src/LuisIntentEditor.tsx @@ -81,6 +81,7 @@ const LuisIntentEditor: React.FC> = (props) => { luFile={luFile} luOption={{ fileId: luFile.id, sectionId: luIntent.Name, projectId, luFeatures }} placeholder={placeholder || inlineModePlaceholder} + telemetryClient={shellApi.telemetryClient} value={luIntent.Body} onChange={commitChanges} onChangeSettings={handleSettingsChange}