Skip to content

Commit 4f0c673

Browse files
committed
feat: enhance telemetry with error classification and duration tracking for environment manager registration
1 parent f983b64 commit 4f0c673

File tree

5 files changed

+63
-5
lines changed

5 files changed

+63
-5
lines changed

src/common/telemetry/constants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ export interface IEventNamePropertyMapping {
6060

6161
/* __GDPR__
6262
"environment_manager.registered": {
63-
"managerId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }
63+
"managerId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
64+
"<duration>": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" }
6465
}
6566
*/
6667
[EventNames.ENVIRONMENT_MANAGER_REGISTERED]: {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { CancellationError } from 'vscode';
2+
3+
export type DiscoveryErrorType =
4+
| 'spawn_timeout'
5+
| 'spawn_enoent'
6+
| 'permission_denied'
7+
| 'canceled'
8+
| 'parse_error'
9+
| 'unknown';
10+
11+
/**
12+
* Classifies an error into a telemetry-safe category for the `errorType` property.
13+
* Does NOT include raw error messages (PII risk from file paths) — only the category.
14+
*/
15+
export function classifyError(ex: unknown): DiscoveryErrorType {
16+
if (ex instanceof CancellationError) {
17+
return 'canceled';
18+
}
19+
20+
if (!(ex instanceof Error)) {
21+
return 'unknown';
22+
}
23+
24+
// Check error code for spawn failures (Node.js sets `code` on spawn errors)
25+
const code = (ex as NodeJS.ErrnoException).code;
26+
if (code === 'ENOENT') {
27+
return 'spawn_enoent';
28+
}
29+
if (code === 'EACCES' || code === 'EPERM') {
30+
return 'permission_denied';
31+
}
32+
33+
// Check message patterns
34+
const msg = ex.message.toLowerCase();
35+
if (msg.includes('timed out') || msg.includes('timeout')) {
36+
return 'spawn_timeout';
37+
}
38+
if (msg.includes('parse') || msg.includes('unexpected token') || msg.includes('json')) {
39+
return 'parse_error';
40+
}
41+
42+
// Check error name for cancellation variants
43+
if (ex.name === 'CancellationError' || ex.name === 'AbortError') {
44+
return 'canceled';
45+
}
46+
47+
return 'unknown';
48+
}

src/extension.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,12 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
547547
await sendProjectStructureTelemetry(projectManager, envManagers);
548548
} catch (error) {
549549
traceError('Failed to initialize environment managers:', error);
550+
sendTelemetryEvent(
551+
EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION,
552+
start.elapsedTime,
553+
undefined,
554+
error instanceof Error ? error : undefined,
555+
);
550556
// Show a user-friendly error message
551557
window.showErrorMessage(
552558
l10n.t(

src/features/envManagers.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
PackageManagerAlreadyRegisteredError,
1717
} from '../common/errors/AlreadyRegisteredError';
1818
import { traceError, traceVerbose } from '../common/logging';
19+
import { StopWatch } from '../common/stopWatch';
1920
import { EventNames } from '../common/telemetry/constants';
2021
import { sendTelemetryEvent } from '../common/telemetry/sender';
2122
import { getCallingExtension } from '../common/utils/frameUtils';
@@ -71,6 +72,7 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
7172
constructor(private readonly pm: PythonProjectManager) {}
7273

7374
public registerEnvironmentManager(manager: EnvironmentManager): Disposable {
75+
const registrationStopWatch = new StopWatch();
7476
const managerId = generateId(manager.name);
7577
if (this._environmentManagers.has(managerId)) {
7678
const ex = new EnvironmentManagerAlreadyRegisteredError(
@@ -105,7 +107,7 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
105107
this._onDidChangeEnvironmentManager.fire({ kind: 'registered', manager: mgr });
106108

107109
if (!managerId.toLowerCase().startsWith('undefined_publisher.')) {
108-
sendTelemetryEvent(EventNames.ENVIRONMENT_MANAGER_REGISTERED, undefined, {
110+
sendTelemetryEvent(EventNames.ENVIRONMENT_MANAGER_REGISTERED, registrationStopWatch.elapsedTime, {
109111
managerId,
110112
});
111113
}

src/internal.api.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { CreateEnvironmentNotSupported, RemoveEnvironmentNotSupported } from './
3131
import { StopWatch } from './common/stopWatch';
3232
import { EventNames } from './common/telemetry/constants';
3333
import { sendTelemetryEvent } from './common/telemetry/sender';
34+
import { classifyError } from './common/telemetry/errorClassifier';
3435

3536
export type EnvironmentManagerScope = undefined | string | Uri | PythonEnvironment;
3637
export type PackageManagerScope = undefined | string | Uri | PythonEnvironment | Package;
@@ -213,14 +214,14 @@ export class InternalEnvironmentManager implements EnvironmentManager {
213214
envCount: envs.length,
214215
});
215216
} catch (ex) {
216-
const isTimeout = ex instanceof Error && ex.message.includes('timed out');
217+
const errorType = classifyError(ex);
217218
sendTelemetryEvent(
218219
EventNames.ENVIRONMENT_DISCOVERY,
219220
sw.elapsedTime,
220221
{
221222
managerId: this.id,
222-
result: isTimeout ? 'timeout' : 'error',
223-
errorType: ex instanceof Error ? ex.name : 'unknown',
223+
result: errorType === 'canceled' || errorType === 'spawn_timeout' ? 'timeout' : 'error',
224+
errorType,
224225
},
225226
ex instanceof Error ? ex : undefined,
226227
);

0 commit comments

Comments
 (0)