From 21ec8e48bcdd9710475fa4ac393f72c4b9513c54 Mon Sep 17 00:00:00 2001 From: Stella Huang Date: Mon, 17 Aug 2026 12:59:13 -0700 Subject: [PATCH 1/4] Add inline script environment lifecycle telemetry Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3cb82ae9-7424-40a4-9156-8c54ac6e0895 --- src/common/telemetry/constants.ts | 55 +++ .../builtin/inlineScript/envManager.ts | 221 ++++++++--- src/managers/builtin/uvPythonInstaller.ts | 61 ++- .../inlineScript/envManager.unit.test.ts | 365 ++++++++++++++++-- 4 files changed, 609 insertions(+), 93 deletions(-) diff --git a/src/common/telemetry/constants.ts b/src/common/telemetry/constants.ts index 6b3432eb..c718e6a8 100644 --- a/src/common/telemetry/constants.ts +++ b/src/common/telemetry/constants.ts @@ -221,6 +221,27 @@ export enum EventNames { * - dependencyCount: number (number of entries in the `dependencies` list) */ INLINE_SCRIPT_DETECTED = 'inlineScript.detected', + /** + * Telemetry event fired when inline-script environment creation completes + * successfully with a newly-built cache entry that passed verification and + * metadata persistence. + * Measures: + * - duration: number (ms spent in the underlying create/rebuild operation) + * - dependencyCount: number (normalized dependency count in the cache key) + */ + INLINE_SCRIPT_ENV_CREATED = 'inlineScript.envCreated', + /** + * Telemetry event fired when inline-script environment creation validates + * and reuses an existing cache entry without rebuilding it. + */ + INLINE_SCRIPT_ENV_REUSE_HIT = 'inlineScript.envReuseHit', + /** + * Telemetry event fired when inline-script environment creation cannot + * complete. + * Properties: + * - category: stable low-cardinality failure category + */ + INLINE_SCRIPT_ENV_ERROR = 'inlineScript.envError', /** * Telemetry event fired once per session, per URI, the first time a `.py` * file that previously raised an `inlineScript.detected` event receives a @@ -232,6 +253,15 @@ export enum EventNames { INLINE_SCRIPT_EDITED = 'inlineScript.edited', } +export type InlineScriptEnvErrorCategory = + | 'compatible-python-declined' + | 'discovery-failure' + | 'no-compatible-python' + | 'package-install-cancelled' + | 'install-failure' + | 'lock-timeout' + | 'lock-unavailable'; + // Map all events to their properties export interface IEventNamePropertyMapping { /* __GDPR__ @@ -695,6 +725,31 @@ export interface IEventNamePropertyMapping { errorType?: string; }; + /* __GDPR__ + "inlineScript.envCreated": { + "dependencyCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "StellaHuang95" }, + "": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "StellaHuang95" } + } + */ + [EventNames.INLINE_SCRIPT_ENV_CREATED]: { + // Goes through the measures payload (numeric); listed here for GDPR only. + dependencyCount?: number; + }; + + /* __GDPR__ + "inlineScript.envReuseHit": {"owner": "StellaHuang95" } + */ + [EventNames.INLINE_SCRIPT_ENV_REUSE_HIT]: never | undefined; + + /* __GDPR__ + "inlineScript.envError": { + "category": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" } + } + */ + [EventNames.INLINE_SCRIPT_ENV_ERROR]: { + category: InlineScriptEnvErrorCategory; + }; + /* __GDPR__ "inlineScript.detected": { "trigger": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "StellaHuang95" }, diff --git a/src/managers/builtin/inlineScript/envManager.ts b/src/managers/builtin/inlineScript/envManager.ts index d68d9dda..00e5883b 100644 --- a/src/managers/builtin/inlineScript/envManager.ts +++ b/src/managers/builtin/inlineScript/envManager.ts @@ -44,6 +44,8 @@ import { } from '../../../common/constants'; import { acquireFileLock, AcquiredFileLock } from '../../../common/lockfile.apis'; import { getWorkspacePersistentState, PersistentState } from '../../../common/persistentState'; +import { EventNames, InlineScriptEnvErrorCategory } from '../../../common/telemetry/constants'; +import { sendTelemetryEvent } from '../../../common/telemetry/sender'; import { isFileNotFoundError } from '../../../common/utils/filesystem'; import { normalizePath } from '../../../common/utils/pathUtils'; import { compareReleaseSegments, parseReleaseSegments } from '../../../common/utils/pep440Release'; @@ -80,8 +82,24 @@ interface CreateOrReuseEnvironmentOptions { interface BuildCacheEntryResult { readonly environment?: PythonEnvironment; readonly retainLock?: boolean; + readonly errorCategory?: InlineScriptEnvErrorCategory; } +interface BaseInterpreterSelectionResult { + readonly selectedBase?: SelectedBaseInterpreter; + readonly errorCategory?: InlineScriptEnvErrorCategory; +} + +interface SelectBaseInterpreterResult { + readonly selectedBase?: SelectedBaseInterpreter; + readonly discoveryFailed: boolean; +} + +type InstallPythonAndRefreshResult = + | { readonly kind: 'installed'; readonly installedPath: string } + | { readonly kind: 'declined' } + | { readonly kind: 'failed' }; + type CacheEntryInspection = | { readonly kind: 'absent' | 'stale' | 'uncertain' } | { readonly kind: 'reusable'; readonly environment: PythonEnvironment }; @@ -167,6 +185,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable { } } } catch (error) { + this.sendInlineScriptEnvErrorTelemetry('install-failure'); this.log.error(`Failed to set up inline-script environment: ${getErrorMessage(error)}`); return undefined; } @@ -178,14 +197,15 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable { packages: readonly string[], options?: CreateEnvironmentOptions, ): Promise { - let selectedBase = await this.selectBaseInterpreter(metadata); - if (!selectedBase && options?.quickCreate !== true) { - selectedBase = await this.installAndSelectBaseInterpreter(metadata); - } - if (!selectedBase) { + const baseSelection = await this.selectOrInstallBaseInterpreter(metadata, options?.quickCreate === true); + if (!baseSelection.selectedBase) { + if (baseSelection.errorCategory) { + this.sendInlineScriptEnvErrorTelemetry(baseSelection.errorCategory); + } this.log.warn(`No compatible Python is available for inline-script environment creation: ${scriptUri.fsPath}.`); return undefined; } + const selectedBase = baseSelection.selectedBase; const cacheKey = computeCacheKey({ dependencies: packages, @@ -227,6 +247,22 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable { ]); } + private async selectOrInstallBaseInterpreter( + metadata: InlineScriptMetadata, + quickCreate: boolean, + ): Promise { + const selection = await this.selectBaseInterpreter(metadata); + if (selection.selectedBase) { + return { selectedBase: selection.selectedBase }; + } + if (quickCreate) { + return { + errorCategory: this.getBaseInterpreterErrorCategory(selection.discoveryFailed, 'no-compatible-python'), + }; + } + return this.installAndSelectBaseInterpreter(metadata, selection.discoveryFailed); + } + async refresh(_scope: RefreshEnvironmentsScope): Promise { return; } @@ -800,11 +836,13 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable { ); } - private async selectBaseInterpreter(metadata: InlineScriptMetadata): Promise { + private async selectBaseInterpreter(metadata: InlineScriptMetadata): Promise { let globalEnvironments: readonly PythonEnvironment[] = []; + let discoveryFailed = false; try { globalEnvironments = await this.api.getEnvironments('global'); } catch (error) { + discoveryFailed = true; this.log.warn(`Unable to query discovered base interpreters: ${getErrorMessage(error)}`); } const reported = [ @@ -845,7 +883,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable { while (candidates.length > 0) { const environment = pickCompatibleInterpreter(candidates, undefined); if (!environment) { - return undefined; + return { discoveryFailed }; } candidates = candidates.filter((candidate) => candidate !== environment); @@ -854,7 +892,10 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable { continue; } try { - return { environment, canonicalPath: await fs.realpath(executable) }; + return { + selectedBase: { environment, canonicalPath: await fs.realpath(executable) }, + discoveryFailed, + }; } catch (error) { this.log.warn( `Skipping base interpreter that cannot be resolved at ${executable}: ${getErrorMessage(error)}`, @@ -862,14 +903,15 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable { } } - return undefined; + return { discoveryFailed }; } private async installAndSelectBaseInterpreter( metadata: InlineScriptMetadata, - ): Promise { + priorDiscoveryFailed = false, + ): Promise { const run = this.baseInterpreterInstallationQueue.then(() => - this.installAndSelectBaseInterpreterSerially(metadata), + this.installAndSelectBaseInterpreterSerially(metadata, priorDiscoveryFailed), ); // Keep the stored queue tail fulfilled so one failed request does not block later attempts; // the caller still observes the original result through `run`. @@ -882,35 +924,43 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable { private async installAndSelectBaseInterpreterSerially( metadata: InlineScriptMetadata, - ): Promise { + priorDiscoveryFailed: boolean, + ): Promise { const existing = await this.selectBaseInterpreter(metadata); - if (existing) { - return existing; + const discoveryFailed = priorDiscoveryFailed || existing.discoveryFailed; + if (existing.selectedBase) { + return { selectedBase: existing.selectedBase }; } const requiresPython = metadata.requiresPython?.trim() || undefined; const lowerBound = extractLowerBoundVersion(requiresPython); - const version = await this.selectInstallablePythonVersion(requiresPython, lowerBound); - if (requiresPython && !version) { + const versionSelection = await this.selectInstallablePythonVersion(requiresPython, lowerBound); + if (requiresPython && !versionSelection.version) { this.log.warn( 'Cannot install a Python for this inline script because no compatible install version could be selected.', ); - return undefined; + return { + errorCategory: this.getBaseInterpreterErrorCategory( + discoveryFailed, + versionSelection.errorCategory ?? 'no-compatible-python', + ), + }; } - const installedPath = await this.installPythonAndRefresh(requiresPython, version); - if (!installedPath) { - return undefined; + const installResult = await this.installPythonAndRefresh(requiresPython, versionSelection.version); + if (installResult.kind !== 'installed') { + return { + errorCategory: this.getBaseInterpreterErrorCategory( + discoveryFailed, + installResult.kind === 'declined' ? 'compatible-python-declined' : 'install-failure', + ), + }; } + const installedPath = installResult.installedPath; - let selected: SelectedBaseInterpreter | undefined; - try { - selected = await this.selectBaseInterpreter(metadata); - } catch (error) { - this.log.warn( - `Unable to refresh base-interpreter discovery after installing Python: ${getErrorMessage(error)}`, - ); - } + const refreshedSelection = await this.selectBaseInterpreter(metadata); + const discoveryFailedAfterInstall = discoveryFailed || refreshedSelection.discoveryFailed; + let selected = refreshedSelection.selectedBase; if (!selected) { const resolved = await resolveSystemPythonEnvironmentPath( installedPath, @@ -940,42 +990,55 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable { this.log.warn( 'Python was installed for an inline script, but no compatible base interpreter was discovered after refreshing environments.', ); + return { + errorCategory: this.getBaseInterpreterErrorCategory(discoveryFailedAfterInstall, 'install-failure'), + }; } - return selected; + return { selectedBase: selected }; } private async selectInstallablePythonVersion( requiresPython: string | undefined, lowerBound: string | undefined, - ): Promise { + ): Promise<{ readonly version?: string; readonly errorCategory?: InlineScriptEnvErrorCategory }> { if (!requiresPython) { - return lowerBound; + return { version: lowerBound }; } const prereleaseLowerBound = this.extractPrereleaseLowerBound(requiresPython); if (prereleaseLowerBound) { - return prereleaseLowerBound; + return { version: prereleaseLowerBound }; } const lowerBoundRelease = lowerBound ? parseReleaseSegments(lowerBound) : undefined; if (lowerBound && lowerBoundRelease?.[0] === 3) { if (/^>=\s*[^,]+$/.test(requiresPython) && this.matchesInstallConstraint(requiresPython, lowerBound)) { - return lowerBound; + return { version: lowerBound }; } if (/^==\s*[^,*]+$/.test(requiresPython) && this.matchesInstallConstraint(requiresPython, lowerBound)) { - return lowerBound; + return { version: lowerBound }; } } let available: uvPythonInstaller.UvPythonVersion[]; try { - if (!(await uvPythonInstaller.ensureUvForInlineScriptVersionLookup(requiresPython, this.log))) { - return undefined; + const uvLookupResult = await uvPythonInstaller.ensureUvForInlineScriptVersionLookupDetailed( + requiresPython, + this.log, + ); + if (uvLookupResult !== 'available') { + return { + errorCategory: + uvLookupResult === 'declined' ? 'compatible-python-declined' : 'install-failure', + }; } available = await uvPythonInstaller.getAvailablePythonVersions(); } catch (error) { this.log.warn(`Unable to query Python versions available from uv: ${getErrorMessage(error)}`); - return undefined; + return { errorCategory: 'install-failure' }; } - return available + if (available.length === 0) { + return { errorCategory: 'install-failure' }; + } + const version = available .filter( (candidate) => candidate.implementation === 'cpython' && @@ -991,6 +1054,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable { } return compareReleaseSegments(rightRelease, leftRelease); })[0]?.version; + return version ? { version } : { errorCategory: 'no-compatible-python' }; } private matchesInstallConstraint(requiresPython: string, version: string): boolean { @@ -1024,22 +1088,26 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable { private async installPythonAndRefresh( requiresPython: string | undefined, version: string | undefined, - ): Promise { - let installedPath: string | undefined; + ): Promise { + let promptResult: uvPythonInstaller.PromptInstallPythonViaUvResult; try { - installedPath = await uvPythonInstaller.promptInstallPythonViaUv('inlineScript', this.log, { + promptResult = await uvPythonInstaller.promptInstallPythonViaUvDetailed('inlineScript', this.log, { requiresPython, version, }); - if (!installedPath) { + if (promptResult.kind === 'declined') { this.log.warn( 'Python installation for inline-script environment creation was declined or did not complete.', ); - return undefined; + return { kind: 'declined' }; + } + if (promptResult.kind === 'failed') { + this.log.error('Failed to install Python for an inline script.'); + return { kind: 'failed' }; } } catch (error) { this.log.error(`Failed to install Python for an inline script: ${getErrorMessage(error)}`); - return undefined; + return { kind: 'failed' }; } try { @@ -1049,7 +1117,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable { `Python was installed for an inline script, but environment discovery could not be refreshed: ${getErrorMessage(error)}`, ); } - return installedPath; + return { kind: 'installed', installedPath: promptResult.pythonPath }; } private async createOrReuseEnvironment({ @@ -1058,6 +1126,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable { metadata, selectedBase, }: CreateOrReuseEnvironmentOptions): Promise { + const dependencyCount = this.getTelemetryDependencyCount(packages); const cacheRoot = getScriptEnvCacheRoot(this.globalStorageUri); const envDir = getScriptEnvDir(this.globalStorageUri, cacheKey); await fs.ensureDir(cacheRoot.fsPath); @@ -1071,20 +1140,24 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable { const cached = await this.inspectCacheEntry(cacheRoot, envDir, metadata, selectedBase); if (cached.kind === 'reusable') { + this.sendInlineScriptEnvReuseHitTelemetry(); return cached.environment; } if (cached.kind === 'uncertain') { this.log.warn( `Preserving an inline-script cache entry that could not be safely inspected: ${envDir.fsPath}`, ); + this.sendInlineScriptEnvErrorTelemetry('install-failure'); return undefined; } if (cached.kind === 'stale') { if (!(await this.removeCacheEntry(envDir))) { + this.sendInlineScriptEnvErrorTelemetry('install-failure'); return undefined; } } + const buildStartAtMs = Date.now(); const build = await this.buildCacheEntry(envDir, cacheRoot, packages, selectedBase); if (build.retainLock) { try { @@ -1095,8 +1168,16 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable { ); } } - return build.environment; + if (build.environment) { + this.sendInlineScriptEnvCreatedTelemetry(buildStartAtMs, dependencyCount); + return build.environment; + } + if (build.errorCategory) { + this.sendInlineScriptEnvErrorTelemetry(build.errorCategory); + } + return undefined; } catch (error) { + this.sendInlineScriptEnvErrorTelemetry(this.getCreateOrReuseErrorCategory(error)); this.log.error(`Failed to create or reuse inline-script cache entry: ${getErrorMessage(error)}`); return undefined; } finally { @@ -1210,21 +1291,21 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable { } catch (error) { this.log.error(`Failed to build inline-script environment: ${getErrorMessage(error)}`); await this.removeCacheEntry(envDir); - return {}; + return { errorCategory: 'install-failure' }; } if (result?.pkgInstallationCancelled) { this.log.warn( 'Inline-script package installation was cancelled; retaining the cache lock until explicit cleanup.', ); - return { retainLock: true }; + return { retainLock: true, errorCategory: 'package-install-cancelled' }; } if (!result?.environment || result.envCreationErr || result.pkgInstallationErr) { const error = result?.envCreationErr ?? result?.pkgInstallationErr ?? 'environment creation returned no result'; this.log.error(`Failed to build inline-script environment: ${error}`); await this.removeCacheEntry(envDir); - return {}; + return { errorCategory: 'install-failure' }; } if ( !this.areEqualPythonReleases(result.environment.version, selectedBase.environment.version) || @@ -1232,7 +1313,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable { ) { this.log.error('Created inline-script environment does not match the requested cache entry.'); await this.removeCacheEntry(envDir); - return {}; + return { errorCategory: 'install-failure' }; } try { @@ -1245,7 +1326,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable { } catch (error) { this.log.error(`Failed to record inline-script cache metadata: ${getErrorMessage(error)}`); await this.removeCacheEntry(envDir); - return {}; + return { errorCategory: 'install-failure' }; } return { environment: result.environment }; @@ -1282,6 +1363,44 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable { return compareReleaseSegments(actualRelease, expectedRelease) === 0; } + private getTelemetryDependencyCount(packages: ReadonlyArray): number { + return new Set(packages.map(normalizeDependency)).size; + } + + private sendInlineScriptEnvCreatedTelemetry(startAtMs: number, dependencyCount: number): void { + sendTelemetryEvent(EventNames.INLINE_SCRIPT_ENV_CREATED, { + duration: Date.now() - startAtMs, + dependencyCount, + }); + } + + private sendInlineScriptEnvReuseHitTelemetry(): void { + sendTelemetryEvent(EventNames.INLINE_SCRIPT_ENV_REUSE_HIT); + } + + private sendInlineScriptEnvErrorTelemetry(category: InlineScriptEnvErrorCategory): void { + sendTelemetryEvent(EventNames.INLINE_SCRIPT_ENV_ERROR, undefined, { category }); + } + + private getBaseInterpreterErrorCategory( + discoveryFailed: boolean, + fallbackCategory: InlineScriptEnvErrorCategory, + ): InlineScriptEnvErrorCategory { + return discoveryFailed ? 'discovery-failure' : fallbackCategory; + } + + private getCreateOrReuseErrorCategory(error: unknown): InlineScriptEnvErrorCategory { + if ( + typeof error === 'object' && + error !== null && + 'code' in error && + ['ELOCKED', 'ELOCKRETAINED', 'ELOCKORPHANED'].includes((error as NodeJS.ErrnoException).code ?? '') + ) { + return (error as NodeJS.ErrnoException).code === 'ELOCKED' ? 'lock-timeout' : 'lock-unavailable'; + } + return 'install-failure'; + } + dispose(): void { this._onDidChangeEnvironments.dispose(); this._onDidChangeEnvironment.dispose(); diff --git a/src/managers/builtin/uvPythonInstaller.ts b/src/managers/builtin/uvPythonInstaller.ts index c0fc90e6..7fabe4b5 100644 --- a/src/managers/builtin/uvPythonInstaller.ts +++ b/src/managers/builtin/uvPythonInstaller.ts @@ -40,6 +40,13 @@ export interface UvPythonInstallPromptOptions { readonly requiresPython?: string; } +export type EnsureUvForInlineScriptVersionLookupResult = 'available' | 'declined' | 'failed'; + +export type PromptInstallPythonViaUvResult = + | { readonly kind: 'installed'; readonly pythonPath: string } + | { readonly kind: 'declined' } + | { readonly kind: 'failed' }; + function sanitizePromptDetail(value: string | undefined): string | undefined { const normalized = value?.replace(PROMPT_CONTROL_CHARACTERS, ' ').replace(/\s+/g, ' ').trim(); if (!normalized) { @@ -185,30 +192,40 @@ export async function installUv(_log?: LogOutputChannel): Promise { return success; } -export async function ensureUvForInlineScriptVersionLookup( +export async function ensureUvForInlineScriptVersionLookupDetailed( requiresPython: string, log?: LogOutputChannel, -): Promise { +): Promise { if (await isUvInstalled(log)) { - return true; + return 'available'; } const displayedRequirement = sanitizePromptDetail(requiresPython); if (!displayedRequirement) { - return false; + return 'failed'; } const selection = await showInformationMessage( UvInstallStrings.inlineScriptInstallUvForVersionLookupPrompt(displayedRequirement), { modal: true }, UvInstallStrings.installUv, ); - if (selection !== UvInstallStrings.installUv || !(await installUv(log))) { - return false; + if (selection !== UvInstallStrings.installUv) { + return 'declined'; + } + if (!(await installUv(log))) { + return 'failed'; } if (await isUvInstalled(log)) { - return true; + return 'available'; } showErrorMessage(UvInstallStrings.uvInstallRestartRequired); - return false; + return 'failed'; +} + +export async function ensureUvForInlineScriptVersionLookup( + requiresPython: string, + log?: LogOutputChannel, +): Promise { + return (await ensureUvForInlineScriptVersionLookupDetailed(requiresPython, log)) === 'available'; } /** @@ -379,19 +396,19 @@ export async function installPythonViaUv(_log?: LogOutputChannel, version?: stri * @param trigger What triggered this prompt * @param log Optional log output channel * @param options Optional version and script requirement shown to the user and passed to uv after consent - * @returns Promise that resolves to the installed Python path, or undefined if not installed + * @returns Promise that resolves to a structured installed / declined / failed outcome */ -export async function promptInstallPythonViaUv( +export async function promptInstallPythonViaUvDetailed( trigger: UvPythonInstallTrigger, log?: LogOutputChannel, options?: UvPythonInstallPromptOptions, -): Promise { +): Promise { const state = trigger === 'inlineScript' ? undefined : await getGlobalPersistentState(); const dontAsk = await state?.get(UV_INSTALL_PYTHON_DONT_ASK_KEY); if (dontAsk) { traceLog('Skipping Python install prompt: user selected "Don\'t ask again"'); - return undefined; + return { kind: 'declined' }; } const version = sanitizePromptDetail(options?.version); @@ -399,11 +416,11 @@ export async function promptInstallPythonViaUv( if (trigger === 'inlineScript' && version && !INSTALLABLE_PYTHON_VERSION.test(version)) { traceWarn(`Skipping inline-script Python install prompt: invalid install version ${JSON.stringify(version)}`); - return undefined; + return { kind: 'failed' }; } if (trigger === 'inlineScript' && requiresPython && !version) { traceWarn('Skipping inline-script Python install prompt: no compatible install version was selected'); - return undefined; + return { kind: 'failed' }; } sendTelemetryEvent(EventNames.UV_PYTHON_INSTALL_PROMPTED, undefined, { trigger }); @@ -434,14 +451,24 @@ export async function promptInstallPythonViaUv( if (result === Common.dontAskAgain && state) { await state.set(UV_INSTALL_PYTHON_DONT_ASK_KEY, true); traceLog('User selected "Don\'t ask again" for Python install prompt'); - return undefined; + return { kind: 'declined' }; } if (result === installAction) { - return await installPythonWithUv(log, version); + const pythonPath = await installPythonWithUv(log, version); + return pythonPath ? { kind: 'installed', pythonPath } : { kind: 'failed' }; } - return undefined; + return { kind: 'declined' }; +} + +export async function promptInstallPythonViaUv( + trigger: UvPythonInstallTrigger, + log?: LogOutputChannel, + options?: UvPythonInstallPromptOptions, +): Promise { + const result = await promptInstallPythonViaUvDetailed(trigger, log, options); + return result.kind === 'installed' ? result.pythonPath : undefined; } /** diff --git a/src/test/managers/builtin/inlineScript/envManager.unit.test.ts b/src/test/managers/builtin/inlineScript/envManager.unit.test.ts index 3d0488ca..ff06d9d0 100644 --- a/src/test/managers/builtin/inlineScript/envManager.unit.test.ts +++ b/src/test/managers/builtin/inlineScript/envManager.unit.test.ts @@ -13,6 +13,8 @@ import * as cacheLayout from '../../../../common/inlineScript/cacheLayout'; import * as metadataReader from '../../../../common/inlineScript/metadata'; import * as lockfileApis from '../../../../common/lockfile.apis'; import * as persistentState from '../../../../common/persistentState'; +import { EventNames } from '../../../../common/telemetry/constants'; +import * as telemetrySender from '../../../../common/telemetry/sender'; import { isWindows } from '../../../../common/utils/platformUtils'; import { normalizePath } from '../../../../common/utils/pathUtils'; import { getVenvPythonPath } from '../../../../common/utils/virtualEnvironment'; @@ -104,6 +106,7 @@ suite('InlineScriptEnvManager', () => { let nativeFinder: NativePythonFinder; let promptInstallPythonViaUvStub: sinon.SinonStub; let readMetadataStub: sinon.SinonStub; + let sendTelemetryStub: sinon.SinonStub; let inspectMetaStub: sinon.SinonStub; let retainLockStub: sinon.SinonStub; let releaseLockStub: sinon.SinonStub; @@ -152,9 +155,12 @@ suite('InlineScriptEnvManager', () => { computeCacheKeyStub = sinon.stub(cacheKey, 'computeCacheKey').returns(CACHE_KEY); getAvailablePythonVersionsStub = sinon.stub(uvPythonInstaller, 'getAvailablePythonVersions').resolves([]); ensureUvForVersionLookupStub = sinon - .stub(uvPythonInstaller, 'ensureUvForInlineScriptVersionLookup') - .resolves(true); - promptInstallPythonViaUvStub = sinon.stub(uvPythonInstaller, 'promptInstallPythonViaUv'); + .stub(uvPythonInstaller, 'ensureUvForInlineScriptVersionLookupDetailed') + .resolves('available'); + promptInstallPythonViaUvStub = sinon + .stub(uvPythonInstaller, 'promptInstallPythonViaUvDetailed') + .resolves({ kind: 'declined' }); + sendTelemetryStub = sinon.stub(telemetrySender, 'sendTelemetryEvent'); inspectMetaStub = sinon.stub(cacheLayout, 'inspectMetaJson').resolves({ kind: 'missing' }); baseInterpreterStatusStub = sinon.stub(cacheLayout, 'getBaseInterpreterStatus').resolves('available'); writeMetaStub = sinon.stub(cacheLayout, 'writeMetaJson').resolves(); @@ -228,6 +234,16 @@ suite('InlineScriptEnvManager', () => { return new Promise((resolve) => setImmediate(resolve)); } + function telemetryCalls(eventName: EventNames): sinon.SinonSpyCall[] { + return sendTelemetryStub.getCalls().filter((call) => call.args[0] === eventName); + } + + function assertNoInlineScriptLifecycleTelemetry(): void { + assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_CREATED).length, 0); + assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_REUSE_HIT).length, 0); + assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_ERROR).length, 0); + } + suite('static metadata and deferred methods', () => { test('exposes creation but leaves later-phase methods empty', async () => { const asInterface: EnvironmentManager = manager; @@ -386,7 +402,7 @@ suite('InlineScriptEnvManager', () => { apiGetEnvironmentsStub.onFirstCall().resolves([baseEnvironment]); apiGetEnvironmentsStub.onSecondCall().resolves([baseEnvironment]); apiGetEnvironmentsStub.onThirdCall().resolves([uvBase]); - promptInstallPythonViaUvStub.resolves(uvExecutable); + promptInstallPythonViaUvStub.resolves({ kind: 'installed', pythonPath: uvExecutable }); assert.ok(await manager.create(scriptUri())); @@ -407,7 +423,7 @@ suite('InlineScriptEnvManager', () => { apiGetEnvironmentsStub.onFirstCall().resolves([]); apiGetEnvironmentsStub.onSecondCall().resolves([]); apiGetEnvironmentsStub.onThirdCall().resolves([uvBase]); - promptInstallPythonViaUvStub.resolves(uvExecutable); + promptInstallPythonViaUvStub.resolves({ kind: 'installed', pythonPath: uvExecutable }); assert.ok(await manager.create(scriptUri())); @@ -422,7 +438,7 @@ suite('InlineScriptEnvManager', () => { test('does not mutate the cache when the user declines installation', async () => { readMetadataStub.resolves({ ...VALID_METADATA, requiresPython: '>=3.13' }); - promptInstallPythonViaUvStub.resolves(undefined); + promptInstallPythonViaUvStub.resolves({ kind: 'declined' }); assert.strictEqual(await manager.create(scriptUri()), undefined); @@ -449,7 +465,7 @@ suite('InlineScriptEnvManager', () => { const uvBase = makeEnvironment('ms-python.python:system', '3.13.2', uvExecutable); readMetadataStub.resolves({ ...VALID_METADATA, requiresPython: '>=3.13' }); apiGetEnvironmentsStub.resolves([baseEnvironment]); - promptInstallPythonViaUvStub.resolves(uvExecutable); + promptInstallPythonViaUvStub.resolves({ kind: 'installed', pythonPath: uvExecutable }); apiRefreshEnvironmentsStub.rejects(new Error('discovery failed')); resolveSystemPythonStub.resolves(uvBase); @@ -474,7 +490,7 @@ suite('InlineScriptEnvManager', () => { apiGetEnvironmentsStub.onFirstCall().resolves([baseEnvironment]); apiGetEnvironmentsStub.onSecondCall().resolves([baseEnvironment]); apiGetEnvironmentsStub.onThirdCall().rejects(new Error('discovery failed')); - promptInstallPythonViaUvStub.resolves(uvExecutable); + promptInstallPythonViaUvStub.resolves({ kind: 'installed', pythonPath: uvExecutable }); resolveSystemPythonStub.resolves(uvBase); assert.ok(await manager.create(scriptUri())); @@ -524,7 +540,7 @@ suite('InlineScriptEnvManager', () => { arch: 'x86_64', }, ]); - promptInstallPythonViaUvStub.resolves(uvExecutable); + promptInstallPythonViaUvStub.resolves({ kind: 'installed', pythonPath: uvExecutable }); assert.ok(await manager.create(scriptUri())); @@ -552,7 +568,7 @@ suite('InlineScriptEnvManager', () => { makeUvPythonVersion('3.13.3'), makeUvPythonVersion('3.13.0'), ]); - promptInstallPythonViaUvStub.resolves(uvExecutable); + promptInstallPythonViaUvStub.resolves({ kind: 'installed', pythonPath: uvExecutable }); assert.ok(await manager.create(scriptUri())); @@ -571,7 +587,7 @@ suite('InlineScriptEnvManager', () => { apiGetEnvironmentsStub.onSecondCall().resolves([baseEnvironment]); apiGetEnvironmentsStub.onThirdCall().resolves([uvBase]); getAvailablePythonVersionsStub.resolves([makeUvPythonVersion('3.11.14')]); - promptInstallPythonViaUvStub.resolves(uvExecutable); + promptInstallPythonViaUvStub.resolves({ kind: 'installed', pythonPath: uvExecutable }); assert.ok(await manager.create(scriptUri())); @@ -590,7 +606,7 @@ suite('InlineScriptEnvManager', () => { apiGetEnvironmentsStub.onSecondCall().resolves([baseEnvironment]); apiGetEnvironmentsStub.onThirdCall().resolves([uvBase]); getAvailablePythonVersionsStub.resolves([]); - promptInstallPythonViaUvStub.resolves(uvExecutable); + promptInstallPythonViaUvStub.resolves({ kind: 'installed', pythonPath: uvExecutable }); assert.ok(await manager.create(scriptUri())); @@ -613,7 +629,7 @@ suite('InlineScriptEnvManager', () => { makeUvPythonVersion('3.15.0a6'), makeUvPythonVersion('3.14.2'), ]); - promptInstallPythonViaUvStub.resolves(uvExecutable); + promptInstallPythonViaUvStub.resolves({ kind: 'installed', pythonPath: uvExecutable }); assert.ok(await manager.create(scriptUri())); @@ -631,7 +647,7 @@ suite('InlineScriptEnvManager', () => { apiGetEnvironmentsStub.onFirstCall().resolves([baseEnvironment]); apiGetEnvironmentsStub.onSecondCall().resolves([baseEnvironment]); apiGetEnvironmentsStub.onThirdCall().resolves([uvBase]); - promptInstallPythonViaUvStub.resolves(uvExecutable); + promptInstallPythonViaUvStub.resolves({ kind: 'installed', pythonPath: uvExecutable }); assert.ok(await manager.create(scriptUri())); @@ -649,7 +665,7 @@ suite('InlineScriptEnvManager', () => { apiGetEnvironmentsStub.onFirstCall().resolves([baseEnvironment]); apiGetEnvironmentsStub.onSecondCall().resolves([baseEnvironment]); apiGetEnvironmentsStub.onThirdCall().resolves([uvBase]); - promptInstallPythonViaUvStub.resolves(uvExecutable); + promptInstallPythonViaUvStub.resolves({ kind: 'installed', pythonPath: uvExecutable }); assert.ok(await manager.create(scriptUri())); @@ -682,7 +698,7 @@ suite('InlineScriptEnvManager', () => { apiGetEnvironmentsStub.onFirstCall().resolves([baseEnvironment]); apiGetEnvironmentsStub.onSecondCall().resolves([baseEnvironment]); apiGetEnvironmentsStub.onThirdCall().resolves(refreshedEnvironments); - promptInstallPythonViaUvStub.resolves(baseExecutable); + promptInstallPythonViaUvStub.resolves({ kind: 'installed', pythonPath: baseExecutable }); assert.strictEqual(await manager.create(scriptUri()), undefined); @@ -730,7 +746,7 @@ suite('InlineScriptEnvManager', () => { signalPrompt!(); await installGate; installed = true; - return uvExecutable; + return { kind: 'installed', pythonPath: uvExecutable }; }); const first = manager.create(uri); @@ -750,23 +766,23 @@ suite('InlineScriptEnvManager', () => { const uri = scriptUri(); readMetadataStub.resolves({ ...VALID_METADATA, requiresPython: '>=3.13' }); apiGetEnvironmentsStub.resolves([baseEnvironment]); - let finishPrompt: ((value: undefined) => void) | undefined; + let finishPrompt: (() => void) | undefined; let signalPrompt: (() => void) | undefined; const promptShown = new Promise((resolve) => { signalPrompt = resolve; }); promptInstallPythonViaUvStub.callsFake( () => - new Promise((resolve) => { + new Promise<{ kind: 'declined' }>((resolve) => { signalPrompt!(); - finishPrompt = resolve; + finishPrompt = () => resolve({ kind: 'declined' }); }), ); const first = manager.create(uri); await promptShown; const second = manager.create(uri); - finishPrompt!(undefined); + finishPrompt!(); assert.deepStrictEqual(await Promise.all([first, second]), [undefined, undefined]); assert.deepStrictEqual(await Promise.all([first, second]), [undefined, undefined]); assert.strictEqual(promptInstallPythonViaUvStub.callCount, 1); @@ -809,7 +825,7 @@ suite('InlineScriptEnvManager', () => { signalPrompt!(); await installGate; isInstalled = true; - return uvExecutable; + return { kind: 'installed', pythonPath: uvExecutable }; }); const first = manager.create(scriptUri('a.py')); @@ -862,7 +878,7 @@ suite('InlineScriptEnvManager', () => { signalPrompt!(); await installGate; installed = true; - return uvExecutable; + return { kind: 'installed', pythonPath: uvExecutable }; }); const first = manager.create(scriptUri('lower-bound.py')); @@ -898,7 +914,7 @@ suite('InlineScriptEnvManager', () => { promptInstallPythonViaUvStub.callsFake(async () => { signalPrompt!(); await installGate; - return uvExecutable; + return { kind: 'installed', pythonPath: uvExecutable }; }); const first = manager.create(scriptUri('first.py')); @@ -925,7 +941,7 @@ suite('InlineScriptEnvManager', () => { apiGetEnvironmentsStub.onThirdCall().resolves([baseEnvironment]); apiGetEnvironmentsStub.onCall(3).rejects(new Error('discovery unavailable')); resolveSystemPythonStub.resolves(uvBase); - promptInstallPythonViaUvStub.resolves(uvExecutable); + promptInstallPythonViaUvStub.resolves({ kind: 'installed', pythonPath: uvExecutable }); assert.ok(await manager.create(scriptUri('first.py'))); assert.ok(await manager.create(scriptUri('second.py'))); @@ -1485,6 +1501,305 @@ suite('InlineScriptEnvManager', () => { }); }); + suite('telemetry', () => { + test('does not emit lifecycle telemetry for non-applicable create calls', async () => { + readMetadataStub.resolves(undefined); + + assert.strictEqual(await manager.create('global'), undefined); + assert.strictEqual(await manager.create(scriptUri()), undefined); + assertNoInlineScriptLifecycleTelemetry(); + }); + + test('emits envCreated with only duration and dependencyCount after verified creation', async () => { + assert.ok(await manager.create(scriptUri())); + + const createdCalls = telemetryCalls(EventNames.INLINE_SCRIPT_ENV_CREATED); + assert.strictEqual(createdCalls.length, 1); + assert.deepStrictEqual(createdCalls[0].args, [ + EventNames.INLINE_SCRIPT_ENV_CREATED, + { duration: 0, dependencyCount: 1 }, + ]); + assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_REUSE_HIT).length, 0); + assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_ERROR).length, 0); + }); + + test('emits envReuseHit only for validated cache hits', async () => { + await fs.ensureDir(envDir().fsPath); + setSidecar({ + schemaVersion: cacheLayout.META_SCHEMA_VERSION, + baseInterpreterPath: baseExecutable, + baseInterpreterVersion: baseEnvironment.version, + lastUsedAt: NOW.toISOString(), + }); + const cached = makeEnvironment( + 'ms-python.python:inline-script', + '3.12.4', + venvPythonPath(envDir().fsPath), + envDir().fsPath, + ); + await fs.outputFile(venvPythonPath(envDir().fsPath), ''); + resolveVenvStub.resolves(cached); + + assert.strictEqual(await manager.create(scriptUri()), cached); + + const reuseCalls = telemetryCalls(EventNames.INLINE_SCRIPT_ENV_REUSE_HIT); + assert.strictEqual(reuseCalls.length, 1); + assert.deepStrictEqual(reuseCalls[0].args, [EventNames.INLINE_SCRIPT_ENV_REUSE_HIT]); + assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_CREATED).length, 0); + assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_ERROR).length, 0); + }); + + test('emits a single compatible-python-declined error for coalesced same-script requests', async () => { + const uri = scriptUri(); + readMetadataStub.resolves({ ...VALID_METADATA, requiresPython: '>=3.13' }); + apiGetEnvironmentsStub.resolves([baseEnvironment]); + let finishPrompt: (() => void) | undefined; + let signalPrompt: (() => void) | undefined; + const promptShown = new Promise((resolve) => { + signalPrompt = resolve; + }); + promptInstallPythonViaUvStub.callsFake( + () => + new Promise<{ kind: 'declined' }>((resolve) => { + signalPrompt!(); + finishPrompt = () => resolve({ kind: 'declined' }); + }), + ); + + const first = manager.create(uri); + await promptShown; + const second = manager.create(uri); + finishPrompt!(); + + assert.deepStrictEqual(await Promise.all([first, second]), [undefined, undefined]); + assert.strictEqual(promptInstallPythonViaUvStub.callCount, 1); + assert.deepStrictEqual( + telemetryCalls(EventNames.INLINE_SCRIPT_ENV_ERROR).map((call) => call.args), + [[EventNames.INLINE_SCRIPT_ENV_ERROR, undefined, { category: 'compatible-python-declined' }]], + ); + assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_CREATED).length, 0); + assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_REUSE_HIT).length, 0); + }); + + test('emits no-compatible-python when quick create cannot prompt for a compatible interpreter', async () => { + readMetadataStub.resolves({ ...VALID_METADATA, requiresPython: '>=3.13' }); + apiGetEnvironmentsStub.resolves([baseEnvironment]); + + assert.strictEqual(await manager.create(scriptUri(), { quickCreate: true }), undefined); + + assert.strictEqual(promptInstallPythonViaUvStub.callCount, 0); + assert.deepStrictEqual( + telemetryCalls(EventNames.INLINE_SCRIPT_ENV_ERROR).map((call) => call.args), + [[EventNames.INLINE_SCRIPT_ENV_ERROR, undefined, { category: 'no-compatible-python' }]], + ); + assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_CREATED).length, 0); + }); + + test('emits discovery-failure when quick create cannot inspect discovered interpreters', async () => { + readMetadataStub.resolves({ ...VALID_METADATA, requiresPython: '>=3.13' }); + apiGetEnvironmentsStub.rejects(new Error('discovery unavailable')); + + assert.strictEqual(await manager.create(scriptUri(), { quickCreate: true }), undefined); + + assert.strictEqual(promptInstallPythonViaUvStub.callCount, 0); + assert.deepStrictEqual( + telemetryCalls(EventNames.INLINE_SCRIPT_ENV_ERROR).map((call) => call.args), + [[EventNames.INLINE_SCRIPT_ENV_ERROR, undefined, { category: 'discovery-failure' }]], + ); + }); + + test('emits discovery-failure instead of compatible-python-declined when discovery is unavailable', async () => { + const uri = scriptUri(); + readMetadataStub.resolves({ ...VALID_METADATA, requiresPython: '>=3.13' }); + apiGetEnvironmentsStub.rejects(new Error('discovery unavailable')); + + assert.strictEqual(await manager.create(uri), undefined); + + assert.strictEqual(promptInstallPythonViaUvStub.callCount, 1); + assert.deepStrictEqual( + telemetryCalls(EventNames.INLINE_SCRIPT_ENV_ERROR).map((call) => call.args), + [[EventNames.INLINE_SCRIPT_ENV_ERROR, undefined, { category: 'discovery-failure' }]], + ); + }); + + test('emits discovery-failure instead of install-failure when discovery never recovers', async () => { + const uvExecutable = path.join(tempRoot, 'uv-python', isWindows() ? 'python.exe' : 'python'); + await fs.outputFile(uvExecutable, ''); + readMetadataStub.resolves({ ...VALID_METADATA, requiresPython: '>=3.13' }); + apiGetEnvironmentsStub.rejects(new Error('discovery unavailable')); + promptInstallPythonViaUvStub.resolves({ kind: 'installed', pythonPath: uvExecutable }); + resolveSystemPythonStub.resolves(undefined); + + assert.strictEqual(await manager.create(scriptUri()), undefined); + + assert.strictEqual(promptInstallPythonViaUvStub.callCount, 1); + assert.strictEqual(resolveSystemPythonStub.callCount, 1); + assert.deepStrictEqual( + telemetryCalls(EventNames.INLINE_SCRIPT_ENV_ERROR).map((call) => call.args), + [[EventNames.INLINE_SCRIPT_ENV_ERROR, undefined, { category: 'discovery-failure' }]], + ); + }); + + test('emits a single envCreated event for coalesced same-key creation', async () => { + let continueCreation: (() => void) | undefined; + let creationStarted: (() => void) | undefined; + const started = new Promise((resolve) => { + creationStarted = resolve; + }); + const gate = new Promise((resolve) => { + continueCreation = resolve; + }); + createWithProgressStub.callsFake(async (...args: unknown[]) => { + const target = args[6] as string; + await fs.outputFile(venvPythonPath(target), ''); + creationStarted!(); + await gate; + return { + environment: makeEnvironment( + 'ms-python.python:inline-script', + '3.12.4', + venvPythonPath(target), + target, + ), + }; + }); + + const first = manager.create(scriptUri('a.py')); + await started; + const second = manager.create(scriptUri('b.py')); + continueCreation!(); + + const [firstResult, secondResult] = await Promise.all([first, second]); + assert.ok(firstResult); + assert.strictEqual(firstResult, secondResult); + assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_CREATED).length, 1); + assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_REUSE_HIT).length, 0); + assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_ERROR).length, 0); + }); + + test('excludes lock and cache inspection time from envCreated duration', async () => { + await fs.ensureDir(envDir().fsPath); + lockStub.callsFake(async () => { + clock.tick(3_000); + return { release: releaseLockStub, retain: retainLockStub }; + }); + inspectMetaStub.callsFake(async () => { + clock.tick(2_000); + return { kind: 'missing' }; + }); + createWithProgressStub.callsFake(async (...args: unknown[]) => { + const target = args[6] as string; + clock.tick(25); + await fs.outputFile(venvPythonPath(target), ''); + return { + environment: makeEnvironment( + 'ms-python.python:inline-script', + '3.12.4', + venvPythonPath(target), + target, + ), + }; + }); + + assert.ok(await manager.create(scriptUri())); + + const createdCalls = telemetryCalls(EventNames.INLINE_SCRIPT_ENV_CREATED); + assert.strictEqual(createdCalls.length, 1); + assert.deepStrictEqual(createdCalls[0].args, [ + EventNames.INLINE_SCRIPT_ENV_CREATED, + { duration: 25, dependencyCount: 1 }, + ]); + }); + + test('emits lock-timeout when the cache lock cannot be acquired', async () => { + lockStub.rejects(Object.assign(new Error('already locked'), { code: 'ELOCKED' })); + + assert.strictEqual(await manager.create(scriptUri()), undefined); + + assert.deepStrictEqual( + telemetryCalls(EventNames.INLINE_SCRIPT_ENV_ERROR).map((call) => call.args), + [[EventNames.INLINE_SCRIPT_ENV_ERROR, undefined, { category: 'lock-timeout' }]], + ); + assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_CREATED).length, 0); + }); + + for (const code of ['ELOCKRETAINED', 'ELOCKORPHANED'] as const) { + test(`emits lock-unavailable when cache lock acquisition fails with ${code}`, async () => { + lockStub.rejects(Object.assign(new Error('lock unavailable'), { code })); + + assert.strictEqual(await manager.create(scriptUri()), undefined); + + assert.deepStrictEqual( + telemetryCalls(EventNames.INLINE_SCRIPT_ENV_ERROR).map((call) => call.args), + [[EventNames.INLINE_SCRIPT_ENV_ERROR, undefined, { category: 'lock-unavailable' }]], + ); + assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_CREATED).length, 0); + }); + } + + test('emits package-install-cancelled and no success event on rollback', async () => { + createWithProgressStub.callsFake(async (...args: unknown[]) => { + const target = args[6] as string; + await fs.outputFile(venvPythonPath(target), ''); + return { + environment: makeEnvironment( + 'ms-python.python:inline-script', + '3.12.4', + venvPythonPath(target), + target, + ), + pkgInstallationErr: 'Canceled', + pkgInstallationCancelled: true, + }; + }); + + assert.strictEqual(await manager.create(scriptUri()), undefined); + + assert.deepStrictEqual( + telemetryCalls(EventNames.INLINE_SCRIPT_ENV_ERROR).map((call) => call.args), + [[EventNames.INLINE_SCRIPT_ENV_ERROR, undefined, { category: 'package-install-cancelled' }]], + ); + assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_CREATED).length, 0); + }); + + test('emits install-failure when sidecar persistence rollback removes the new environment', async () => { + writeMetaStub.rejects(new Error('disk full')); + + assert.strictEqual(await manager.create(scriptUri()), undefined); + + assert.deepStrictEqual( + telemetryCalls(EventNames.INLINE_SCRIPT_ENV_ERROR).map((call) => call.args), + [[EventNames.INLINE_SCRIPT_ENV_ERROR, undefined, { category: 'install-failure' }]], + ); + assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_CREATED).length, 0); + }); + + test('rebuilds a failed reuse validation as creation without counting a reuse hit', async () => { + await fs.ensureDir(envDir().fsPath); + setSidecar({ + schemaVersion: cacheLayout.META_SCHEMA_VERSION, + baseInterpreterPath: baseExecutable, + baseInterpreterVersion: baseEnvironment.version, + lastUsedAt: NOW.toISOString(), + }); + await fs.outputFile(venvPythonPath(envDir().fsPath), ''); + resolveVenvStub.resolves( + makeEnvironment( + 'ms-python.python:inline-script', + '3.10.0', + venvPythonPath(envDir().fsPath), + envDir().fsPath, + ), + ); + + assert.ok(await manager.create(scriptUri())); + + assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_CREATED).length, 1); + assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_REUSE_HIT).length, 0); + assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_ERROR).length, 0); + }); + }); + suite('script association persistence', () => { test('sets, gets, unsets, persists, and reports only actual selection changes', async () => { const uri = scriptUri(); From 15c38c72584e2bab19263dfe3bbc733108a33424 Mon Sep 17 00:00:00 2001 From: Stella Huang Date: Mon, 17 Aug 2026 17:22:21 -0700 Subject: [PATCH 2/4] Expand inline script telemetry coverage Cover detailed uv outcomes and normalized dependency counts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6b12d843-8011-4bfc-9ba9-f75761eadee2 --- .../inlineScript/envManager.unit.test.ts | 13 +++++ .../builtin/uvPythonInstaller.unit.test.ts | 56 ++++++++++++++++++- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/test/managers/builtin/inlineScript/envManager.unit.test.ts b/src/test/managers/builtin/inlineScript/envManager.unit.test.ts index ff06d9d0..8b9ca2af 100644 --- a/src/test/managers/builtin/inlineScript/envManager.unit.test.ts +++ b/src/test/managers/builtin/inlineScript/envManager.unit.test.ts @@ -1523,6 +1523,19 @@ suite('InlineScriptEnvManager', () => { assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_ERROR).length, 0); }); + test('deduplicates normalized dependencies for envCreated dependencyCount', async () => { + readMetadataStub.resolves({ ...VALID_METADATA, dependencies: ['Requests', 'requests'] }); + + assert.ok(await manager.create(scriptUri())); + + const createdCalls = telemetryCalls(EventNames.INLINE_SCRIPT_ENV_CREATED); + assert.strictEqual(createdCalls.length, 1); + assert.deepStrictEqual(createdCalls[0].args, [ + EventNames.INLINE_SCRIPT_ENV_CREATED, + { duration: 0, dependencyCount: 1 }, + ]); + }); + test('emits envReuseHit only for validated cache hits', async () => { await fs.ensureDir(envDir().fsPath); setSidecar({ diff --git a/src/test/managers/builtin/uvPythonInstaller.unit.test.ts b/src/test/managers/builtin/uvPythonInstaller.unit.test.ts index cf99c84f..b2d55658 100644 --- a/src/test/managers/builtin/uvPythonInstaller.unit.test.ts +++ b/src/test/managers/builtin/uvPythonInstaller.unit.test.ts @@ -11,10 +11,12 @@ import * as windowApis from '../../../common/window.apis'; import * as helpers from '../../../managers/builtin/helpers'; import { clearDontAskAgain, + ensureUvForInlineScriptVersionLookupDetailed, ensureUvForInlineScriptVersionLookup, getAvailablePythonVersions, getUvPythonPath, isDontAskAgainSet, + promptInstallPythonViaUvDetailed, promptInstallPythonViaUv, UV_INSTALL_PYTHON_DONT_ASK_KEY, UvPythonVersion, @@ -68,6 +70,15 @@ suite('uvPythonInstaller - promptInstallPythonViaUv', () => { return executeTaskStub; } + test('should report available from the detailed uv lookup API', async () => { + isUvInstalledStub.resolves(true); + + const result = await ensureUvForInlineScriptVersionLookupDetailed('>=3.13,<3.14', mockLog); + + assert.strictEqual(result, 'available'); + assert(showInformationMessageStub.notCalled, 'Should not prompt when uv is already available'); + }); + test('should return undefined when "Don\'t ask again" is set', async () => { mockState.get.resolves(true); @@ -96,6 +107,15 @@ suite('uvPythonInstaller - promptInstallPythonViaUv', () => { ); }); + test('should report a declined detailed uv lookup distinctly from the boolean wrapper', async () => { + isUvInstalledStub.resolves(false); + showInformationMessageStub.resolves(undefined); + + const result = await ensureUvForInlineScriptVersionLookupDetailed('>=3.13,<3.14', mockLog); + + assert.strictEqual(result, 'declined'); + }); + test('should show correct prompt when uv is NOT installed', async () => { mockState.get.resolves(false); isUvInstalledStub.resolves(false); @@ -214,6 +234,28 @@ suite('uvPythonInstaller - promptInstallPythonViaUv', () => { assert(isUvInstalledStub.notCalled, 'Should stop before checking or installing uv'); }); + test('should report a failed detailed Python install prompt distinctly from the undefined wrapper', async () => { + mockState.get.resolves(false); + + const result = await promptInstallPythonViaUvDetailed('inlineScript', mockLog, { + requiresPython: '>=3.13', + version: 'latest\nInstall anyway', + }); + + assert.deepStrictEqual(result, { kind: 'failed' }); + assert(showInformationMessageStub.notCalled, 'Should not display an invalid install version'); + }); + + test('should report a declined detailed Python install prompt distinctly from the undefined wrapper', async () => { + mockState.get.resolves(false); + isUvInstalledStub.resolves(true); + showInformationMessageStub.resolves(undefined); + + const result = await promptInstallPythonViaUvDetailed('activation', mockLog); + + assert.deepStrictEqual(result, { kind: 'declined' }); + }); + test('should allow a validated prerelease install version', async () => { mockState.get.resolves(false); isUvInstalledStub.resolves(true); @@ -252,6 +294,16 @@ suite('uvPythonInstaller - promptInstallPythonViaUv', () => { assert.strictEqual(showErrorMessageStub.callCount, 0); }); + test('should report a failed detailed uv lookup distinctly from the boolean wrapper', async () => { + isUvInstalledStub.resolves(false); + showInformationMessageStub.resolves(UvInstallStrings.installUv); + stubUvInstallTask(1); + + const result = await ensureUvForInlineScriptVersionLookupDetailed('>=3.13,<3.14', mockLog); + + assert.strictEqual(result, 'failed'); + }); + test('should stop version lookup when uv installation fails', async () => { isUvInstalledStub.resolves(false); showInformationMessageStub.resolves(UvInstallStrings.installUv); @@ -402,7 +454,7 @@ suite('uvPythonInstaller - promptInstallPythonViaUv', () => { const spawnStub: sinon.SinonStub = sinon.stub(childProcessApis, 'spawnProcess'); spawnStub.returns(mockProcess); - const resultPromise = promptInstallPythonViaUv('inlineScript', mockLog, { + const resultPromise = promptInstallPythonViaUvDetailed('inlineScript', mockLog, { requiresPython: '>=3.13', version: '3.13', }); @@ -411,7 +463,7 @@ suite('uvPythonInstaller - promptInstallPythonViaUv', () => { mockProcess.emit('exit', 0, null); }, 10); - assert.strictEqual(await resultPromise, '/usr/bin/python3.13'); + assert.deepStrictEqual(await resultPromise, { kind: 'installed', pythonPath: '/usr/bin/python3.13' }); const installTask = executeTaskStub.firstCall.args[0]; const execution = installTask.execution as ShellExecution; assert.strictEqual(execution.command, 'uv'); From d9692b30a3950c824a31b8fd1451742472f665f2 Mon Sep 17 00:00:00 2001 From: Stella Huang Date: Mon, 17 Aug 2026 20:00:01 -0700 Subject: [PATCH 3/4] Localize inline telemetry test helpers Keep telemetry-only helpers scoped to their consuming test suite. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6b12d843-8011-4bfc-9ba9-f75761eadee2 --- .../inlineScript/envManager.unit.test.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/test/managers/builtin/inlineScript/envManager.unit.test.ts b/src/test/managers/builtin/inlineScript/envManager.unit.test.ts index 8b9ca2af..127f3795 100644 --- a/src/test/managers/builtin/inlineScript/envManager.unit.test.ts +++ b/src/test/managers/builtin/inlineScript/envManager.unit.test.ts @@ -234,16 +234,6 @@ suite('InlineScriptEnvManager', () => { return new Promise((resolve) => setImmediate(resolve)); } - function telemetryCalls(eventName: EventNames): sinon.SinonSpyCall[] { - return sendTelemetryStub.getCalls().filter((call) => call.args[0] === eventName); - } - - function assertNoInlineScriptLifecycleTelemetry(): void { - assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_CREATED).length, 0); - assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_REUSE_HIT).length, 0); - assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_ERROR).length, 0); - } - suite('static metadata and deferred methods', () => { test('exposes creation but leaves later-phase methods empty', async () => { const asInterface: EnvironmentManager = manager; @@ -1502,6 +1492,16 @@ suite('InlineScriptEnvManager', () => { }); suite('telemetry', () => { + function telemetryCalls(eventName: EventNames): sinon.SinonSpyCall[] { + return sendTelemetryStub.getCalls().filter((call) => call.args[0] === eventName); + } + + function assertNoInlineScriptLifecycleTelemetry(): void { + assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_CREATED).length, 0); + assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_REUSE_HIT).length, 0); + assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_ERROR).length, 0); + } + test('does not emit lifecycle telemetry for non-applicable create calls', async () => { readMetadataStub.resolves(undefined); From ad4627458bac3a11b702dea6e983ee1657e2fe65 Mon Sep 17 00:00:00 2001 From: Stella Huang Date: Tue, 18 Aug 2026 16:39:51 -0700 Subject: [PATCH 4/4] Correct inline script telemetry contracts Report reuse dependency counts and preserve accurate final failure outcomes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6b12d843-8011-4bfc-9ba9-f75761eadee2 --- src/common/telemetry/constants.ts | 12 ++++- .../builtin/inlineScript/envManager.ts | 38 ++++++---------- .../inlineScript/envManager.unit.test.ts | 44 ++++++++++++++++--- 3 files changed, 61 insertions(+), 33 deletions(-) diff --git a/src/common/telemetry/constants.ts b/src/common/telemetry/constants.ts index c718e6a8..5e12d5f3 100644 --- a/src/common/telemetry/constants.ts +++ b/src/common/telemetry/constants.ts @@ -233,6 +233,8 @@ export enum EventNames { /** * Telemetry event fired when inline-script environment creation validates * and reuses an existing cache entry without rebuilding it. + * Measures: + * - dependencyCount: number (normalized dependency count in the cache key) */ INLINE_SCRIPT_ENV_REUSE_HIT = 'inlineScript.envReuseHit', /** @@ -259,6 +261,7 @@ export type InlineScriptEnvErrorCategory = | 'no-compatible-python' | 'package-install-cancelled' | 'install-failure' + | 'setup-failure' | 'lock-timeout' | 'lock-unavailable'; @@ -737,9 +740,14 @@ export interface IEventNamePropertyMapping { }; /* __GDPR__ - "inlineScript.envReuseHit": {"owner": "StellaHuang95" } + "inlineScript.envReuseHit": { + "dependencyCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "StellaHuang95" } + } */ - [EventNames.INLINE_SCRIPT_ENV_REUSE_HIT]: never | undefined; + [EventNames.INLINE_SCRIPT_ENV_REUSE_HIT]: { + // Goes through the measures payload (numeric); listed here for GDPR only. + dependencyCount?: number; + }; /* __GDPR__ "inlineScript.envError": { diff --git a/src/managers/builtin/inlineScript/envManager.ts b/src/managers/builtin/inlineScript/envManager.ts index 00e5883b..3390da8c 100644 --- a/src/managers/builtin/inlineScript/envManager.ts +++ b/src/managers/builtin/inlineScript/envManager.ts @@ -185,7 +185,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable { } } } catch (error) { - this.sendInlineScriptEnvErrorTelemetry('install-failure'); + this.sendInlineScriptEnvErrorTelemetry('setup-failure'); this.log.error(`Failed to set up inline-script environment: ${getErrorMessage(error)}`); return undefined; } @@ -257,7 +257,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable { } if (quickCreate) { return { - errorCategory: this.getBaseInterpreterErrorCategory(selection.discoveryFailed, 'no-compatible-python'), + errorCategory: selection.discoveryFailed ? 'discovery-failure' : 'no-compatible-python', }; } return this.installAndSelectBaseInterpreter(metadata, selection.discoveryFailed); @@ -940,20 +940,15 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable { 'Cannot install a Python for this inline script because no compatible install version could be selected.', ); return { - errorCategory: this.getBaseInterpreterErrorCategory( - discoveryFailed, - versionSelection.errorCategory ?? 'no-compatible-python', - ), + errorCategory: versionSelection.errorCategory ?? 'no-compatible-python', }; } const installResult = await this.installPythonAndRefresh(requiresPython, versionSelection.version); if (installResult.kind !== 'installed') { return { - errorCategory: this.getBaseInterpreterErrorCategory( - discoveryFailed, + errorCategory: installResult.kind === 'declined' ? 'compatible-python-declined' : 'install-failure', - ), }; } const installedPath = installResult.installedPath; @@ -991,7 +986,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable { 'Python was installed for an inline script, but no compatible base interpreter was discovered after refreshing environments.', ); return { - errorCategory: this.getBaseInterpreterErrorCategory(discoveryFailedAfterInstall, 'install-failure'), + errorCategory: discoveryFailedAfterInstall ? 'discovery-failure' : 'setup-failure', }; } return { selectedBase: selected }; @@ -1140,19 +1135,19 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable { const cached = await this.inspectCacheEntry(cacheRoot, envDir, metadata, selectedBase); if (cached.kind === 'reusable') { - this.sendInlineScriptEnvReuseHitTelemetry(); + this.sendInlineScriptEnvReuseHitTelemetry(dependencyCount); return cached.environment; } if (cached.kind === 'uncertain') { this.log.warn( `Preserving an inline-script cache entry that could not be safely inspected: ${envDir.fsPath}`, ); - this.sendInlineScriptEnvErrorTelemetry('install-failure'); + this.sendInlineScriptEnvErrorTelemetry('setup-failure'); return undefined; } if (cached.kind === 'stale') { if (!(await this.removeCacheEntry(envDir))) { - this.sendInlineScriptEnvErrorTelemetry('install-failure'); + this.sendInlineScriptEnvErrorTelemetry('setup-failure'); return undefined; } } @@ -1313,7 +1308,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable { ) { this.log.error('Created inline-script environment does not match the requested cache entry.'); await this.removeCacheEntry(envDir); - return { errorCategory: 'install-failure' }; + return { errorCategory: 'setup-failure' }; } try { @@ -1326,7 +1321,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable { } catch (error) { this.log.error(`Failed to record inline-script cache metadata: ${getErrorMessage(error)}`); await this.removeCacheEntry(envDir); - return { errorCategory: 'install-failure' }; + return { errorCategory: 'setup-failure' }; } return { environment: result.environment }; @@ -1374,21 +1369,14 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable { }); } - private sendInlineScriptEnvReuseHitTelemetry(): void { - sendTelemetryEvent(EventNames.INLINE_SCRIPT_ENV_REUSE_HIT); + private sendInlineScriptEnvReuseHitTelemetry(dependencyCount: number): void { + sendTelemetryEvent(EventNames.INLINE_SCRIPT_ENV_REUSE_HIT, { dependencyCount }); } private sendInlineScriptEnvErrorTelemetry(category: InlineScriptEnvErrorCategory): void { sendTelemetryEvent(EventNames.INLINE_SCRIPT_ENV_ERROR, undefined, { category }); } - private getBaseInterpreterErrorCategory( - discoveryFailed: boolean, - fallbackCategory: InlineScriptEnvErrorCategory, - ): InlineScriptEnvErrorCategory { - return discoveryFailed ? 'discovery-failure' : fallbackCategory; - } - private getCreateOrReuseErrorCategory(error: unknown): InlineScriptEnvErrorCategory { if ( typeof error === 'object' && @@ -1398,7 +1386,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable { ) { return (error as NodeJS.ErrnoException).code === 'ELOCKED' ? 'lock-timeout' : 'lock-unavailable'; } - return 'install-failure'; + return 'setup-failure'; } dispose(): void { diff --git a/src/test/managers/builtin/inlineScript/envManager.unit.test.ts b/src/test/managers/builtin/inlineScript/envManager.unit.test.ts index 127f3795..940dbb9c 100644 --- a/src/test/managers/builtin/inlineScript/envManager.unit.test.ts +++ b/src/test/managers/builtin/inlineScript/envManager.unit.test.ts @@ -1537,6 +1537,7 @@ suite('InlineScriptEnvManager', () => { }); test('emits envReuseHit only for validated cache hits', async () => { + readMetadataStub.resolves({ ...VALID_METADATA, dependencies: ['Requests', 'requests'] }); await fs.ensureDir(envDir().fsPath); setSidecar({ schemaVersion: cacheLayout.META_SCHEMA_VERSION, @@ -1557,11 +1558,28 @@ suite('InlineScriptEnvManager', () => { const reuseCalls = telemetryCalls(EventNames.INLINE_SCRIPT_ENV_REUSE_HIT); assert.strictEqual(reuseCalls.length, 1); - assert.deepStrictEqual(reuseCalls[0].args, [EventNames.INLINE_SCRIPT_ENV_REUSE_HIT]); + assert.deepStrictEqual(reuseCalls[0].args, [ + EventNames.INLINE_SCRIPT_ENV_REUSE_HIT, + { dependencyCount: 1 }, + ]); assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_CREATED).length, 0); assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_ERROR).length, 0); }); + test('emits setup-failure when cache inspection is unavailable', async () => { + await fs.ensureDir(envDir().fsPath); + inspectMetaStub.resolves({ kind: 'unavailable' }); + + assert.strictEqual(await manager.create(scriptUri()), undefined); + + assert.deepStrictEqual( + telemetryCalls(EventNames.INLINE_SCRIPT_ENV_ERROR).map((call) => call.args), + [[EventNames.INLINE_SCRIPT_ENV_ERROR, undefined, { category: 'setup-failure' }]], + ); + assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_CREATED).length, 0); + assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_REUSE_HIT).length, 0); + }); + test('emits a single compatible-python-declined error for coalesced same-script requests', async () => { const uri = scriptUri(); readMetadataStub.resolves({ ...VALID_METADATA, requiresPython: '>=3.13' }); @@ -1621,7 +1639,7 @@ suite('InlineScriptEnvManager', () => { ); }); - test('emits discovery-failure instead of compatible-python-declined when discovery is unavailable', async () => { + test('emits the final compatible-python-declined outcome when discovery was unavailable', async () => { const uri = scriptUri(); readMetadataStub.resolves({ ...VALID_METADATA, requiresPython: '>=3.13' }); apiGetEnvironmentsStub.rejects(new Error('discovery unavailable')); @@ -1631,11 +1649,25 @@ suite('InlineScriptEnvManager', () => { assert.strictEqual(promptInstallPythonViaUvStub.callCount, 1); assert.deepStrictEqual( telemetryCalls(EventNames.INLINE_SCRIPT_ENV_ERROR).map((call) => call.args), - [[EventNames.INLINE_SCRIPT_ENV_ERROR, undefined, { category: 'discovery-failure' }]], + [[EventNames.INLINE_SCRIPT_ENV_ERROR, undefined, { category: 'compatible-python-declined' }]], + ); + }); + + test('emits the final install-failure outcome when discovery was unavailable', async () => { + readMetadataStub.resolves({ ...VALID_METADATA, requiresPython: '>=3.13' }); + apiGetEnvironmentsStub.rejects(new Error('discovery unavailable')); + promptInstallPythonViaUvStub.resolves({ kind: 'failed' }); + + assert.strictEqual(await manager.create(scriptUri()), undefined); + + assert.strictEqual(promptInstallPythonViaUvStub.callCount, 1); + assert.deepStrictEqual( + telemetryCalls(EventNames.INLINE_SCRIPT_ENV_ERROR).map((call) => call.args), + [[EventNames.INLINE_SCRIPT_ENV_ERROR, undefined, { category: 'install-failure' }]], ); }); - test('emits discovery-failure instead of install-failure when discovery never recovers', async () => { + test('emits discovery-failure when installed Python cannot be discovered or resolved', async () => { const uvExecutable = path.join(tempRoot, 'uv-python', isWindows() ? 'python.exe' : 'python'); await fs.outputFile(uvExecutable, ''); readMetadataStub.resolves({ ...VALID_METADATA, requiresPython: '>=3.13' }); @@ -1775,14 +1807,14 @@ suite('InlineScriptEnvManager', () => { assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_CREATED).length, 0); }); - test('emits install-failure when sidecar persistence rollback removes the new environment', async () => { + test('emits setup-failure when sidecar persistence rollback removes the new environment', async () => { writeMetaStub.rejects(new Error('disk full')); assert.strictEqual(await manager.create(scriptUri()), undefined); assert.deepStrictEqual( telemetryCalls(EventNames.INLINE_SCRIPT_ENV_ERROR).map((call) => call.args), - [[EventNames.INLINE_SCRIPT_ENV_ERROR, undefined, { category: 'install-failure' }]], + [[EventNames.INLINE_SCRIPT_ENV_ERROR, undefined, { category: 'setup-failure' }]], ); assert.strictEqual(telemetryCalls(EventNames.INLINE_SCRIPT_ENV_CREATED).length, 0); });