Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/common/telemetry/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ export interface IEventNamePropertyMapping {

/* __GDPR__
"environment_manager.registered": {
"managerId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }
"managerId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" },
"<duration>": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" }
}
*/
[EventNames.ENVIRONMENT_MANAGER_REGISTERED]: {
Expand Down
48 changes: 48 additions & 0 deletions src/common/telemetry/errorClassifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { CancellationError } from 'vscode';

export type DiscoveryErrorType =
| 'spawn_timeout'
| 'spawn_enoent'
| 'permission_denied'
| 'canceled'
| 'parse_error'
| 'unknown';

/**
* Classifies an error into a telemetry-safe category for the `errorType` property.
* Does NOT include raw error messages (PII risk from file paths) — only the category.
*/
export function classifyError(ex: unknown): DiscoveryErrorType {
if (ex instanceof CancellationError) {
return 'canceled';
}

if (!(ex instanceof Error)) {
return 'unknown';
}

// Check error code for spawn failures (Node.js sets `code` on spawn errors)
const code = (ex as NodeJS.ErrnoException).code;
if (code === 'ENOENT') {
return 'spawn_enoent';
}
if (code === 'EACCES' || code === 'EPERM') {
return 'permission_denied';
}

// Check message patterns
const msg = ex.message.toLowerCase();
if (msg.includes('timed out') || msg.includes('timeout')) {
return 'spawn_timeout';
}
if (msg.includes('parse') || msg.includes('unexpected token') || msg.includes('json')) {
return 'parse_error';
}

// Check error name for cancellation variants
if (ex.name === 'CancellationError' || ex.name === 'AbortError') {
return 'canceled';
}

return 'unknown';
Comment thread
eleanorjboyd marked this conversation as resolved.
}
Comment thread
eleanorjboyd marked this conversation as resolved.
6 changes: 6 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,12 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
await sendProjectStructureTelemetry(projectManager, envManagers);
} catch (error) {
traceError('Failed to initialize environment managers:', error);
sendTelemetryEvent(
EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION,
start.elapsedTime,
undefined,
error instanceof Error ? error : undefined,
);
Comment thread
eleanorjboyd marked this conversation as resolved.
// Show a user-friendly error message
window.showErrorMessage(
l10n.t(
Expand Down
4 changes: 3 additions & 1 deletion src/features/envManagers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
PackageManagerAlreadyRegisteredError,
} from '../common/errors/AlreadyRegisteredError';
import { traceError, traceVerbose } from '../common/logging';
import { StopWatch } from '../common/stopWatch';
import { EventNames } from '../common/telemetry/constants';
import { sendTelemetryEvent } from '../common/telemetry/sender';
import { getCallingExtension } from '../common/utils/frameUtils';
Expand Down Expand Up @@ -71,6 +72,7 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
constructor(private readonly pm: PythonProjectManager) {}

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

if (!managerId.toLowerCase().startsWith('undefined_publisher.')) {
sendTelemetryEvent(EventNames.ENVIRONMENT_MANAGER_REGISTERED, undefined, {
sendTelemetryEvent(EventNames.ENVIRONMENT_MANAGER_REGISTERED, registrationStopWatch.elapsedTime, {
managerId,
});
}
Expand Down
7 changes: 4 additions & 3 deletions src/internal.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { CreateEnvironmentNotSupported, RemoveEnvironmentNotSupported } from './
import { StopWatch } from './common/stopWatch';
import { EventNames } from './common/telemetry/constants';
import { sendTelemetryEvent } from './common/telemetry/sender';
import { classifyError } from './common/telemetry/errorClassifier';

export type EnvironmentManagerScope = undefined | string | Uri | PythonEnvironment;
export type PackageManagerScope = undefined | string | Uri | PythonEnvironment | Package;
Expand Down Expand Up @@ -213,14 +214,14 @@ export class InternalEnvironmentManager implements EnvironmentManager {
envCount: envs.length,
});
} catch (ex) {
const isTimeout = ex instanceof Error && ex.message.includes('timed out');
const errorType = classifyError(ex);
sendTelemetryEvent(
EventNames.ENVIRONMENT_DISCOVERY,
sw.elapsedTime,
{
managerId: this.id,
result: isTimeout ? 'timeout' : 'error',
errorType: ex instanceof Error ? ex.name : 'unknown',
result: errorType === 'canceled' || errorType === 'spawn_timeout' ? 'timeout' : 'error',
errorType,
},
ex instanceof Error ? ex : undefined,
);
Expand Down
Loading