From e52cfbe70f7005d8d317bc4a7bb15f5e4a45488c Mon Sep 17 00:00:00 2001 From: Florent BENOIT Date: Tue, 11 Apr 2023 14:43:47 +0200 Subject: [PATCH] feat(kind): Add telemetry to see what is happening (#1997) * feat(kind): Add telemetry to see if kind is downloaded or not Also log usage of create clusters fixes https://github.com/containers/podman-desktop/issues/1954 Change-Id: I7f7a59360e5ed32edc3a6b4d4593c9771928705e Signed-off-by: Florent Benoit --- extensions/kind/src/create-cluster.spec.ts | 19 +++++++++++-- extensions/kind/src/create-cluster.ts | 10 +++++++ extensions/kind/src/extension.ts | 32 ++++++++++++++-------- extensions/kind/src/kind-installer.spec.ts | 14 +++++++++- extensions/kind/src/kind-installer.ts | 7 ++++- 5 files changed, 66 insertions(+), 16 deletions(-) diff --git a/extensions/kind/src/create-cluster.spec.ts b/extensions/kind/src/create-cluster.spec.ts index 5191df6dd49..8253b5df68b 100644 --- a/extensions/kind/src/create-cluster.spec.ts +++ b/extensions/kind/src/create-cluster.spec.ts @@ -20,6 +20,7 @@ import { beforeEach, expect, test, vi } from 'vitest'; import type { Mock } from 'vitest'; import { createCluster, getKindClusterConfig } from './create-cluster'; import { runCliCommand } from './util'; +import type { TelemetryLogger } from '@podman-desktop/api'; vi.mock('@podman-desktop/api', async () => { return { @@ -37,19 +38,33 @@ beforeEach(() => { vi.clearAllMocks(); }); +const telemetryLogUsageMock = vi.fn(); +const telemetryLogErrorMock = vi.fn(); +const telemetryLoggerMock = { + logUsage: telemetryLogUsageMock, + logError: telemetryLogErrorMock, +} as unknown as TelemetryLogger; + test('expect error is cli returns non zero exit code', async () => { try { (runCliCommand as Mock).mockReturnValue({ exitCode: -1, error: 'error' }); - await createCluster({}, undefined, ''); + await createCluster({}, undefined, '', undefined, telemetryLoggerMock); } catch (err) { expect(err).to.be.a('Error'); expect(err.message).equal('Failed to create kind cluster. error'); + expect(telemetryLogErrorMock).toBeCalledWith('createCluster', expect.objectContaining({ error: 'error' })); } }); test('expect cluster to be created', async () => { (runCliCommand as Mock).mockReturnValue({ exitCode: 0 }); - await createCluster({}, undefined, ''); + await createCluster({}, undefined, '', undefined, telemetryLoggerMock); + expect(telemetryLogUsageMock).toHaveBeenNthCalledWith( + 1, + 'createCluster', + expect.objectContaining({ provider: 'docker' }), + ); + expect(telemetryLogErrorMock).not.toBeCalled(); }); test('check cluster configuration generation', async () => { diff --git a/extensions/kind/src/create-cluster.ts b/extensions/kind/src/create-cluster.ts index d83ac35aba7..a124b5f4960 100644 --- a/extensions/kind/src/create-cluster.ts +++ b/extensions/kind/src/create-cluster.ts @@ -39,6 +39,7 @@ export async function createCluster( logger: extensionApi.Logger, kindCli: string, token?: CancellationToken, + telemetryLogger: extensionApi.TelemetryLogger, ): Promise { let clusterName = 'kind'; if (params['kind.cluster.creation.name']) { @@ -88,6 +89,15 @@ export async function createCluster( await fs.promises.rm(tmpDirectory, { recursive: true }); if (result.exitCode !== 0) { + telemetryLogger.logError('createCluster', { + provider, + httpHostPort, + httpsHostPort, + error: result.error, + stdErr: result.stdErr, + }); throw new Error(`Failed to create kind cluster. ${result.error}`); + } else { + telemetryLogger.logUsage('createCluster', { provider, httpHostPort, httpsHostPort }); } } diff --git a/extensions/kind/src/extension.ts b/extensions/kind/src/extension.ts index fb222ddaf4a..58e694300da 100644 --- a/extensions/kind/src/extension.ts +++ b/extensions/kind/src/extension.ts @@ -47,11 +47,15 @@ let kindCli: string | undefined; const imageHandler = new ImageHandler(); -function registerProvider(extensionContext: extensionApi.ExtensionContext, provider: extensionApi.Provider): void { +function registerProvider( + extensionContext: extensionApi.ExtensionContext, + provider: extensionApi.Provider, + telemetryLogger: extensionApi.TelemetryLogger, +): void { const disposable = provider.setKubernetesProviderConnectionFactory({ // eslint-disable-next-line @typescript-eslint/no-explicit-any create: (params: { [key: string]: any }, logger?: Logger, token?: CancellationToken) => - createCluster(params, logger, kindCli, token), + createCluster(params, logger, kindCli, token, telemetryLogger), creationDisplayName: 'Kind cluster', }); extensionContext.subscriptions.push(disposable); @@ -123,11 +127,13 @@ async function searchKindClusters(provider: extensionApi.Provider) { const kindContainers = allContainers.filter(container => { return container.Labels?.['io.x-k8s.kind.cluster']; }); - updateClusters(provider, kindContainers); } -function createProvider(extensionContext: extensionApi.ExtensionContext) { +function createProvider( + extensionContext: extensionApi.ExtensionContext, + telemetryLogger: extensionApi.TelemetryLogger, +) { const provider = extensionApi.provider.createProvider({ name: 'Kind', id: 'kind', @@ -141,11 +147,12 @@ function createProvider(extensionContext: extensionApi.ExtensionContext) { }, }); extensionContext.subscriptions.push(provider); - registerProvider(extensionContext, provider); + registerProvider(extensionContext, provider, telemetryLogger); extensionContext.subscriptions.push( - extensionApi.commands.registerCommand(KIND_MOVE_IMAGE_COMMAND, image => - imageHandler.moveImage(image, kindClusters, kindCli), - ), + extensionApi.commands.registerCommand(KIND_MOVE_IMAGE_COMMAND, image => { + telemetryLogger.logUsage('moveImage'); + imageHandler.moveImage(image, kindClusters, kindCli); + }), ); // when containers are refreshed, update @@ -163,11 +170,12 @@ function createProvider(extensionContext: extensionApi.ExtensionContext) { extensionApi.provider.onDidUnregisterContainerConnection(() => { searchKindClusters(provider); }); - extensionApi.provider.onDidUpdateProvider(() => registerProvider(extensionContext, provider)); + extensionApi.provider.onDidUpdateProvider(() => registerProvider(extensionContext, provider, telemetryLogger)); } export async function activate(extensionContext: extensionApi.ExtensionContext): Promise { - const installer = new KindInstaller(extensionContext.storagePath); + const telemetryLogger = extensionApi.env.createTelemetryLogger(); + const installer = new KindInstaller(extensionContext.storagePath, telemetryLogger); kindCli = await detectKind(extensionContext.storagePath, installer); if (!kindCli) { @@ -184,7 +192,7 @@ export async function activate(extensionContext: extensionApi.ExtensionContext): if (status) { statusBarItem.dispose(); kindCli = await detectKind(extensionContext.storagePath, installer); - createProvider(extensionContext); + createProvider(extensionContext, telemetryLogger); } }, err => window.showErrorMessage('Kind installation failed ' + err), @@ -194,7 +202,7 @@ export async function activate(extensionContext: extensionApi.ExtensionContext): statusBarItem.show(); } } else { - createProvider(extensionContext); + createProvider(extensionContext, telemetryLogger); } } diff --git a/extensions/kind/src/kind-installer.spec.ts b/extensions/kind/src/kind-installer.spec.ts index 051e2ce2982..d9cb9a82b9b 100644 --- a/extensions/kind/src/kind-installer.spec.ts +++ b/extensions/kind/src/kind-installer.spec.ts @@ -44,8 +44,15 @@ vi.mock('@octokit/rest', () => { }; }); +const telemetryLogUsageMock = vi.fn(); +const telemetryLogErrorMock = vi.fn(); +const telemetryLoggerMock = { + logUsage: telemetryLogUsageMock, + logError: telemetryLogErrorMock, +} as unknown as extensionApi.TelemetryLogger; + beforeEach(() => { - installer = new KindInstaller('.'); + installer = new KindInstaller('.', telemetryLoggerMock); vi.clearAllMocks(); }); @@ -66,6 +73,11 @@ test('expect showNotification to be called', async () => { }; }); const result = await installer.performInstall(); + expect(telemetryLogErrorMock).not.toBeCalled(); + expect(telemetryLogUsageMock).toHaveBeenNthCalledWith(1, 'install-kind-prompt'); + expect(telemetryLogUsageMock).toHaveBeenNthCalledWith(2, 'install-kind-prompt-yes'); + expect(telemetryLogUsageMock).toHaveBeenNthCalledWith(3, 'install-kind-downloaded'); + expect(result).toBeDefined(); expect(result).toBeTruthy(); expect(spy).toBeCalled(); diff --git a/extensions/kind/src/kind-installer.ts b/extensions/kind/src/kind-installer.ts index b26e86ceac6..267e7c523ba 100644 --- a/extensions/kind/src/kind-installer.ts +++ b/extensions/kind/src/kind-installer.ts @@ -59,7 +59,7 @@ export class KindInstaller { private assetPromise: Promise; - constructor(private readonly storagePath: string) { + constructor(private readonly storagePath: string, private telemetryLogger: extensionApi.TelemetryLogger) { this.assetNames.set(WINDOWS_X64_PLATFORM, WINDOWS_X64_ASSET_NAME); this.assetNames.set(LINUX_X64_PLATFORM, LINUX_X64_ASSET_NAME); this.assetNames.set(LINUX_ARM64_PLATFORM, LINUX_ARM64_ASSET_NAME); @@ -102,12 +102,14 @@ export class KindInstaller { } async performInstall(): Promise { + this.telemetryLogger.logUsage('install-kind-prompt'); const dialogResult = await extensionApi.window.showInformationMessage( 'kind is not installed on this system, would you like to install Kind ?', 'Yes', 'No', ); if (dialogResult === 'Yes') { + this.telemetryLogger.logUsage('install-kind-prompt-yes'); return extensionApi.window.withProgress({ location: ProgressLocation.APP_ICON }, async progress => { progress.report({ increment: 5 }); try { @@ -133,6 +135,7 @@ export class KindInstaller { const stat = fs.statSync(destFile); fs.chmodSync(destFile, stat.mode | fs.constants.S_IXUSR); } + this.telemetryLogger.logUsage('install-kind-downloaded'); extensionApi.window.showNotification({ body: 'Kind is successfully installed.' }); return true; } @@ -141,6 +144,8 @@ export class KindInstaller { progress.report({ increment: -1 }); } }); + } else { + this.telemetryLogger.logUsage('install-kind-prompt-no'); } return false; }