diff --git a/apps/examples/desktop-app/sidecar/marketplace-telemetry.ts b/apps/examples/desktop-app/sidecar/marketplace-telemetry.ts index d08aa16eaf..0383ac365a 100644 --- a/apps/examples/desktop-app/sidecar/marketplace-telemetry.ts +++ b/apps/examples/desktop-app/sidecar/marketplace-telemetry.ts @@ -1,20 +1,11 @@ -import type { ITelemetryService, TelemetryProperties } from "@cline/shared"; +import { + captureMarketplaceInstall, + type MarketplaceInstallProperties, +} from "@cline/core"; +import type { ITelemetryService } from "@cline/shared"; import type { JsonRecord } from "./types"; -export type MarketplaceInstallOutcome = - | "installed" - | "already_installed" - | "failed"; - -export interface MarketplaceInstallEvent { - type: string; - outcome: MarketplaceInstallOutcome; - /** Which path produced the outcome: skills-cli, github-download, already-installed. */ - method?: string; - /** Coarse class of the skills CLI failure, when one happened. */ - cli_failure?: string; - fallback_failed?: boolean; -} +export type MarketplaceInstallEvent = MarketplaceInstallProperties; /** The marketplace entry type a desktop install request asked for. */ export function requestedMarketplaceType( @@ -66,11 +57,7 @@ export function captureMarketplaceInstallEvent( ): void { try { if (!telemetry?.isEnabled()) return; - const properties: TelemetryProperties = {}; - for (const [key, value] of Object.entries(event)) { - if (value !== undefined) properties[key] = value; - } - telemetry.capture({ event: "desktop.marketplace.install", properties }); + captureMarketplaceInstall(telemetry, event); } catch { // Product interactions must continue if the telemetry provider fails. } diff --git a/sdk/packages/core/src/index.ts b/sdk/packages/core/src/index.ts index 232792206b..d7097f45eb 100644 --- a/sdk/packages/core/src/index.ts +++ b/sdk/packages/core/src/index.ts @@ -791,6 +791,7 @@ export type { CaptureCompactionExecutedProperties, CaptureCompactionSkippedProperties, GitSnapshotProperties, + MarketplaceInstallProperties, TelemetryAgentIdentityProperties, TelemetryAgentKind, TelemetryCompactionMode, @@ -816,6 +817,7 @@ export { captureExtensionActivated, captureGitSnapshot, captureHookDiscovery, + captureMarketplaceInstall, captureMentionFailed, captureMentionSearchResults, captureMentionUsed, diff --git a/sdk/packages/core/src/services/telemetry/core-events.test.ts b/sdk/packages/core/src/services/telemetry/core-events.test.ts index 2ae225fd40..cc24b1f4ec 100644 --- a/sdk/packages/core/src/services/telemetry/core-events.test.ts +++ b/sdk/packages/core/src/services/telemetry/core-events.test.ts @@ -17,6 +17,7 @@ import { captureCompactionSkipped, captureExtensionActivated, captureGitSnapshot, + captureMarketplaceInstall, captureMistakeLimitReached, captureProviderConfigured, captureRunCommandsTimeout, @@ -1072,3 +1073,39 @@ test("scheduler telemetry allowlists diagnostics and tolerates capture failures" expect(() => captureScheduleRun(telemetry, input)).not.toThrow(); expect(() => captureScheduleRun(undefined, input)).not.toThrow(); }); + +describe("captureMarketplaceInstall", () => { + test("emits desktop.marketplace.install with only the defined fields", () => { + const capture = vi.fn(); + const telemetry = { capture } as unknown as Parameters< + typeof captureMarketplaceInstall + >[0]; + + captureMarketplaceInstall(telemetry, { + type: "skill", + outcome: "failed", + method: "github-download", + cli_failure: "git-missing", + fallback_failed: true, + }); + captureMarketplaceInstall(telemetry, { type: "mcp", outcome: "installed" }); + + expect(CORE_TELEMETRY_EVENTS.DESKTOP.MARKETPLACE_INSTALL).toBe( + "desktop.marketplace.install", + ); + expect(capture).toHaveBeenNthCalledWith(1, { + event: "desktop.marketplace.install", + properties: { + type: "skill", + outcome: "failed", + method: "github-download", + cli_failure: "git-missing", + fallback_failed: true, + }, + }); + expect(capture).toHaveBeenNthCalledWith(2, { + event: "desktop.marketplace.install", + properties: { type: "mcp", outcome: "installed" }, + }); + }); +}); diff --git a/sdk/packages/core/src/services/telemetry/core-events.ts b/sdk/packages/core/src/services/telemetry/core-events.ts index 38c8bca355..b66c9e0f80 100644 --- a/sdk/packages/core/src/services/telemetry/core-events.ts +++ b/sdk/packages/core/src/services/telemetry/core-events.ts @@ -105,6 +105,9 @@ export const CORE_TELEMETRY_EVENTS = { INIT_ERROR: "workspace.init_error", PATH_RESOLVED: "workspace.path_resolved", }, + DESKTOP: { + MARKETPLACE_INSTALL: "desktop.marketplace.install", + }, SDK: { ERROR: SDK_ERROR_TELEMETRY_EVENT, TOOL_TIMEOUT: "sdk.tool_timeout", @@ -297,6 +300,39 @@ export function captureWorkspacePathResolved( }); } +export interface MarketplaceInstallProperties { + /** Marketplace entry type: skill, plugin, mcp. */ + type: string; + outcome: "installed" | "already_installed" | "failed"; + /** Path that produced the outcome: skills-cli, github-download, already-installed. */ + method?: string; + /** Coarse class of a skills CLI failure: npx-missing, node-too-old, git-missing, other. */ + cli_failure?: string; + fallback_failed?: boolean; +} + +/** + * One event per Desktop marketplace install attempt. Only coarse classes are + * sent, never CLI output, so the payload cannot carry paths or tokens. + */ +export function captureMarketplaceInstall( + telemetry: ITelemetryService | undefined, + properties: MarketplaceInstallProperties, +): void { + const payload: TelemetryProperties = { + type: properties.type, + outcome: properties.outcome, + }; + if (properties.method !== undefined) payload.method = properties.method; + if (properties.cli_failure !== undefined) { + payload.cli_failure = properties.cli_failure; + } + if (properties.fallback_failed !== undefined) { + payload.fallback_failed = properties.fallback_failed; + } + emit(telemetry, CORE_TELEMETRY_EVENTS.DESKTOP.MARKETPLACE_INSTALL, payload); +} + export function captureAuthStarted( telemetry: ITelemetryService | undefined, provider?: string,