Skip to content
This repository was archived by the owner on Jan 5, 2026. It is now read-only.

Commit d3d9bf5

Browse files
Added new telemetry events and properties. (#2063)
* Added new telemetry events and properties. * Linting. Co-authored-by: Srinaath Ravichandran <[email protected]>
1 parent 99be493 commit d3d9bf5

15 files changed

Lines changed: 243 additions & 32 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
## Added
1010
- [client/main] Added Ngrok Status Viewer in PR [2032](https://github.com/microsoft/BotFramework-Emulator/pull/2032)
1111
- [client/main] Changed conversation infrastructure to use Web Sockets to communicate with Web Chat in PR [2034](https://github.com/microsoft/BotFramework-Emulator/pull/2034)
12+
- [client/main] Added new telemetry events and properties in PR [2063](https://github.com/microsoft/BotFramework-Emulator/pull/2063)
1213

1314
## Fixed
1415
- [client] Hid services pane by default in PR [2059](https://github.com/microsoft/BotFramework-Emulator/pull/2059)

packages/app/client/src/state/sagas/azureAuthSaga.spec.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,9 @@ describe('The azureAuthSaga', () => {
198198
ct++;
199199
}
200200
expect(ct).toBe(5);
201-
expect(remoteCallSpy).toHaveBeenCalledWith(SharedConstants.Commands.Telemetry.TrackEvent, 'signIn_failure');
201+
expect(remoteCallSpy).toHaveBeenCalledWith(SharedConstants.Commands.Telemetry.TrackEvent, 'azure_signIn', {
202+
success: false,
203+
});
202204
});
203205

204206
it('should contain 6 steps when the Azure login dialog prompt is confirmed and auth succeeds', async () => {
@@ -267,7 +269,10 @@ describe('The azureAuthSaga', () => {
267269
}
268270
expect(ct).toBe(6);
269271
expect(store.getState().azureAuth.access_token).toBe('a valid access_token');
270-
expect(remoteCallSpy).toHaveBeenCalledWith(SharedConstants.Commands.Telemetry.TrackEvent, 'signIn_success');
272+
expect(remoteCallSpy).toHaveBeenCalledWith(SharedConstants.Commands.Telemetry.TrackEvent, 'azure_signIn', {
273+
persistLogin: true,
274+
success: true,
275+
});
271276
});
272277
});
273278
});

packages/app/client/src/state/sagas/azureAuthSaga.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,12 @@ export class AzureAuthSaga {
7474
PersistAzureLoginChanged,
7575
persistLogin
7676
);
77-
AzureAuthSaga.commandService.remoteCall(TrackEvent, 'signIn_success').catch(_e => void 0);
77+
AzureAuthSaga.commandService
78+
.remoteCall(TrackEvent, 'azure_signIn', { persistLogin: !!persistLogin, success: true })
79+
.catch(_e => void 0);
7880
} else {
7981
yield DialogService.showDialog(action.payload.loginFailedDialog);
80-
AzureAuthSaga.commandService.remoteCall(TrackEvent, 'signIn_failure').catch(_e => void 0);
82+
AzureAuthSaga.commandService.remoteCall(TrackEvent, 'azure_signIn', { success: false }).catch(_e => void 0);
8183
}
8284
yield put(azureArmTokenDataChanged(azureAuth.access_token));
8385
return azureAuth;

packages/app/client/src/state/sagas/botSagas.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -168,14 +168,20 @@ export class BotSagas {
168168

169169
// telemetry
170170
if (!action.payload.isFromBotFile) {
171-
BotSagas.commandService.remoteCall(SharedConstants.Commands.Telemetry.TrackEvent, 'bot_open', {
172-
numOfServices: 0,
173-
source: 'url',
174-
});
175-
}
176-
if (!isLocalHostUrl(action.payload.endpoint)) {
177-
BotSagas.commandService.remoteCall(SharedConstants.Commands.Telemetry.TrackEvent, 'livechat_openRemote').catch();
171+
BotSagas.commandService
172+
.remoteCall(SharedConstants.Commands.Telemetry.TrackEvent, 'bot_open', {
173+
numOfServices: 0,
174+
source: 'url',
175+
})
176+
.catch(_ => void 0);
178177
}
178+
BotSagas.commandService
179+
.remoteCall(SharedConstants.Commands.Telemetry.TrackEvent, 'livechat_open', {
180+
isDebug: action.payload.mode === 'debug',
181+
isGov: action.payload.channelService === 'azureusgovernment',
182+
isRemote: !isLocalHostUrl(action.payload.endpoint),
183+
})
184+
.catch(_ => void 0);
179185
}
180186
}
181187

packages/app/client/src/state/sagas/frameworkSettingsSagas.spec.ts

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
3131
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
3232
//
33+
3334
import {
3435
beginAdd,
3536
editor,
@@ -40,14 +41,20 @@ import {
4041
setDirtyFlag,
4142
setFrameworkSettings,
4243
FrameworkActionType,
44+
FrameworkSettings,
4345
SharedConstants,
4446
} from '@bfemulator/app-shared';
4547
import { applyMiddleware, combineReducers, createStore } from 'redux';
4648
import sagaMiddlewareFactory from 'redux-saga';
47-
import { put, select, takeEvery } from 'redux-saga/effects';
49+
import { call, put, select, takeEvery } from 'redux-saga/effects';
4850
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
4951

50-
import { activeDocumentSelector, frameworkSettingsSagas, FrameworkSettingsSagas } from './frameworkSettingsSagas';
52+
import {
53+
activeDocumentSelector,
54+
frameworkSettingsSagas,
55+
FrameworkSettingsSagas,
56+
getFrameworkSettings,
57+
} from './frameworkSettingsSagas';
5158

5259
jest.mock('electron', () => ({
5360
ipcMain: new Proxy(
@@ -101,31 +108,59 @@ describe('The frameworkSettingsSagas', () => {
101108
});
102109

103110
it('should register the expected generators', () => {
104-
const it = frameworkSettingsSagas();
105-
expect(it.next().value).toEqual(
111+
const gen = frameworkSettingsSagas();
112+
expect(gen.next().value).toEqual(
106113
takeEvery(FrameworkActionType.SAVE_FRAMEWORK_SETTINGS, FrameworkSettingsSagas.saveFrameworkSettings)
107114
);
108115
});
109116

110117
it('should save the framework settings', async () => {
111-
const it = FrameworkSettingsSagas.saveFrameworkSettings(saveFrameworkSettingsAction({}));
118+
const currentSettings: Partial<FrameworkSettings> = {
119+
autoUpdate: false,
120+
useCustomId: false,
121+
usePrereleases: false,
122+
userGUID: '',
123+
ngrokPath: 'some/path/to/ngrok',
124+
};
125+
const updatedSettings: Partial<FrameworkSettings> = {
126+
autoUpdate: true,
127+
useCustomId: true,
128+
usePrereleases: false,
129+
userGUID: 'some-user-id',
130+
ngrokPath: 'some/different/path/to/ngrok',
131+
};
132+
const gen = FrameworkSettingsSagas.saveFrameworkSettings(saveFrameworkSettingsAction(updatedSettings));
112133
// selector to get the active document from the state
113-
const selector = it.next().value;
134+
const selector = gen.next().value;
114135
expect(selector).toEqual(select(activeDocumentSelector));
115136
const value = selector.SELECT.selector(mockStore.getState());
116137
// put the dirty state to false
117-
expect(it.next(value).value).toEqual(put(setDirtyFlag(value.documentId, false)));
118-
expect(it.next().value).toEqual(put(setFrameworkSettings({})));
119-
expect(it.next().done).toBe(true);
138+
expect(gen.next(value).value).toEqual(put(setDirtyFlag(value.documentId, false)));
139+
expect(gen.next().value).toEqual(put(setFrameworkSettings(updatedSettings)));
140+
expect(gen.next().value).toEqual(select(getFrameworkSettings));
141+
expect(gen.next(currentSettings).value).toEqual(
142+
call(
143+
[commandService, commandService.remoteCall],
144+
SharedConstants.Commands.Telemetry.TrackEvent,
145+
'app_changeSettings',
146+
{
147+
autoUpdate: true,
148+
useCustomId: true,
149+
userGUID: 'some-user-id',
150+
ngrokPath: 'some/different/path/to/ngrok',
151+
}
152+
)
153+
);
154+
expect(gen.next().done).toBe(true);
120155
});
121156

122157
it('should send a notification when saving the settings fails', () => {
123-
const it = FrameworkSettingsSagas.saveFrameworkSettings(saveFrameworkSettingsAction({}));
124-
it.next();
158+
const gen = FrameworkSettingsSagas.saveFrameworkSettings(saveFrameworkSettingsAction({}));
159+
gen.next();
125160
const errMsg = `Error while saving emulator settings: oh noes!`;
126161
const notification = newNotification(errMsg);
127162
notification.timestamp = jasmine.any(Number) as any;
128163
notification.id = jasmine.any(String) as any;
129-
expect(it.throw('oh noes!').value).toEqual(put(beginAdd(notification)));
164+
expect(gen.throw('oh noes!').value).toEqual(put(beginAdd(notification)));
130165
});
131166
});

packages/app/client/src/state/sagas/frameworkSettingsSagas.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,25 +40,43 @@ import {
4040
FrameworkAction,
4141
FrameworkActionType,
4242
FrameworkSettings,
43+
SharedConstants,
4344
} from '@bfemulator/app-shared';
44-
import { ForkEffect, put, select, takeEvery } from 'redux-saga/effects';
45+
import { ForkEffect, call, put, select, takeEvery } from 'redux-saga/effects';
46+
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
4547

4648
import { RootState } from '../store';
49+
import { getSettingsDelta } from '../../utils';
4750

4851
export const activeDocumentSelector = (state: RootState) => {
4952
const { editors, activeEditor } = state.editor;
5053
const { activeDocumentId } = editors[activeEditor];
5154
return editors[activeEditor].documents[activeDocumentId];
5255
};
5356

57+
export const getFrameworkSettings = (state: RootState): FrameworkSettings => state.framework;
58+
5459
export class FrameworkSettingsSagas {
60+
@CommandServiceInstance()
61+
private static commandService: CommandServiceImpl;
62+
5563
// when saving settings from the settings editor we need to mark the document as clean
5664
// and then set the settings
5765
public static *saveFrameworkSettings(action: FrameworkAction<FrameworkSettings>): IterableIterator<any> {
5866
try {
5967
const activeDoc: Document = yield select(activeDocumentSelector);
6068
yield put(setDirtyFlag(activeDoc.documentId, false)); // mark as clean
6169
yield put(setFrameworkSettings(action.payload));
70+
const currentSettings = yield select(getFrameworkSettings);
71+
const settingsDelta = getSettingsDelta(currentSettings, action.payload);
72+
if (settingsDelta) {
73+
yield call(
74+
[FrameworkSettingsSagas.commandService, FrameworkSettingsSagas.commandService.remoteCall],
75+
SharedConstants.Commands.Telemetry.TrackEvent,
76+
'app_changeSettings',
77+
settingsDelta
78+
);
79+
}
6280
} catch (e) {
6381
const errMsg = `Error while saving emulator settings: ${e}`;
6482
const notification = newNotification(errMsg);
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
//
2+
// Bot Framework Emulator Github:
3+
// https://github.com/Microsoft/BotFramwork-Emulator
4+
//
5+
// Copyright (c) Microsoft Corporation
6+
// All rights reserved.
7+
//
8+
// MIT License:
9+
// Permission is hereby granted, free of charge, to any person obtaining
10+
// a copy of this software and associated documentation files (the
11+
// "Software"), to deal in the Software without restriction, including
12+
// without limitation the rights to use, copy, modify, merge, publish,
13+
// distribute, sublicense, and/or sell copies of the Software, and to
14+
// permit persons to whom the Software is furnished to do so, subject to
15+
// the following conditions:
16+
//
17+
// The above copyright notice and this permission notice shall be
18+
// included in all copies or substantial portions of the Software.
19+
//
20+
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
21+
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22+
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23+
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24+
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25+
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26+
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27+
//
28+
29+
import { FrameworkSettings } from '@bfemulator/app-shared';
30+
31+
import { getSettingsDelta } from './getSettingsDelta';
32+
33+
describe('getSettingsDelta', () => {
34+
it('should return an object containing the delta between 2 settings objects', () => {
35+
const currentSettings: Partial<FrameworkSettings> = {
36+
autoUpdate: true,
37+
locale: 'en-us',
38+
use10Tokens: true,
39+
usePrereleases: true,
40+
userGUID: 'some-id',
41+
};
42+
const updatedSettings: Partial<FrameworkSettings> = {
43+
autoUpdate: true,
44+
runNgrokAtStartup: true,
45+
use10Tokens: false,
46+
usePrereleases: false,
47+
userGUID: 'some-other-id',
48+
};
49+
50+
expect(getSettingsDelta(currentSettings, updatedSettings)).toEqual({
51+
locale: undefined,
52+
runNgrokAtStartup: true,
53+
use10Tokens: false,
54+
usePrereleases: false,
55+
userGUID: 'some-other-id',
56+
});
57+
});
58+
59+
it('should return undefined for settings objects that do not contain a delta', () => {
60+
const currentSettings: Partial<FrameworkSettings> = {
61+
autoUpdate: true,
62+
use10Tokens: true,
63+
usePrereleases: true,
64+
userGUID: 'some-id',
65+
};
66+
const updatedSettings = currentSettings;
67+
68+
expect(getSettingsDelta(currentSettings, updatedSettings)).toBe(undefined);
69+
});
70+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//
2+
// Copyright (c) Microsoft. All rights reserved.
3+
// Licensed under the MIT license.
4+
//
5+
// Microsoft Bot Framework: http://botframework.com
6+
//
7+
// Bot Framework Emulator Github:
8+
// https://github.com/Microsoft/BotFramwork-Emulator
9+
//
10+
// Copyright (c) Microsoft Corporation
11+
// All rights reserved.
12+
//
13+
// MIT License:
14+
// Permission is hereby granted, free of charge, to any person obtaining
15+
// a copy of this software and associated documentation files (the
16+
// "Software"), to deal in the Software without restriction, including
17+
// without limitation the rights to use, copy, modify, merge, publish,
18+
// distribute, sublicense, and/or sell copies of the Software, and to
19+
// permit persons to whom the Software is furnished to do so, subject to
20+
// the following conditions:
21+
//
22+
// The above copyright notice and this permission notice shall be
23+
// included in all copies or substantial portions of the Software.
24+
//
25+
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
26+
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
27+
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
28+
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
29+
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
30+
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
31+
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32+
//
33+
34+
import { FrameworkSettings } from '@bfemulator/app-shared';
35+
36+
export function getSettingsDelta(
37+
prevSettings: FrameworkSettings,
38+
updatedSettings: FrameworkSettings
39+
): Partial<FrameworkSettings> {
40+
const delta: Partial<FrameworkSettings> = {};
41+
// get delta for keys present in updated settings
42+
for (const key in updatedSettings) {
43+
const prevVal = prevSettings[key];
44+
const updatedVal = updatedSettings[key];
45+
if (prevVal !== updatedVal) {
46+
delta[key] = updatedVal;
47+
}
48+
}
49+
// get delta for any keys that were deleted from updated settings
50+
for (const key in prevSettings) {
51+
if (!Object.prototype.hasOwnProperty.call(updatedSettings, key)) {
52+
delta[key] = undefined;
53+
}
54+
}
55+
return Object.keys(delta).length ? delta : undefined;
56+
}

packages/app/client/src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,6 @@
3434
export * from './debounce';
3535
export * from './expandFlatTree';
3636
export * from './getGlobal';
37+
export * from './getSettingsDelta';
3738
export * from './generateBotSecret';
3839
export * from './chatUtils';

packages/app/main/src/commands/ngrokCommands.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { Command } from '@bfemulator/sdk-shared';
3636

3737
import { store } from '../state/store';
3838
import { Emulator } from '../emulator';
39+
import { TelemetryService } from '../telemetry';
3940

4041
const Commands = SharedConstants.Commands.Ngrok;
4142

@@ -48,6 +49,7 @@ export class NgrokCommands {
4849
try {
4950
await emulator.ngrok.recycle();
5051
emulator.ngrok.broadcastNgrokReconnected();
52+
TelemetryService.trackEvent('ngrok_reconnect');
5153
} catch (e) {
5254
throw new Error(`There was an error while trying to reconnect ngrok: ${e}`);
5355
}

0 commit comments

Comments
 (0)