Skip to content

Commit eafbd9e

Browse files
authored
Add telemetry for testing usage (#16913)
* Add telemetry for testing usage * Improve trigger telemetry.
1 parent 0b39090 commit eafbd9e

8 files changed

Lines changed: 79 additions & 83 deletions

File tree

src/client/common/constants.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,8 @@ export const AppinsightsKey = 'AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217';
2323
export type Channel = 'stable' | 'insiders';
2424

2525
export enum CommandSource {
26-
auto = 'auto',
2726
ui = 'ui',
28-
codelens = 'codelens',
2927
commandPalette = 'commandpalette',
30-
testExplorer = 'testExplorer',
3128
}
3229

3330
export namespace Commands {

src/client/telemetry/constants.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -57,17 +57,18 @@ export enum EventName {
5757
DEBUGGER_ATTACH_TO_LOCAL_PROCESS = 'DEBUGGER.ATTACH_TO_LOCAL_PROCESS',
5858
DEBUGGER_CONFIGURATION_PROMPTS = 'DEBUGGER.CONFIGURATION.PROMPTS',
5959
DEBUGGER_CONFIGURATION_PROMPTS_IN_LAUNCH_JSON = 'DEBUGGER.CONFIGURATION.PROMPTS.IN.LAUNCH.JSON',
60-
UNITTEST_STOP = 'UNITTEST.STOP',
61-
UNITTEST_DISABLE = 'UNITTEST.DISABLE',
62-
UNITTEST_RUN = 'UNITTEST.RUN',
63-
UNITTEST_DISCOVER = 'UNITTEST.DISCOVER',
64-
UNITTEST_DISCOVER_WITH_PYCODE = 'UNITTEST.DISCOVER.WITH.PYTHONCODE',
65-
UNITTEST_CONFIGURE = 'UNITTEST.CONFIGURE',
60+
61+
// Python testing specific telemetry
6662
UNITTEST_CONFIGURING = 'UNITTEST.CONFIGURING',
67-
UNITTEST_VIEW_OUTPUT = 'UNITTEST.VIEW_OUTPUT',
68-
UNITTEST_NAVIGATE = 'UNITTEST.NAVIGATE',
69-
UNITTEST_ENABLED = 'UNITTEST.ENABLED',
70-
UNITTEST_EXPLORER_WORK_SPACE_COUNT = 'UNITTEST.TEST_EXPLORER.WORK_SPACE_COUNT',
63+
UNITTEST_CONFIGURE = 'UNITTEST.CONFIGURE',
64+
UNITTEST_DISCOVERY_TRIGGER = 'UNITTEST.DISCOVERY.TRIGGER',
65+
UNITTEST_DISCOVERING = 'UNITTEST.DISCOVERING',
66+
UNITTEST_DISCOVERING_STOP = 'UNITTEST.DISCOVERY.STOP',
67+
UNITTEST_DISCOVERY_DONE = 'UNITTEST.DISCOVERY.DONE',
68+
UNITTEST_RUN_STOP = 'UNITTEST.RUN.STOP',
69+
UNITTEST_RUN = 'UNITTEST.RUN',
70+
UNITTEST_RUN_ALL_FAILED = 'UNITTEST.RUN_ALL_FAILED',
71+
UNITTEST_DISABLED = 'UNITTEST.DISABLED',
7172

7273
PYTHON_EXPERIMENTS_OPT_IN_OPT_OUT_SETTINGS = 'PYTHON_EXPERIMENTS_OPT_IN_OPT_OUT_SETTINGS',
7374
EXTENSION_SURVEY_PROMPT = 'EXTENSION_SURVEY_PROMPT',

src/client/telemetry/index.ts

Lines changed: 32 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import {
2424
TensorBoardSessionStartResult,
2525
TensorBoardEntrypoint,
2626
} from '../tensorBoard/constants';
27-
import { TestProvider } from '../testing/types';
2827
import { EventName, PlatformErrors } from './constants';
2928
import type { LinterTrigger, TestTool } from './types';
3029
import { JupyterNotInstalledOrigin } from '../jupyter/types';
@@ -1507,65 +1506,53 @@ export interface IEventNamePropertyMapping {
15071506
interpreterType?: EnvironmentType;
15081507
};
15091508
/**
1510-
* Telemetry event sent with details about discovering tests
1509+
* Telemetry event sent indicating the trigger source for discovery.
15111510
*/
1512-
[EventName.UNITTEST_DISCOVER]: {
1513-
/**
1514-
* The test framework used to discover tests
1515-
*
1516-
* @type {TestTool}
1517-
*/
1518-
tool: TestTool;
1511+
[EventName.UNITTEST_DISCOVERY_TRIGGER]: {
15191512
/**
15201513
* Carries the source which triggered discovering of tests
15211514
*
1522-
* @type {('ui' | 'commandpalette')}
1515+
* @type {('auto' | 'ui' | 'commandpalette' | 'watching' | 'interpreter')}
1516+
* auto : Triggered by VS Code editor.
1517+
* ui : Triggered by clicking a button.
1518+
* commandpalette : Triggered by running the command from the command palette.
1519+
* watching : Triggered by filesystem or content changes.
1520+
* interpreter : Triggered by interpreter change.
15231521
*/
1524-
trigger: 'ui' | 'commandpalette';
1525-
/**
1526-
* Carries `true` if discovering tests failed, `false` otherwise
1527-
*
1528-
* @type {boolean}
1529-
*/
1530-
failed: boolean;
1522+
trigger: 'auto' | 'ui' | 'commandpalette' | 'watching' | 'interpreter';
15311523
};
15321524
/**
1533-
* Telemetry event is sent if we are doing test discovery using python code
1534-
*/
1535-
[EventName.UNITTEST_DISCOVER_WITH_PYCODE]: never | undefined;
1536-
/**
1537-
* Telemetry event sent when user clicks a file, function, or suite in test explorer.
1525+
* Telemetry event sent with details about discovering tests
15381526
*/
1539-
[EventName.UNITTEST_NAVIGATE]: {
1527+
[EventName.UNITTEST_DISCOVERING]: {
15401528
/**
1541-
* Carries `true` if user clicks a file, `false` otherwise
1542-
*
1543-
* @type {boolean}
1544-
*/
1545-
byFile?: boolean;
1546-
/**
1547-
* Carries `true` if user clicks a function, `false` otherwise
1529+
* The test framework used to discover tests
15481530
*
1549-
* @type {boolean}
1531+
* @type {TestTool}
15501532
*/
1551-
byFunction?: boolean;
1533+
tool: TestTool;
1534+
};
1535+
/**
1536+
* Telemetry event sent with details about discovering tests
1537+
*/
1538+
[EventName.UNITTEST_DISCOVERY_DONE]: {
15521539
/**
1553-
* Carries `true` if user clicks a suite, `false` otherwise
1540+
* The test framework used to discover tests
15541541
*
1555-
* @type {boolean}
1542+
* @type {TestTool}
15561543
*/
1557-
bySuite?: boolean;
1544+
tool: TestTool;
15581545
/**
1559-
* Carries `true` if we are changing focus to the suite/file/function, `false` otherwise
1546+
* Carries `true` if discovering tests failed, `false` otherwise
15601547
*
15611548
* @type {boolean}
15621549
*/
1563-
focusCode?: boolean;
1550+
failed: boolean;
15641551
};
15651552
/**
1566-
* Tracks number of workspace folders shown in test explorer
1553+
* Telemetry event sent when cancelling discovering tests
15671554
*/
1568-
[EventName.UNITTEST_EXPLORER_WORK_SPACE_COUNT]: { count: number };
1555+
[EventName.UNITTEST_DISCOVERING_STOP]: never | undefined;
15691556
/**
15701557
* Telemetry event sent with details about running the tests, what is being run, what framework is being used etc.
15711558
*/
@@ -1574,45 +1561,23 @@ export interface IEventNamePropertyMapping {
15741561
* Framework being used to run tests
15751562
*/
15761563
tool: TestTool;
1577-
/**
1578-
* Carries info what is being run
1579-
*/
1580-
scope: 'currentFile' | 'all' | 'file' | 'class' | 'function' | 'failed';
15811564
/**
15821565
* Carries `true` if debugging, `false` otherwise
15831566
*/
15841567
debugging: boolean;
1585-
/**
1586-
* Carries what triggered the execution of the tests
1587-
*/
1588-
triggerSource: 'ui' | 'codelens' | 'commandpalette' | 'auto' | 'testExplorer';
1589-
/**
1590-
* Carries `true` if running tests failed, `false` otherwise
1591-
*/
1592-
failed: boolean;
15931568
};
15941569
/**
1595-
* Telemetry event sent when cancelling running or discovering tests
1570+
* Telemetry event sent when cancelling running tests
15961571
*/
1597-
[EventName.UNITTEST_STOP]: never | undefined;
1572+
[EventName.UNITTEST_RUN_STOP]: never | undefined;
15981573
/**
1599-
* Telemetry event sent when disabling all test frameworks
1574+
* Telemetry event sent when run all failed test command is triggered
16001575
*/
1601-
[EventName.UNITTEST_DISABLE]: never | undefined;
1576+
[EventName.UNITTEST_RUN_ALL_FAILED]: never | undefined;
16021577
/**
1603-
* Telemetry event sent when viewing Python test log output
1604-
*/
1605-
[EventName.UNITTEST_VIEW_OUTPUT]: never | undefined;
1606-
/**
1607-
* Tracks which testing framework has been enabled by the user.
1608-
* Telemetry is sent when settings have been modified by the user.
1609-
* Values sent include:
1610-
* unittest - If this value is `true`, then unittest has been enabled by the user.
1611-
* pytest - If this value is `true`, then pytest has been enabled by the user.
1612-
* @type {(never | undefined)}
1613-
* @memberof IEventNamePropertyMapping
1578+
* Telemetry event sent when testing is disabled for a workspace.
16141579
*/
1615-
[EventName.UNITTEST_ENABLED]: Partial<Record<TestProvider, undefined | boolean>>;
1580+
[EventName.UNITTEST_DISABLED]: never | undefined;
16161581
/**
16171582
* Telemetry sent when building workspace symbols
16181583
*/

src/client/telemetry/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export type PythonInterpreterTelemetry = IEventNamePropertyMapping[EventName.PYT
1616
export type DebuggerTelemetry = IEventNamePropertyMapping[EventName.DEBUGGER];
1717
export type TestTool = 'pytest' | 'unittest';
1818
export type TestRunTelemetry = IEventNamePropertyMapping[EventName.UNITTEST_RUN];
19-
export type TestDiscoveryTelemetry = IEventNamePropertyMapping[EventName.UNITTEST_DISCOVER];
19+
export type TestDiscoveryTelemetry = IEventNamePropertyMapping[EventName.UNITTEST_DISCOVERY_DONE];
2020
export type TestConfiguringTelemetry = IEventNamePropertyMapping[EventName.UNITTEST_CONFIGURING];
2121
export const IImportTracker = Symbol('IImportTracker');
2222
export interface IImportTracker {}

src/client/testing/main.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { IDisposableRegistry, IExperimentService, Product } from '../common/type
99
import { IInterpreterService } from '../interpreter/contracts';
1010
import { IServiceContainer } from '../ioc/types';
1111
import { EventName } from '../telemetry/constants';
12-
import { captureTelemetry } from '../telemetry/index';
12+
import { captureTelemetry, sendTelemetryEvent } from '../telemetry/index';
1313
import { selectTestWorkspace } from './common/testUtils';
1414
import { TestSettingsPropertyNames } from './configuration/types';
1515
import { ITestConfigurationService, ITestsHelper } from './common/types';
@@ -155,10 +155,13 @@ export class UnitTestManagementService implements IExtensionActivationService {
155155
constants.Commands.Test_Refresh,
156156
async (
157157
_,
158-
_cmdSource: constants.CommandSource = constants.CommandSource.commandPalette,
158+
cmdSource: constants.CommandSource = constants.CommandSource.commandPalette,
159159
resource?: Uri,
160160
) => {
161161
traceVerbose('Testing: Manually triggered test refresh');
162+
sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_TRIGGER, undefined, {
163+
trigger: cmdSource,
164+
});
162165
this.testController?.refreshTestData(resource, { forceRefresh: true });
163166
},
164167
),
@@ -168,6 +171,7 @@ export class UnitTestManagementService implements IExtensionActivationService {
168171
}),
169172
commandManager.registerCommand(constants.Commands.Test_Stop_Refreshing, () => {
170173
traceVerbose('Testing: Stop refreshing clicked.');
174+
sendTelemetryEvent(EventName.UNITTEST_DISCOVERING_STOP);
171175
this.testController?.stopRefreshing();
172176
}),
173177
);
@@ -181,6 +185,7 @@ export class UnitTestManagementService implements IExtensionActivationService {
181185
}),
182186
interpreterService.onDidChangeInterpreter(async () => {
183187
traceVerbose('Testing: Triggered refresh due to interpreter change.');
188+
sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_TRIGGER, undefined, { trigger: 'interpreter' });
184189
await this.testController?.refreshTestData(undefined, { forceRefresh: true });
185190
}),
186191
);

src/client/testing/testController/controller.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import { IWorkspaceService } from '../../common/application/types';
2020
import { traceVerbose } from '../../common/logger';
2121
import { IConfigurationService, IDisposableRegistry, Resource } from '../../common/types';
2222
import { DelayedTrigger, IDelayedTrigger } from '../../common/utils/delayTrigger';
23+
import { sendTelemetryEvent } from '../../telemetry';
24+
import { EventName } from '../../telemetry/constants';
2325
import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../common/constants';
2426
import { getNodeByUri } from './common/testItemUtilities';
2527
import { ITestController, ITestFrameworkController, TestRefreshOptions } from './common/types';
@@ -119,6 +121,7 @@ export class PythonTestController implements ITestController {
119121
} else if (settings.testing.unittestEnabled) {
120122
await this.unittest.refreshTestData(this.testController, uri, this.refreshCancellation.token);
121123
} else {
124+
sendTelemetryEvent(EventName.UNITTEST_DISABLED);
122125
// If we are here we may have to remove an existing node from the tree
123126
// This handles the case where user removes test settings. Which should remove the
124127
// tests for that particular case from the tree view
@@ -155,6 +158,7 @@ export class PythonTestController implements ITestController {
155158
}
156159
} else {
157160
traceVerbose('Testing: Refreshing all test data');
161+
sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_TRIGGER, undefined, { trigger: 'auto' });
158162
const workspaces: readonly WorkspaceFolder[] = this.workspaceService.workspaceFolders || [];
159163
await Promise.all(workspaces.map((workspace) => this.refreshTestDataInternal(workspace.uri)));
160164
}
@@ -198,6 +202,10 @@ export class PythonTestController implements ITestController {
198202
if (testItems.length > 0) {
199203
const settings = this.configSettings.getSettings(workspace.uri);
200204
if (settings.testing.pytestEnabled) {
205+
sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, {
206+
tool: 'pytest',
207+
debugging: request.profile?.kind === TestRunProfileKind.Debug,
208+
});
201209
return this.pytest.runTests(
202210
{
203211
includes: testItems,
@@ -210,6 +218,10 @@ export class PythonTestController implements ITestController {
210218
);
211219
}
212220
if (settings.testing.unittestEnabled) {
221+
sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, {
222+
tool: 'unittest',
223+
debugging: request.profile?.kind === TestRunProfileKind.Debug,
224+
});
213225
return this.unittest.runTests(
214226
{
215227
includes: testItems,
@@ -264,18 +276,21 @@ export class PythonTestController implements ITestController {
264276
this.disposables.push(
265277
watcher.onDidChange((uri) => {
266278
traceVerbose(`Testing: Trigger refresh after change in ${uri.fsPath}`);
279+
sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_TRIGGER, undefined, { trigger: 'watching' });
267280
this.refreshData.trigger(uri, false);
268281
}),
269282
);
270283
this.disposables.push(
271284
watcher.onDidCreate((uri) => {
272285
traceVerbose(`Testing: Trigger refresh after creating ${uri.fsPath}`);
286+
sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_TRIGGER, undefined, { trigger: 'watching' });
273287
this.refreshData.trigger(uri, false);
274288
}),
275289
);
276290
this.disposables.push(
277291
watcher.onDidDelete((uri) => {
278292
traceVerbose(`Testing: Trigger refresh after deleting in ${uri.fsPath}`);
293+
sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_TRIGGER, undefined, { trigger: 'watching' });
279294
this.refreshData.trigger(uri, false);
280295
}),
281296
);
@@ -289,19 +304,22 @@ export class PythonTestController implements ITestController {
289304
this.disposables.push(
290305
watcher.onDidChange((uri) => {
291306
traceVerbose(`Testing: Trigger refresh after change in ${uri.fsPath}`);
307+
sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_TRIGGER, undefined, { trigger: 'watching' });
292308
// We want to invalidate tests for code change
293309
this.refreshData.trigger(uri, true);
294310
}),
295311
);
296312
this.disposables.push(
297313
watcher.onDidCreate((uri) => {
298314
traceVerbose(`Testing: Trigger refresh after creating ${uri.fsPath}`);
315+
sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_TRIGGER, undefined, { trigger: 'watching' });
299316
this.refreshData.trigger(uri, false);
300317
}),
301318
);
302319
this.disposables.push(
303320
watcher.onDidDelete((uri) => {
304321
traceVerbose(`Testing: Trigger refresh after deleting in ${uri.fsPath}`);
322+
sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_TRIGGER, undefined, { trigger: 'watching' });
305323
this.refreshData.trigger(uri, false);
306324
}),
307325
);

src/client/testing/testController/pytest/pytestController.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { traceError } from '../../../common/logger';
1111
import { runAdapter } from '../../../common/process/internal/scripts/testing_tools';
1212
import { IConfigurationService } from '../../../common/types';
1313
import { createDeferred, Deferred } from '../../../common/utils/async';
14+
import { sendTelemetryEvent } from '../../../telemetry';
15+
import { EventName } from '../../../telemetry/constants';
1416
import { PYTEST_PROVIDER } from '../../common/constants';
1517
import { TestDiscoveryOptions } from '../../common/types';
1618
import {
@@ -142,6 +144,7 @@ export class PytestController implements ITestFrameworkController {
142144
}
143145

144146
public async refreshTestData(testController: TestController, uri: Uri, token?: CancellationToken): Promise<void> {
147+
sendTelemetryEvent(EventName.UNITTEST_DISCOVERING, undefined, { tool: 'pytest' });
145148
const workspace = this.workspaceService.getWorkspaceFolder(uri);
146149
if (workspace) {
147150
// Discovery is expensive. So if it is already running then use the promise
@@ -199,6 +202,7 @@ export class PytestController implements ITestFrameworkController {
199202

200203
deferred.resolve();
201204
} catch (ex) {
205+
sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { tool: 'pytest', failed: true });
202206
const cancel = options.token?.isCancellationRequested ? 'Cancelled' : 'Error';
203207
traceError(`${cancel} discovering pytest tests:\r\n`, ex);
204208
const message = getTestDiscoveryExceptions((ex as Error).message);
@@ -252,6 +256,7 @@ export class PytestController implements ITestFrameworkController {
252256
await this.resolveChildren(testController, newItem);
253257
}
254258
}
259+
sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { tool: 'pytest', failed: false });
255260
return Promise.resolve();
256261
}
257262

src/client/testing/testController/unittest/unittestController.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import {
2929
updateTestItemFromRawData,
3030
} from '../common/testItemUtilities';
3131
import { traceError } from '../../../common/logger';
32+
import { sendTelemetryEvent } from '../../../telemetry';
33+
import { EventName } from '../../../telemetry/constants';
3234

3335
@injectable()
3436
export class UnittestController implements ITestFrameworkController {
@@ -89,6 +91,7 @@ export class UnittestController implements ITestFrameworkController {
8991
}
9092

9193
public async refreshTestData(testController: TestController, uri: Uri, token?: CancellationToken): Promise<void> {
94+
sendTelemetryEvent(EventName.UNITTEST_DISCOVERING, undefined, { tool: 'unittest' });
9295
const workspace = this.workspaceService.getWorkspaceFolder(uri);
9396
if (workspace) {
9497
// Discovery is expensive. So if it is already running then use the promise
@@ -199,6 +202,7 @@ for error in loader_errors:
199202

200203
deferred.resolve();
201204
} catch (ex) {
205+
sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { tool: 'unittest', failed: true });
202206
const cancel = options.token?.isCancellationRequested ? 'Cancelled' : 'Error';
203207
traceError(`${cancel} discovering unittest tests:\r\n`, ex);
204208

@@ -253,6 +257,7 @@ for error in loader_errors:
253257
await this.resolveChildren(testController, newItem);
254258
}
255259
}
260+
sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { tool: 'unittest', failed: false });
256261
return Promise.resolve();
257262
}
258263

0 commit comments

Comments
 (0)