From 2133a73bb1c0d27c7c546304934fc57161899480 Mon Sep 17 00:00:00 2001 From: TJ Durnford Date: Thu, 3 Dec 2020 13:56:58 -1000 Subject: [PATCH] feat: telemetry api (#4968) * feat: telemetry api * lint * persist machine id * move telemetry settings to user settings * reverted changes to en-US * pool telemetry events * changed batch size * fix uuid * add telemetry classes * requested changes * shorted interval * fix integration test * change parameter type Co-authored-by: Andy Brown --- Composer/cypress/integration/ToDoBot.spec.ts | 1 + Composer/cypress/support/index.ts | 10 +- .../components/CreationFlow/index.test.tsx | 1 + .../__tests__/components/appSettings.test.tsx | 1 + Composer/packages/client/src/App.tsx | 6 +- .../components/AppComponents/Assistant.tsx | 6 +- .../src/components/DataCollectionDialog.tsx | 4 +- .../setting/app-settings/AppSettings.tsx | 11 +- .../client/src/recoilModel/atoms/appState.ts | 11 +- .../__tests__/serverSettings.test.tsx | 103 ------------------ .../dispatchers/__tests__/user.test.ts | 1 + .../src/recoilModel/dispatchers/index.ts | 2 - .../dispatchers/serverSettings.tsx | 45 -------- .../src/recoilModel/dispatchers/user.ts | 12 +- .../packages/client/src/recoilModel/types.ts | 3 +- .../client/src/recoilModel/utils/index.ts | 3 +- .../client/src/telemetry/AppInsightsClient.ts | 58 ++++++++++ .../client/src/telemetry/ConsoleClient.ts | 18 +++ .../client/src/telemetry/TelemetryClient.ts | 60 ++++++++++ .../__tests__/AppInsightsClient.test.ts | 71 ++++++++++++ .../__tests__/TelemetryClient.test.ts | 48 ++++++++ .../src/telemetry/useInitializeLogger.ts | 27 +++++ .../__tests__/utility/machineId.test.ts | 44 ++++++++ .../packages/electron-server/package.json | 4 +- Composer/packages/electron-server/src/main.ts | 4 + .../src/utility/getMacAddress.ts | 47 ++++++++ .../electron-server/src/utility/machineId.ts | 36 ++++++ Composer/packages/server/package.json | 1 + Composer/packages/server/src/constants.ts | 4 + .../server/src/controllers/telemetry.ts | 22 ++++ Composer/packages/server/src/router/api.ts | 4 + .../packages/server/src/services/telemetry.ts | 80 ++++++++++++++ .../server/src/utility/electronContext.ts | 1 + Composer/packages/types/src/settings.ts | 5 + Composer/packages/types/src/telemetry.ts | 84 +++++++++++++- Composer/yarn.lock | 32 ++++++ 36 files changed, 681 insertions(+), 189 deletions(-) delete mode 100644 Composer/packages/client/src/recoilModel/dispatchers/__tests__/serverSettings.test.tsx delete mode 100644 Composer/packages/client/src/recoilModel/dispatchers/serverSettings.tsx create mode 100644 Composer/packages/client/src/telemetry/AppInsightsClient.ts create mode 100644 Composer/packages/client/src/telemetry/ConsoleClient.ts create mode 100644 Composer/packages/client/src/telemetry/TelemetryClient.ts create mode 100644 Composer/packages/client/src/telemetry/__tests__/AppInsightsClient.test.ts create mode 100644 Composer/packages/client/src/telemetry/__tests__/TelemetryClient.test.ts create mode 100644 Composer/packages/client/src/telemetry/useInitializeLogger.ts create mode 100644 Composer/packages/electron-server/__tests__/utility/machineId.test.ts create mode 100644 Composer/packages/electron-server/src/utility/getMacAddress.ts create mode 100644 Composer/packages/electron-server/src/utility/machineId.ts create mode 100644 Composer/packages/server/src/controllers/telemetry.ts create mode 100644 Composer/packages/server/src/services/telemetry.ts diff --git a/Composer/cypress/integration/ToDoBot.spec.ts b/Composer/cypress/integration/ToDoBot.spec.ts index 8af24257ca..6e950892aa 100644 --- a/Composer/cypress/integration/ToDoBot.spec.ts +++ b/Composer/cypress/integration/ToDoBot.spec.ts @@ -3,6 +3,7 @@ context('ToDo Bot', () => { before(() => { + window.localStorage.setItem('composer:userSettings', JSON.stringify({ telemetry: { allowDataCollection: false } })); cy.visit('/home'); cy.createBot('TodoSample'); cy.findByTestId('WelcomeModalCloseIcon').click(); diff --git a/Composer/cypress/support/index.ts b/Composer/cypress/support/index.ts index cae920db73..03d376e369 100644 --- a/Composer/cypress/support/index.ts +++ b/Composer/cypress/support/index.ts @@ -1,21 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import axios from 'axios'; - import './commands'; beforeEach(() => { cy.exec('yarn test:integration:clean'); + window.localStorage.setItem('composer:userSettings', JSON.stringify({ telemetry: { allowDataCollection: false } })); window.localStorage.setItem('composer:OnboardingState', JSON.stringify({ complete: true })); window.sessionStorage.setItem('composer:ProjectIdCache', ''); - cy.request('post', '/api/settings', { - settings: { - telemetry: { - allowDataCollection: false, - }, - }, - }); }); after(() => { diff --git a/Composer/packages/client/__tests__/components/CreationFlow/index.test.tsx b/Composer/packages/client/__tests__/components/CreationFlow/index.test.tsx index e1b4d8e322..0c7b4075c1 100644 --- a/Composer/packages/client/__tests__/components/CreationFlow/index.test.tsx +++ b/Composer/packages/client/__tests__/components/CreationFlow/index.test.tsx @@ -30,6 +30,7 @@ describe('', () => { setCreationFlowStatus: jest.fn(), navTo: jest.fn(), saveTemplateId: jest.fn(), + setCurrentPageMode: jest.fn(), }); set(creationFlowStatusState, CreationFlowStatus.NEW_FROM_TEMPLATE); set(featureFlagsState, getDefaultFeatureFlags()); diff --git a/Composer/packages/client/__tests__/components/appSettings.test.tsx b/Composer/packages/client/__tests__/components/appSettings.test.tsx index 36fdff0bbe..1ae60045cc 100644 --- a/Composer/packages/client/__tests__/components/appSettings.test.tsx +++ b/Composer/packages/client/__tests__/components/appSettings.test.tsx @@ -45,6 +45,7 @@ describe(' & ', () => { propertyEditorWidth: 400, dialogNavWidth: 180, appLocale: 'en-US', + telemetry: {}, }); }); getByText('Auto update'); diff --git a/Composer/packages/client/src/App.tsx b/Composer/packages/client/src/App.tsx index deba75ad70..6d71951bb9 100644 --- a/Composer/packages/client/src/App.tsx +++ b/Composer/packages/client/src/App.tsx @@ -11,12 +11,13 @@ import { MainContainer } from './components/AppComponents/MainContainer'; import { userSettingsState } from './recoilModel'; import { loadLocale } from './utils/fileUtil'; import { dispatcherState } from './recoilModel/DispatcherWrapper'; +import { useInitializeLogger } from './telemetry/useInitializeLogger'; initializeIcons(undefined, { disableWarnings: true }); export const App: React.FC = () => { const { appLocale } = useRecoilValue(userSettingsState); - const { fetchExtensions, fetchFeatureFlags, fetchServerSettings } = useRecoilValue(dispatcherState); + const { fetchExtensions, fetchFeatureFlags } = useRecoilValue(dispatcherState); useEffect(() => { loadLocale(appLocale); @@ -25,9 +26,10 @@ export const App: React.FC = () => { useEffect(() => { fetchExtensions(); fetchFeatureFlags(); - fetchServerSettings(); }, []); + useInitializeLogger(); + return ( diff --git a/Composer/packages/client/src/components/AppComponents/Assistant.tsx b/Composer/packages/client/src/components/AppComponents/Assistant.tsx index 52f3a570f2..df922ecfbc 100644 --- a/Composer/packages/client/src/components/AppComponents/Assistant.tsx +++ b/Composer/packages/client/src/components/AppComponents/Assistant.tsx @@ -7,18 +7,18 @@ import { Suspense, Fragment } from 'react'; import React from 'react'; import { isElectron } from './../../utils/electronUtil'; -import { ServerSettingsState, onboardingState } from './../../recoilModel'; +import { userSettingsState, onboardingState } from './../../recoilModel'; const Onboarding = React.lazy(() => import('./../../Onboarding/Onboarding')); const AppUpdater = React.lazy(() => import('./../AppUpdater').then((module) => ({ default: module.AppUpdater }))); const DataCollectionDialog = React.lazy(() => import('./../DataCollectionDialog')); export const Assistant = () => { - const { telemetry } = useRecoilValue(ServerSettingsState); + const { telemetry } = useRecoilValue(userSettingsState); const onboarding = useRecoilValue(onboardingState); const renderAppUpdater = isElectron(); - const renderDataCollectionDialog = typeof telemetry?.allowDataCollection === 'undefined'; + const renderDataCollectionDialog = typeof telemetry.allowDataCollection === 'undefined'; const renderOnboarding = !renderDataCollectionDialog && !onboarding.complete; return ( diff --git a/Composer/packages/client/src/components/DataCollectionDialog.tsx b/Composer/packages/client/src/components/DataCollectionDialog.tsx index fb0f11fc7d..00a6eca482 100644 --- a/Composer/packages/client/src/components/DataCollectionDialog.tsx +++ b/Composer/packages/client/src/components/DataCollectionDialog.tsx @@ -11,10 +11,10 @@ import { useRecoilValue } from 'recoil'; import { dispatcherState } from '../recoilModel'; const DataCollectionDialog: React.FC = () => { - const { updateServerSettings } = useRecoilValue(dispatcherState); + const { updateUserSettings } = useRecoilValue(dispatcherState); const handleDataCollectionChange = (allowDataCollection: boolean) => () => { - updateServerSettings({ + updateUserSettings({ telemetry: { allowDataCollection, }, diff --git a/Composer/packages/client/src/pages/setting/app-settings/AppSettings.tsx b/Composer/packages/client/src/pages/setting/app-settings/AppSettings.tsx index 880a54bb45..421587ebe3 100644 --- a/Composer/packages/client/src/pages/setting/app-settings/AppSettings.tsx +++ b/Composer/packages/client/src/pages/setting/app-settings/AppSettings.tsx @@ -13,7 +13,7 @@ import { RouteComponentProps } from '@reach/router'; import { useRecoilValue } from 'recoil'; import { isElectron } from '../../../utils/electronUtil'; -import { onboardingState, userSettingsState, dispatcherState, ServerSettingsState } from '../../../recoilModel'; +import { onboardingState, userSettingsState, dispatcherState } from '../../../recoilModel'; import { container, section } from './styles'; import { SettingToggle } from './SettingToggle'; @@ -28,8 +28,7 @@ const ElectronSettings = lazy(() => const AppSettings: React.FC = () => { const [calloutIsShown, showCallout] = useState(false); - const { onboardingSetComplete, updateUserSettings, updateServerSettings } = useRecoilValue(dispatcherState); - const { telemetry } = useRecoilValue(ServerSettingsState); + const { onboardingSetComplete, updateUserSettings } = useRecoilValue(dispatcherState); const userSettings = useRecoilValue(userSettingsState); const { complete } = useRecoilValue(onboardingState); const onOnboardingChange = useCallback( @@ -49,8 +48,8 @@ const AppSettings: React.FC = () => { updateUserSettings({ appLocale }); }; - const handleDataCollectionChange = (allowDataCollection) => { - updateServerSettings({ + const handleDataCollectionChange = (allowDataCollection: boolean) => { + updateUserSettings({ telemetry: { allowDataCollection, }, @@ -204,7 +203,7 @@ const AppSettings: React.FC = () => {

{formatMessage('Data Collection')}

({ - key: getFullyQualifiedKey('serverSettings'), - default: { - telemetry: { - allowDataCollection: false, - }, - }, -}); - export const showCreateDialogModalState = atom({ key: getFullyQualifiedKey('showCreateDialogModal'), default: false, diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/serverSettings.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/serverSettings.test.tsx deleted file mode 100644 index 252e911a42..0000000000 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/serverSettings.test.tsx +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { useRecoilValue } from 'recoil'; -import { act } from '@botframework-composer/test-utils/lib/hooks'; - -import { renderRecoilHook } from '../../../../__tests__/testUtils'; -import { ServerSettingsState } from '../../atoms'; -import { dispatcherState } from '../../../recoilModel/DispatcherWrapper'; -import { Dispatcher } from '..'; -import { serverSettingsDispatcher } from '../serverSettings'; -import httpClient from '../../../utils/httpUtil'; - -jest.mock('../../../utils/httpUtil'); - -describe('server setting dispatcher', () => { - let renderedComponent, dispatcher: Dispatcher; - beforeEach(() => { - const useRecoilTestHook = () => { - const serverSettings = useRecoilValue(ServerSettingsState); - const currentDispatcher = useRecoilValue(dispatcherState); - - return { - serverSettings, - currentDispatcher, - }; - }; - - const { result } = renderRecoilHook(useRecoilTestHook, { - states: [{ recoilState: ServerSettingsState, initialValue: {} }], - dispatcher: { - recoilState: dispatcherState, - initialValue: { - serverSettingsDispatcher, - }, - }, - }); - renderedComponent = result; - dispatcher = renderedComponent.current.currentDispatcher; - }); - - it('should set allowDataCollection to false', async () => { - await act(async () => { - await dispatcher.updateServerSettings({ - telemetry: { - allowDataCollection: false, - }, - }); - }); - - expect(renderedComponent.current.serverSettings.telemetry.allowDataCollection).toBe(false); - expect(httpClient.post).toBeCalledWith( - '/settings', - expect.objectContaining({ - settings: { - telemetry: { - allowDataCollection: false, - }, - }, - }) - ); - }); - - it('should set allowDataCollection to true', async () => { - (httpClient.post as jest.Mock).mockResolvedValue({}); - - await act(async () => { - await dispatcher.updateServerSettings({ - telemetry: { - allowDataCollection: true, - }, - }); - }); - - expect(renderedComponent.current.serverSettings.telemetry.allowDataCollection).toBe(true); - expect(httpClient.post).toBeCalledWith( - '/settings', - expect.objectContaining({ - settings: { - telemetry: { - allowDataCollection: true, - }, - }, - }) - ); - }); - - it('should fetch settings from server', async () => { - (httpClient.get as jest.Mock).mockResolvedValue({ - data: { - telemetry: { - allowDataCollection: null, - }, - }, - }); - - await act(async () => { - await dispatcher.fetchServerSettings(); - }); - - expect(renderedComponent.current.serverSettings.telemetry.allowDataCollection).toBe(null); - }); -}); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/user.test.ts b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/user.test.ts index 343cffd0f6..539681346b 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/user.test.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/user.test.ts @@ -159,6 +159,7 @@ describe('user dispatcher', () => { propertyEditorWidth: 400, dialogNavWidth: 555, appLocale: 'en-US', + telemetry: {}, }); }); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/index.ts b/Composer/packages/client/src/recoilModel/dispatchers/index.ts index 55fe4dbbdd..bc9df1d989 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/index.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/index.ts @@ -25,7 +25,6 @@ import { formDialogsDispatcher } from './formDialogs'; import { botProjectFileDispatcher } from './botProjectFile'; import { zoomDispatcher } from './zoom'; import { recognizerDispatcher } from './recognizers'; -import { serverSettingsDispatcher } from './serverSettings'; const createDispatchers = () => { return { @@ -53,7 +52,6 @@ const createDispatchers = () => { ...botProjectFileDispatcher(), ...zoomDispatcher(), ...recognizerDispatcher(), - ...serverSettingsDispatcher(), }; }; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/serverSettings.tsx b/Composer/packages/client/src/recoilModel/dispatchers/serverSettings.tsx deleted file mode 100644 index a9e52995de..0000000000 --- a/Composer/packages/client/src/recoilModel/dispatchers/serverSettings.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/* eslint-disable react-hooks/rules-of-hooks */ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { ServerSettings } from '@bfc/shared'; -import { CallbackInterface, useRecoilCallback } from 'recoil'; -import merge from 'lodash/merge'; - -import httpClient from '../../utils/httpUtil'; -import { ServerSettingsState } from '../atoms/appState'; - -import { logMessage } from './shared'; - -export const serverSettingsDispatcher = () => { - const fetchServerSettings = useRecoilCallback((callbackHelpers: CallbackInterface) => async () => { - const { set } = callbackHelpers; - try { - const { data: settings } = await httpClient.get('/settings'); - - set(ServerSettingsState, settings); - } catch (error) { - logMessage(callbackHelpers, `Error fetching server settings: ${error}`); - } - }); - - const updateServerSettings = useRecoilCallback( - (callbackHelpers: CallbackInterface) => async (partialSettings: Partial) => { - const { set, snapshot } = callbackHelpers; - try { - const currentSettings = await snapshot.getPromise(ServerSettingsState); - const settings = merge({}, currentSettings, partialSettings); - - await httpClient.post('/settings', { settings }); - set(ServerSettingsState, settings); - } catch (error) { - logMessage(callbackHelpers, `Error updating server settings: ${error}`); - } - } - ); - - return { - fetchServerSettings, - updateServerSettings, - }; -}; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/user.ts b/Composer/packages/client/src/recoilModel/dispatchers/user.ts index 257d4235b0..7879dcff89 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/user.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/user.ts @@ -4,6 +4,7 @@ import { CallbackInterface, useRecoilCallback } from 'recoil'; import jwtDecode from 'jwt-decode'; +import pick from 'lodash/pick'; import { userSettingsState, currentUserState, CurrentUser } from '../atoms/appState'; import { getUserTokenFromCache, loginPopup, refreshToken } from '../../utils/auth'; @@ -11,6 +12,9 @@ import storage from '../../utils/storage'; import { loadLocale } from '../../utils/fileUtil'; import { UserSettingsPayload } from '../types'; import { isElectron } from '../../utils/electronUtil'; +import httpClient from '../../utils/httpUtil'; + +import { logMessage } from './shared'; enum ClaimNames { upn = 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn', @@ -72,7 +76,8 @@ export const userDispatcher = () => { }); const updateUserSettings = useRecoilCallback( - ({ set }: CallbackInterface) => async (settings: Partial) => { + (callbackHelpers: CallbackInterface) => async (settings: Partial) => { + const { set } = callbackHelpers; if (settings.appLocale != null) { await loadLocale(settings.appLocale); } @@ -91,6 +96,11 @@ export const userDispatcher = () => { } storage.set('userSettings', newSettings); + // push telemetry settings to the server + httpClient.post('/settings', { settings: pick(newSettings, ['telemetry']) }).catch((error) => { + logMessage(callbackHelpers, `Error updating server settings: ${error}`); + }); + if (isElectron()) { // push the settings to the electron main process window.ipcRenderer.send('update-user-settings', newSettings); diff --git a/Composer/packages/client/src/recoilModel/types.ts b/Composer/packages/client/src/recoilModel/types.ts index 9a680bb020..47fd9cb1ae 100644 --- a/Composer/packages/client/src/recoilModel/types.ts +++ b/Composer/packages/client/src/recoilModel/types.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import { JSONSchema7 } from '@bfc/extension-client'; -import { AppUpdaterSettings, CodeEditorSettings, PromptTab } from '@bfc/shared'; +import { AppUpdaterSettings, CodeEditorSettings, PromptTab, TelemetrySettings } from '@bfc/shared'; import { AppUpdaterStatus } from '../constants'; @@ -98,6 +98,7 @@ export type UserSettingsPayload = { propertyEditorWidth: number; dialogNavWidth: number; appLocale: string; + telemetry: Partial; }; export type BoilerplateVersion = { diff --git a/Composer/packages/client/src/recoilModel/utils/index.ts b/Composer/packages/client/src/recoilModel/utils/index.ts index 6f5d74103f..252d7e147e 100644 --- a/Composer/packages/client/src/recoilModel/utils/index.ts +++ b/Composer/packages/client/src/recoilModel/utils/index.ts @@ -20,10 +20,11 @@ export const DEFAULT_USER_SETTINGS = { propertyEditorWidth: 400, dialogNavWidth: 180, appLocale: 'en-US', + telemetry: {}, }; export const getUserSettings = (): UserSettings => { - const loadedSettings = storage.get('userSettings') || {}; + const loadedSettings = storage.get('userSettings', {}); const settings = merge(DEFAULT_USER_SETTINGS, loadedSettings); if (isElectron()) { diff --git a/Composer/packages/client/src/telemetry/AppInsightsClient.ts b/Composer/packages/client/src/telemetry/AppInsightsClient.ts new file mode 100644 index 0000000000..0c9d903a0e --- /dev/null +++ b/Composer/packages/client/src/telemetry/AppInsightsClient.ts @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { LogData, TelemetryEvent, TelemetryEventTypes } from '@bfc/shared'; +import chunk from 'lodash/chunk'; + +import httpClient from '../utils/httpUtil'; + +const BATCH_SIZE = 20; +export default class AppInsightsClient { + private static _eventPool: TelemetryEvent[] = []; + private static _intervalId: NodeJS.Timeout | null = null; + + public static logEvent(name: string, properties: LogData) { + this.startInterval(); + this._eventPool.push({ type: TelemetryEventTypes.TrackEvent, name, properties }); + if (this._eventPool.length >= BATCH_SIZE) { + this.drain(); + } + } + + public static logPageView(name: string, url: string, properties: LogData) { + this.startInterval(); + this._eventPool.push({ type: TelemetryEventTypes.PageView, name, properties, url }); + if (this._eventPool.length >= BATCH_SIZE) { + this.drain(); + } + } + + public static drain() { + const events = this._eventPool.splice(0, this._eventPool.length); + const batches = chunk(events, BATCH_SIZE); + return Promise.all(batches.map(this.postEvents)); + } + + private static async postEvents(events: TelemetryEvent[]) { + try { + if (events.length) { + await httpClient.post('/telemetry/events', { events }); + } + } catch (error) { + this._eventPool.unshift(...events); + } + } + + private static startInterval() { + if (!this._intervalId) { + this._intervalId = setInterval(() => { + if (this._eventPool.length === 0 && this._intervalId !== null) { + clearInterval(this._intervalId); + this._intervalId = null; + return; + } + this.drain(); + }, 2000); + } + } +} diff --git a/Composer/packages/client/src/telemetry/ConsoleClient.ts b/Composer/packages/client/src/telemetry/ConsoleClient.ts new file mode 100644 index 0000000000..1db887d9b3 --- /dev/null +++ b/Composer/packages/client/src/telemetry/ConsoleClient.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { LogData } from '@botframework-composer/types'; + +export default class ConsoleClient { + public static logEvent(name: string, properties: LogData) { + console.log('bfc-telemetry', { name, properties }); + } + + public static logPageView(name: string, url: string, properties: LogData) { + console.log('bfc-telemetry', { name, url, properties }); + } + + public static drain() { + return; + } +} diff --git a/Composer/packages/client/src/telemetry/TelemetryClient.ts b/Composer/packages/client/src/telemetry/TelemetryClient.ts new file mode 100644 index 0000000000..7561d0ac82 --- /dev/null +++ b/Composer/packages/client/src/telemetry/TelemetryClient.ts @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { LogData, TelemetryEventName, TelemetryEvents, TelemetrySettings } from '@bfc/shared'; + +import AppInsightsClient from './AppInsightsClient'; +import ConsoleClient from './ConsoleClient'; + +export default class TelemetryClient { + private static _additionalProperties?: () => LogData; + private static _telemetrySettings?: TelemetrySettings; + + public static setup(telemetrySettings: TelemetrySettings, additionalProperties: LogData | (() => LogData)) { + if (this._telemetrySettings?.allowDataCollection !== telemetrySettings.allowDataCollection) { + this.client?.drain(); + } + + this._additionalProperties = + typeof additionalProperties === 'function' ? additionalProperties : () => additionalProperties; + this._telemetrySettings = telemetrySettings; + } + + public static log( + eventName: TN, + properties?: TelemetryEvents[TN] extends undefined ? never : TelemetryEvents[TN] + ) { + this.client?.logEvent(eventName, { ...this.sharedProperties, ...properties }); + } + + public static pageView( + eventName: TN, + url: string, + properties?: TelemetryEvents[TN] extends undefined ? never : TelemetryEvents[TN] + ) { + this.client?.logPageView(eventName, url, { ...this.sharedProperties, ...properties }); + } + + public static drain() { + return this.client?.drain(); + } + + private static get client() { + if (this._telemetrySettings?.allowDataCollection) { + if (process.env.NODE_ENV !== 'development') { + return AppInsightsClient; + } else { + return ConsoleClient; + } + } + } + + private static get sharedProperties(): Record { + return { + ...this._additionalProperties?.(), + timestamp: Date.now(), + composerVersion: process.env.COMPOSER_VERSION || 'unknown', + sdkPackageVersion: process.env.SDK_PACKAGE_VERSION || 'unknown', + }; + } +} diff --git a/Composer/packages/client/src/telemetry/__tests__/AppInsightsClient.test.ts b/Composer/packages/client/src/telemetry/__tests__/AppInsightsClient.test.ts new file mode 100644 index 0000000000..ad87d35eee --- /dev/null +++ b/Composer/packages/client/src/telemetry/__tests__/AppInsightsClient.test.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { TelemetryEventTypes } from '@bfc/shared'; + +import httpClient from '../../utils/httpUtil'; +import AppInsightsClient from '../AppInsightsClient'; + +jest.mock('../../utils/httpUtil'); + +describe('Application Insights Logger', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should log event to the server', async () => { + (httpClient.post as jest.Mock).mockResolvedValue({}); + + AppInsightsClient.logEvent('TestEvent', { value: '1' }); + AppInsightsClient.drain(); + + expect(httpClient.post).toBeCalledWith( + '/telemetry/events', + expect.objectContaining({ + events: expect.arrayContaining([ + expect.objectContaining({ + type: TelemetryEventTypes.TrackEvent, + name: 'TestEvent', + properties: { + value: '1', + }, + }), + ]), + }) + ); + }); + + it('should log page views to the server', async () => { + (httpClient.post as jest.Mock).mockResolvedValue({}); + + AppInsightsClient.logPageView('TestEvent', 'https://composer', { value: '1' }); + AppInsightsClient.drain(); + + expect(httpClient.post).toBeCalledWith( + '/telemetry/events', + expect.objectContaining({ + events: expect.arrayContaining([ + expect.objectContaining({ + type: TelemetryEventTypes.PageView, + url: 'https://composer', + name: 'TestEvent', + properties: { + value: '1', + }, + }), + ]), + }) + ); + }); + + it('should drain event pool in batches', async () => { + (httpClient.post as jest.Mock).mockResolvedValue({}); + + for (let i = 0; i < 42; i++) { + AppInsightsClient.logPageView('TestEvent', 'https://composer', { value: '1' }); + } + AppInsightsClient.drain(); + + expect(httpClient.post).toHaveBeenCalledTimes(3); + }); +}); diff --git a/Composer/packages/client/src/telemetry/__tests__/TelemetryClient.test.ts b/Composer/packages/client/src/telemetry/__tests__/TelemetryClient.test.ts new file mode 100644 index 0000000000..8b60ebfcfc --- /dev/null +++ b/Composer/packages/client/src/telemetry/__tests__/TelemetryClient.test.ts @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import TelemetryClient from '../TelemetryClient'; +import AppInsightsClient from '../AppInsightsClient'; + +AppInsightsClient.logEvent = jest.fn(); +AppInsightsClient.logPageView = jest.fn(); + +describe('TelemetryClient', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('adds property to logEvent', () => { + TelemetryClient.setup({ allowDataCollection: true }, { prop1: 'prop1' }); + TelemetryClient.log('ToolbarButtonClicked', { name: 'test' }); + + expect(AppInsightsClient.logEvent).toBeCalledWith( + 'ToolbarButtonClicked', + expect.objectContaining({ + prop1: 'prop1', + name: 'test', + }) + ); + }); + + it('adds property to pageView', () => { + TelemetryClient.setup({ allowDataCollection: true }, { prop1: 'prop1' }); + TelemetryClient.pageView('ToolbarButtonClicked', 'http://composer', { name: 'test' }); + + expect(AppInsightsClient.logPageView).toBeCalledWith( + 'ToolbarButtonClicked', + 'http://composer', + expect.objectContaining({ + prop1: 'prop1', + name: 'test', + }) + ); + }); + + it('does not call AppInsightsClient.logEvent when allowDataCollection is false', () => { + TelemetryClient.setup({ allowDataCollection: false }, { prop1: 'prop1' }); + TelemetryClient.log('ToolbarButtonClicked', { name: 'test' }); + + expect(AppInsightsClient.logEvent).not.toHaveBeenCalled(); + }); +}); diff --git a/Composer/packages/client/src/telemetry/useInitializeLogger.ts b/Composer/packages/client/src/telemetry/useInitializeLogger.ts new file mode 100644 index 0000000000..d7093259c2 --- /dev/null +++ b/Composer/packages/client/src/telemetry/useInitializeLogger.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { useCallback, useEffect } from 'react'; +import { useRecoilValue } from 'recoil'; + +import { currentProjectIdState, userSettingsState } from '../recoilModel'; + +import TelemetryClient from './TelemetryClient'; + +export const useInitializeLogger = () => { + const projectId = useRecoilValue(currentProjectIdState); + const { telemetry } = useRecoilValue(userSettingsState); + + TelemetryClient.setup(telemetry, { projectId }); + + const handleBeforeUnload = useCallback(() => { + TelemetryClient.drain(); + }, []); + + useEffect(() => { + window.addEventListener('beforeunload', handleBeforeUnload); + return () => { + window.removeEventListener('beforeunload', handleBeforeUnload); + }; + }, []); +}; diff --git a/Composer/packages/electron-server/__tests__/utility/machineId.test.ts b/Composer/packages/electron-server/__tests__/utility/machineId.test.ts new file mode 100644 index 0000000000..dc3c647c05 --- /dev/null +++ b/Composer/packages/electron-server/__tests__/utility/machineId.test.ts @@ -0,0 +1,44 @@ +import { getMac, getMacAddress } from '../../src/utility/getMacAddress'; + +import { networkInterfaces } from 'os'; + +jest.mock('os'); + +describe('getMac', () => { + it('returns mac address', async () => { + (networkInterfaces as jest.Mock).mockReturnValue({ + test: [ + { + mac: '11:aa:22:bb:00:00', + }, + ], + }); + expect(getMac()).resolves.toEqual('11:aa:22:bb:00:00'); + }); + + it('throws format error', async () => { + (networkInterfaces as jest.Mock).mockReturnValue({}); + expect(getMac()).rejects.toEqual('Format: Unable to retrieve mac address'); + }); +}); + +describe('getMacAddress', () => { + it('returns mac address', async () => { + (networkInterfaces as jest.Mock).mockReturnValue({ + test: [ + { + mac: '11:aa:22:bb:00:00', + }, + ], + }); + expect(getMacAddress()).resolves.toEqual('11:aa:22:bb:00:00'); + }); + + it('throws timeout error', async () => { + (networkInterfaces as jest.Mock).mockReturnValue({}); + jest.useFakeTimers(); + const macAddress = getMacAddress(); + jest.advanceTimersToNextTimer(10001); + expect(macAddress).rejects.toEqual('Timeout: Unable to retrieve mac address'); + }); +}); diff --git a/Composer/packages/electron-server/package.json b/Composer/packages/electron-server/package.json index 8e3c52a37f..813815581d 100644 --- a/Composer/packages/electron-server/package.json +++ b/Composer/packages/electron-server/package.json @@ -63,6 +63,7 @@ "@bfc/server": "*", "@bfc/shared": "*", "@botframework-composer/types": "*", + "crypto": "^1.0.1", "debug": "4.1.1", "electron-updater": "4.2.5", "fix-path": "^3.0.0", @@ -70,7 +71,8 @@ "format-message-generate-id": "^6.2.3", "fs-extra": "^9.0.0", "lodash": "^4.17.19", - "semver": "7.3.2" + "semver": "7.3.2", + "uuid": "^8.3.1" }, "peerDependencies": { "oneauth-mac": "1.11.0", diff --git a/Composer/packages/electron-server/src/main.ts b/Composer/packages/electron-server/src/main.ts index 5a943b3321..7d971bcec5 100644 --- a/Composer/packages/electron-server/src/main.ts +++ b/Composer/packages/electron-server/src/main.ts @@ -22,6 +22,7 @@ import { getAppLocale, loadLocale, updateAppLocale } from './utility/locale'; import log from './utility/logger'; import { isLinux, isMac, isWindows } from './utility/platform'; import { parseDeepLinkUrl } from './utility/url'; +import { getMachineId } from './utility/machineId'; const env = log.extend('env'); env('%O', process.env); @@ -141,6 +142,8 @@ async function loadServer() { process.env.COMPOSER_FORM_DIALOG_TEMPLATES_DIR = join(unpackedDir, 'form-dialog-templates'); } + const machineId = await getMachineId(); + // only create a new data directory if packaged electron app log('Creating app data directory...'); await createAppDataDir(); @@ -150,6 +153,7 @@ async function loadServer() { const { start } = await import('@bfc/server'); serverPort = await start({ getAccessToken: OneAuthService.getAccessToken.bind(OneAuthService), + machineId, }); log(`Server started at port: ${serverPort}`); } diff --git a/Composer/packages/electron-server/src/utility/getMacAddress.ts b/Composer/packages/electron-server/src/utility/getMacAddress.ts new file mode 100644 index 0000000000..b4e916b1c9 --- /dev/null +++ b/Composer/packages/electron-server/src/utility/getMacAddress.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { networkInterfaces } from 'os'; + +const invalidMacAddresses = new Set(['00:00:00:00:00:00', 'ff:ff:ff:ff:ff:ff', 'ac:de:48:00:11:22']); + +function validateMacAddress(candidate: string): boolean { + const tempCandidate = candidate.replace(/-/g, ':').toLowerCase(); + return !invalidMacAddresses.has(tempCandidate); +} + +export function getMacAddress(): Promise { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject('Timeout: Unable to retrieve mac address'), 10000); + getMac() + .then((macAddress) => { + resolve(macAddress); + clearTimeout(timeout); + }) + .catch((error) => { + reject(error); + }) + .finally(() => { + clearTimeout(timeout); + }); + }); +} + +export function getMac(): Promise { + return new Promise((resolve, reject) => { + try { + const interfaces = networkInterfaces(); + for (const [, infos] of Object.entries(interfaces)) { + for (const { mac } of infos) { + if (validateMacAddress(mac)) { + return resolve(mac); + } + } + } + + reject('Format: Unable to retrieve mac address'); + } catch (err) { + reject(err); + } + }); +} diff --git a/Composer/packages/electron-server/src/utility/machineId.ts b/Composer/packages/electron-server/src/utility/machineId.ts new file mode 100644 index 0000000000..b4bd645423 --- /dev/null +++ b/Composer/packages/electron-server/src/utility/machineId.ts @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import * as path from 'path'; +import crypto from 'crypto'; + +import { app } from 'electron'; +import { existsSync } from 'fs-extra'; +import { v4 as uuid } from 'uuid'; + +import { getMacAddress } from './getMacAddress'; +import { readTextFileSync, writeJsonFileSync } from './fs'; + +export const persistedFilePath = path.join(app.getPath('userData'), 'persisted.json'); + +export async function getMachineId(): Promise { + if (!existsSync(persistedFilePath)) { + const machineId = (await getMacMachineId()) || uuid(); + const telemetrySettings = { machineId }; + + writeJsonFileSync(persistedFilePath, telemetrySettings); + return machineId; + } else { + const raw = readTextFileSync(persistedFilePath); + return JSON.parse(raw).machineId; + } +} + +async function getMacMachineId(): Promise { + try { + const macAddress = await getMacAddress(); + return crypto.createHash('sha256').update(macAddress, 'utf8').digest('hex'); + } catch (err) { + return; + } +} diff --git a/Composer/packages/server/package.json b/Composer/packages/server/package.json index cfae4ffa01..e9b358c93f 100644 --- a/Composer/packages/server/package.json +++ b/Composer/packages/server/package.json @@ -68,6 +68,7 @@ "@microsoft/bf-generate-library": "^4.10.0-daily.20201202.190604", "@microsoft/bf-lu": "^4.11.0-rc.20201030.a9f9b96", "@microsoft/bf-orchestrator": "^4.11.0-beta.20201111.10062c1", + "applicationinsights": "^1.8.7", "archiver": "^5.0.2", "axios": "^0.19.2", "azure-storage": "^2.10.3", diff --git a/Composer/packages/server/src/constants.ts b/Composer/packages/server/src/constants.ts index 9bddac8227..64d34f8005 100644 --- a/Composer/packages/server/src/constants.ts +++ b/Composer/packages/server/src/constants.ts @@ -14,3 +14,7 @@ export enum ClaimNames { name = 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name', expiration = 'exp', } + +export const APPINSIGHTS_INSTRUMENTATIONKEY = process.env.APPINSIGHTS_INSTRUMENTATIONKEY; + +export const piiProperties = ['sessionId', 'userId', 'projectId']; diff --git a/Composer/packages/server/src/controllers/telemetry.ts b/Composer/packages/server/src/controllers/telemetry.ts new file mode 100644 index 0000000000..b5602226e9 --- /dev/null +++ b/Composer/packages/server/src/controllers/telemetry.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Request, Response } from 'express'; + +import { TelemetryService } from '../services/telemetry'; + +async function track(req: Request, res: Response) { + try { + const { events } = req.body; + TelemetryService.track(events); + return res.sendStatus(200); + } catch (err) { + return res.status(500).json({ + message: err instanceof Error ? err.message : err, + }); + } +} + +export const TelemetryController = { + track, +}; diff --git a/Composer/packages/server/src/router/api.ts b/Composer/packages/server/src/router/api.ts index ba1fe38c65..6bf3997e5b 100644 --- a/Composer/packages/server/src/router/api.ts +++ b/Composer/packages/server/src/router/api.ts @@ -18,6 +18,7 @@ import { csrfProtection } from '../middleware/csrfProtection'; import { ImportController } from '../controllers/import'; import { StatusController } from '../controllers/status'; import { SettingsController } from '../controllers/settings'; +import { TelemetryController } from '../controllers/telemetry'; import { UtilitiesController } from './../controllers/utilities'; @@ -112,6 +113,9 @@ router.get('/status/:jobId', StatusController.getStatus); router.get('/settings', SettingsController.getUserSettings); router.post('/settings', SettingsController.updateUserSettings); +// Telemetry +router.post('/telemetry/events', TelemetryController.track); + const errorHandler = (handler: RequestHandler) => (req: Request, res: Response, next: NextFunction) => { Promise.resolve(handler(req, res, next)).catch(next); }; diff --git a/Composer/packages/server/src/services/telemetry.ts b/Composer/packages/server/src/services/telemetry.ts new file mode 100644 index 0000000000..10ab11c9b2 --- /dev/null +++ b/Composer/packages/server/src/services/telemetry.ts @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import * as AppInsights from 'applicationinsights'; +import { TelemetryEventName, TelemetryEvents, TelemetryEventTypes, TelemetryEvent } from '@bfc/shared'; + +import { APPINSIGHTS_INSTRUMENTATIONKEY, piiProperties } from '../constants'; +import { useElectronContext } from '../utility/electronContext'; + +import { SettingsService } from './settings'; + +let client; +if (APPINSIGHTS_INSTRUMENTATIONKEY) { + AppInsights.setup(APPINSIGHTS_INSTRUMENTATIONKEY) + // turn off extra instrumentation + .setAutoCollectConsole(false) + .setAutoCollectDependencies(false) + .setAutoCollectExceptions(false) + .setAutoCollectPerformance(false) + .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 { telemetry: { allowDataCollection } = {} } = SettingsService.getSettings(); + const electionContext = useElectronContext(); + + const data = envelope.data as AppInsights.Contracts.Data; + + if (AppInsights.Contracts.domainSupportsProperties(data.baseData)) { + data.baseData.properties.toolName = 'bf-composer'; + + if (electionContext?.machineId) { + data.baseData.properties.userId = electionContext.machineId; + } + + // remove PII + for (const property of piiProperties) { + if (data.baseData.properties[property]) { + delete data.baseData.properties[property]; + } + } + } + + return !!allowDataCollection; + }); + AppInsights.start(); + client = AppInsights.defaultClient; +} + +const track = (events: TelemetryEvent[]) => { + for (const { type, name, properties = {}, url } of events) { + if (name) { + try { + switch (type) { + case TelemetryEventTypes.TrackEvent: + client?.trackEvent({ name, properties }); + break; + case TelemetryEventTypes.PageView: + client?.trackPageView({ name, url, properties }); + break; + } + } catch (error) { + // swallow the exception on a failed attempt to collect usage data + } + } + } +}; + +const trackEvent = ( + name: TN, + ...args: TelemetryEvents[TN] extends undefined ? [never?] : [TelemetryEvents[TN]] +) => { + const [properties = {}] = args; + track([{ type: TelemetryEventTypes.TrackEvent, name, properties }]); +}; + +export const TelemetryService = { + track, + trackEvent, +}; diff --git a/Composer/packages/server/src/utility/electronContext.ts b/Composer/packages/server/src/utility/electronContext.ts index 26f42be5c5..5da02d52ed 100644 --- a/Composer/packages/server/src/utility/electronContext.ts +++ b/Composer/packages/server/src/utility/electronContext.ts @@ -7,6 +7,7 @@ export type ElectronContext = { getAccessToken: ( params: ElectronAuthParameters ) => Promise<{ accessToken: string; acquiredAt: number; expiryTime: number }>; + machineId: string; }; let context; diff --git a/Composer/packages/types/src/settings.ts b/Composer/packages/types/src/settings.ts index 15e8b9493e..f1f5b39586 100644 --- a/Composer/packages/types/src/settings.ts +++ b/Composer/packages/types/src/settings.ts @@ -15,12 +15,17 @@ export type CodeEditorSettings = { minimap: boolean; }; +export type TelemetrySettings = { + allowDataCollection?: boolean; +}; + export type UserSettings = { appUpdater: AppUpdaterSettings; codeEditor: CodeEditorSettings; propertyEditorWidth: number; dialogNavWidth: number; appLocale: string; + telemetry: TelemetrySettings; }; export type AppUpdaterSettings = { diff --git a/Composer/packages/types/src/telemetry.ts b/Composer/packages/types/src/telemetry.ts index 2a68664b07..0bf23584e3 100644 --- a/Composer/packages/types/src/telemetry.ts +++ b/Composer/packages/types/src/telemetry.ts @@ -1,8 +1,86 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -export type TelemetrySettings = { - allowDataCollection?: boolean; -}; +import { TelemetrySettings } from './settings'; export type ServerSettings = Partial<{ telemetry: TelemetrySettings }>; + +export type LogData = Record; + +export type TelemetryLogger = { + logEvent: (name: string, properties: LogData) => void; + logPageView: (name: string, url: string, properties: LogData) => void; + drain?: () => void; +}; + +export enum TelemetryEventTypes { + TrackEvent = 'TrackEvent', + PageView = 'PageView', +} + +export type TelemetryEvent = { + type: TelemetryEventTypes; + name: string; + url?: string; + properties?: LogData; +}; + +type SessionEvents = { + SessionStarted: { resolution: string; pva: boolean }; + SessionEnded: undefined; + NavigateTo: { sectionName: string }; +}; + +type BotProjectEvents = { + CreateNewBotProjectUsingNewButton: undefined; + CreateNewBotProjectNextButton: undefined; + CreateNewBotProjectFromExample: undefined; + CreateNewBotProjectCompleted: undefined; + BotProjectOpened: undefined; +}; + +type DesignerEvents = { + ActionAdded: { type: string }; + ActionDeleted: { type: string }; + EditModeToggled: undefined; + HelpLinkClicked: { url: string }; + ToolbarButtonClicked: { name: string }; + EmulatorButtonClicked: undefined; + LeftMenuExpanded: undefined; + LeftMenuCollapsed: undefined; + LeftMenuFilterUsed: undefined; + TooltipOpened: { location?: string; title: string }; + NewTriggerStarted: undefined; + NewTriggerCompleted: { kind: string }; + NewDialogAdded: undefined; + AddNewSkillStarted: undefined; + AddNewSkillCompleted: undefined; + UseCustomRuntimeToggle: undefined; + NewTemplateAdded: undefined; +}; + +type QnaEvents = { + NewKnowledgeBaseStarted: undefined; + NewKnowledgeBaseCreated: undefined; + NewQnAPair: undefined; + AlternateQnAPhraseAdded: undefined; + QnAEditModeToggled: undefined; +}; + +type PublishingEvents = { + NewPublishingProfileStarted: undefined; + NewPublishingProfileSaved: undefined; + PublishingProfileStarted: undefined; + PublishingProfileCompleted: undefined; +}; + +type OtherEvents = {}; + +export type TelemetryEvents = BotProjectEvents & + DesignerEvents & + OtherEvents & + SessionEvents & + PublishingEvents & + QnaEvents; + +export type TelemetryEventName = keyof TelemetryEvents; diff --git a/Composer/yarn.lock b/Composer/yarn.lock index 10527761ef..e72c6bf893 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -5110,6 +5110,16 @@ applicationinsights@^1.0.8: diagnostic-channel "0.2.0" diagnostic-channel-publishers "^0.3.3" +applicationinsights@^1.8.7: + version "1.8.7" + resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.8.7.tgz#f98774b2da03fdb95afb9d9042ae9d6f10025db6" + integrity sha512-+HENzPBdSjnWL9mc+9o+j9pEaVNI4WsH5RNvfmRLfwQYvbJumcBi4S5bUzclug5KCcFP0S4bYJOmm9MV3kv2GA== + dependencies: + cls-hooked "^4.2.2" + continuation-local-storage "^3.2.1" + diagnostic-channel "0.3.1" + diagnostic-channel-publishers "0.4.1" + aproba@^1.0.3, aproba@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -7541,6 +7551,11 @@ crypto-random-string@^2.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== +crypto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/crypto/-/crypto-1.0.1.tgz#2af1b7cad8175d24c8a1b0778255794a21803037" + integrity sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig== + css-blank-pseudo@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz#dfdefd3254bf8a82027993674ccf35483bfcb3c5" @@ -8672,6 +8687,11 @@ detect-port-alt@1.1.6: address "^1.0.1" debug "^2.6.0" +diagnostic-channel-publishers@0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.4.1.tgz#a1147ee0d5a4a06cd2b0795433bc1aee1dbc9801" + integrity sha512-NpZ7IOVUfea/kAx4+ub4NIYZyRCSymjXM5BZxnThs3ul9gAKqjm7J8QDDQW3Ecuo2XxjNLoWLeKmrPUWKNZaYw== + diagnostic-channel-publishers@^0.3.3: version "0.3.3" resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.3.3.tgz#376b7798f4fa90f37eb4f94d2caca611b0e9c330" @@ -8684,6 +8704,13 @@ diagnostic-channel@0.2.0: dependencies: semver "^5.3.0" +diagnostic-channel@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/diagnostic-channel/-/diagnostic-channel-0.3.1.tgz#7faa143e107f861be3046539eb4908faab3f53fd" + integrity sha512-6eb9YRrimz8oTr5+JDzGmSYnXy5V7YnK5y/hd8AUDK1MssHjQKm9LlD6NSrHx4vMDF3+e/spI2hmWTviElgWZA== + dependencies: + semver "^5.3.0" + diff-sequences@^25.2.6: version "25.2.6" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.2.6.tgz#5f467c00edd35352b7bca46d7927d60e687a76dd" @@ -20231,6 +20258,11 @@ uuid@^8.3.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea" integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ== +uuid@^8.3.1: + version "8.3.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31" + integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg== + v8-compile-cache@2.0.3: version "2.0.3" resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe"