feat(telemetry): count plugin installs from Orca's public catalogs (#36132)

* feat(telemetry): report plugin installs and removals from Orca's public catalogs

* refactor(plugins): move marketplace install origin out of the installer
This commit is contained in:
Neil
2026-10-29 16:32:49 -07:00
committed by GitHub
parent d063703de2
commit 0426223be5
18 changed files with 352 additions and 37 deletions
+2 -1
View File
@@ -28,8 +28,9 @@ The categories of behavior we observe:
- **Tab actions** — for a consistent 10% sample of anonymous installs, the first time per app session you choose “Close Tabs To The Left” or “Close Tabs To The Right,” limited to two events per session across all windows. For that first request in each direction, we record the selected tab's type, whether it is in a workspace or floating panel, how many unpinned tabs are eligible to close, and the sampling rate. The app-open event includes sample membership so we can measure the share of sampled sessions that use each command. Repeat requests are not recorded, so these events do not measure click frequency, overall usage by tab type or panel, or total tabs closed. This counts the request, including when you cancel a save prompt; never tab titles, file paths, or workspace identifiers.
- **Privacy controls** — when you opt in or out of telemetry, so we can tell from aggregate data whether our consent UI is working.
- **SSH remote runtime** — once per SSH host and outcome per app session, which runtime Orca used to run on that host and why, or that its runtime check could not be confirmed or failed: coarse host facts (operating system, CPU architecture, C library family and glibc minor version, and the host's Node.js major version when Orca runs on it), the category of any refusal (including security software blocking the runtime on Windows), whether a runtime was uploaded, and a coarse duration bucket. Never the hostname, user name, paths, or raw error text.
- **Plugins** — when you install, update or remove a plugin from Orca's official or community catalog. We record the plugin's public name (`publisher.id`), its version, which of those two catalogs it came from, whether it was a first install, an update or a reinstall, and whether you started it from the catalog, from an install link, or through automatic updates. These events power the public install counts on the plugin directory, which therefore count only users who share usage data. Plugins from a catalog you added yourself, from a Git URL, or from a local or development folder are never reported.
Fields include fixed enum values, version strings, numeric counts and revisions, and random local IDs. Conversation IDs allow repeated token summaries for the same conversation to be associated; they are pseudonymous. No free-form strings from any UI input ever leave your machine.
Fields include fixed enum values, version strings, numeric counts and revisions, and random local IDs. Conversation IDs allow repeated token summaries for the same conversation to be associated; they are pseudonymous. Plugin names are the public identifiers their catalog lists. No free-form strings from any UI input ever leave your machine.
## What we never send
+4 -1
View File
@@ -168,8 +168,11 @@ describe('plugin marketplace IPC authority', () => {
}
await invoke('plugins:installMarketplacePlugin', preview)
expect(services.installer.install).toHaveBeenCalledWith(preview)
expect(services.installer.install).toHaveBeenCalledWith(preview, { trigger: 'catalog' })
expect(pluginService.refresh).toHaveBeenCalledOnce()
await invoke('plugins:installMarketplacePlugin', { ...preview, fromInstallLink: true })
expect(services.installer.install).toHaveBeenLastCalledWith(preview, { trigger: 'deep_link' })
vi.mocked(pluginService.refresh).mockClear()
vi.mocked(pluginService.refresh).mockClear()
vi.mocked(services.installer.install).mockResolvedValueOnce({ ok: false, error: 'failed' })
+8 -3
View File
@@ -43,7 +43,9 @@ const installMarketplacePluginSchema = marketplacePluginSchema.extend({
/** Ask main to approve in the same step if the installed plugin is content only. */
enableIfData: z.literal(true).optional(),
/** The preview's consentFingerprint: one-click approves only these exact bytes. */
expectedFingerprint: z.string().min(1).max(256).optional()
expectedFingerprint: z.string().min(1).max(256).optional(),
/** The page was opened from an `orca://plugins/...` link; only labels install telemetry. */
fromInstallLink: z.literal(true).optional()
})
const installedPluginSchema = z.strictObject({
pluginKey: z.string().refine(isQualifiedPluginKey, 'invalid qualified plugin key')
@@ -106,12 +108,15 @@ export function registerPluginMarketplaceHandlers(
return services.installer.preview(parsed.marketplaceSourceId, parsed.pluginKey)
})
ipcMain.handle('plugins:installMarketplacePlugin', async (event, args: unknown) => {
const { enableIfData, expectedFingerprint, ...identity } =
const { enableIfData, expectedFingerprint, fromInstallLink, ...identity } =
installMarketplacePluginSchema.parse(args)
return installCatalogReleaseWithApproval({
pluginKey: identity.pluginKey,
pluginService,
install: () => services.installer.install(identity),
install: () =>
services.installer.install(identity, {
trigger: fromInstallLink ? 'deep_link' : 'catalog'
}),
approve: (pluginKey, reviewedFingerprint) =>
approveConsent(pluginKey, reviewedFingerprint, event.sender.id),
...(enableIfData ? { enableIfData: { expectedFingerprint } } : {})
+2
View File
@@ -23,6 +23,7 @@ import {
isAllowedPluginGitUrl,
type PluginLockfile
} from '../../shared/plugins/installation/plugin-install-lockfile'
import { trackPluginUninstalled } from '../telemetry/plugin-install-telemetry'
import {
registerPluginMarketplaceHandlers,
type PluginMarketplaceHandlerServices
@@ -258,6 +259,7 @@ export function registerPluginHandlers(
pluginKey: parsed.pluginKey
})
)
trackPluginUninstalled(parsed.pluginKey, lock.plugins[parsed.pluginKey]?.source)
const settings = store.getSettings()
const consents = { ...settings.pluginConsents }
delete consents[parsed.pluginKey]
+2
View File
@@ -29,6 +29,8 @@ const MAIN_OWNED_TELEMETRY_EVENTS = new Set<EventName>([
'daemon_pty_cwd_readable',
'star_nag_outcome',
'feature_interaction_usage_bucket_reached',
'plugin_installed',
'plugin_uninstalled',
'ssh_remote_runtime_resolved',
'ssh_host_server_decided',
'ssh_host_server_conversion',
@@ -183,7 +183,10 @@ describe('plugin auto-update', () => {
advance(HOUR)
await updater.run()
expect(install).toHaveBeenCalledTimes(1)
expect(install).toHaveBeenCalledWith(expect.anything(), { requireInstalled: true })
expect(install).toHaveBeenCalledWith(expect.anything(), {
requireInstalled: true,
trigger: 'auto_update'
})
expect(consents[KEY]).toBe(next.consentFingerprint)
expect(updater.getState()).toEqual({ updated: { [KEY]: '1.1.0' }, needsReview: {} })
expect(changes.at(-1)?.updated).toEqual({ [KEY]: '1.1.0' })
@@ -200,7 +200,7 @@ export class PluginAutoUpdater {
// Why at the last await: the user may turn auto-update off while this pass waits.
install: async () =>
this.options.isEnabled()
? installer.install(preview, { requireInstalled: true })
? installer.install(preview, { requireInstalled: true, trigger: 'auto_update' })
: { ok: false, error: 'automatic updates were turned off' },
approve: this.options.approve
})
@@ -0,0 +1,20 @@
import type {
PluginInstallSource,
PluginLockEntry
} from '../../../shared/plugins/installation/plugin-install-lockfile'
import { marketplaceSourceId } from './plugin-marketplace-store'
type MarketplaceInstallSource = Extract<PluginInstallSource, { kind: 'marketplace' }>
export type MarketplaceInstall = { sourceId: string; plugin: MarketplaceInstallSource['plugin'] }
/** The catalog a lock entry was installed from, or null when it was not a marketplace install. */
export function marketplaceInstallOf(entry: PluginLockEntry | null): MarketplaceInstall | null {
if (entry?.source.kind !== 'marketplace') {
return null
}
const { marketplace, plugin } = entry.source
return {
sourceId: marketplaceSourceId({ kind: 'git', url: marketplace.url, ref: marketplace.ref }),
plugin
}
}
@@ -50,6 +50,9 @@ vi.mock('../installation/plugin-git-repository', () => ({
checkoutPluginGitSource: git.checkout
}))
const telemetry = vi.hoisted(() => ({ track: vi.fn() }))
vi.mock('../../telemetry/client', () => telemetry)
const roots: string[] = []
async function tempRoot(): Promise<string> {
@@ -126,7 +129,10 @@ function squarePng(side: number): Buffer {
return Buffer.from([...signature, ...ihdr, 0, 0, 0, 0])
}
async function setup(imageCache?: PluginImageCache): Promise<{
async function setup(
imageCache?: PluginImageCache,
catalogUrl = 'https://github.com/community/plugins.git'
): Promise<{
root: string
marketplace: PluginMarketplaceService
installer: PluginMarketplaceInstaller
@@ -137,11 +143,7 @@ async function setup(imageCache?: PluginImageCache): Promise<{
pluginsDataDir: join(root, 'plugins-data'),
fetcher: async () => ({ marketplaceCommit: git.marketplaceCommit, marketplace: marketplace() })
})
const added = await service.addSource({
kind: 'git',
url: 'https://github.com/community/plugins.git',
ref: 'main'
})
const added = await service.addSource({ kind: 'git', url: catalogUrl, ref: 'main' })
return {
root,
marketplace: service,
@@ -168,6 +170,7 @@ beforeEach(() => {
git.worker = false
git.icon = false
git.paths = null
telemetry.track.mockReset()
git.checkout.mockReset()
git.checkout.mockImplementation(
async ({ destination, path }: { destination: string; path?: string }) => {
@@ -467,4 +470,32 @@ describe('PluginMarketplaceInstaller', () => {
repositoryMoved: true
})
})
it('reports public-catalog installs as install, reinstall, then update', async () => {
const communityCatalog = 'https://github.com/stablyai/orca-community-plugins.git'
const { installer, sourceId } = await setup(undefined, communityCatalog)
const preview = await installer.preview(sourceId, 'community.notes')
await installer.install(preview, { trigger: 'deep_link' })
await installer.install(preview)
git.commit = 'b'.repeat(40)
git.version = '2.0.0'
await installer.install(await installer.previewInstalledUpdate('community.notes'), {
trigger: 'auto_update'
})
const base = { plugin_key: 'community.notes', catalog: 'community' }
expect(telemetry.track.mock.calls).toEqual([
['plugin_installed', { ...base, version: '1.0.0', kind: 'install', trigger: 'deep_link' }],
['plugin_installed', { ...base, version: '1.0.0', kind: 'reinstall', trigger: 'catalog' }],
['plugin_installed', { ...base, version: '2.0.0', kind: 'update', trigger: 'auto_update' }]
])
})
it('never reports installs from a private catalog', async () => {
const { installer, sourceId } = await setup()
const result = await installer.install(await installer.preview(sourceId, 'community.notes'))
expect(result.ok).toBe(true)
expect(telemetry.track).not.toHaveBeenCalled()
})
})
@@ -29,12 +29,17 @@ import type {
PluginMarketplaceListing,
PluginMarketplaceService
} from './plugin-marketplace-service'
import { marketplaceSourceId } from './plugin-marketplace-store'
import { marketplaceInstallOf, type MarketplaceInstall } from './plugin-marketplace-install-origin'
import { isSamePluginSource } from '../../../shared/plugins/catalog/plugin-marketplace'
import type {
PluginInstallSource,
PluginLockEntry,
PluginLockfile
} from '../../../shared/plugins/installation/plugin-install-lockfile'
import {
pluginInstallKind,
trackPluginInstalled,
type PluginInstallTrigger
} from '../../telemetry/plugin-install-telemetry'
export type PluginMarketplaceInstallPreview = {
marketplaceSourceId: string
@@ -61,8 +66,6 @@ export type PluginMarketplaceInstallPreview = {
/** `repositoryMoved`: this replaced an install that came from a different repository. */
export type PluginMarketplaceInstallResult = PluginInstallResult & { repositoryMoved?: true }
type MarketplaceInstallSource = Extract<PluginInstallSource, { kind: 'marketplace' }>
export type PluginMarketplacePreviewIdentity = Pick<
PluginMarketplaceInstallPreview,
'marketplaceSourceId' | 'marketplaceCommit' | 'pluginKey' | 'resolvedCommit'
@@ -146,7 +149,7 @@ export class PluginMarketplaceInstaller {
async install(
preview: PluginMarketplacePreviewIdentity,
options: { requireInstalled?: boolean } = {}
options: { requireInstalled?: boolean; trigger?: PluginInstallTrigger } = {}
): Promise<PluginMarketplaceInstallResult> {
const listing = await this.requireListing(preview.marketplaceSourceId, preview.pluginKey)
const blockedReason =
@@ -168,7 +171,8 @@ export class PluginMarketplaceInstaller {
return { ok: false, error: 'marketplace source is no longer configured' }
}
// Why read first: the install rewrites the lock entry this comparison needs.
const installed = await this.readMarketplaceInstall(preview.pluginKey)
const priorEntry = await this.readLockEntry(preview.pluginKey)
const installed = marketplaceInstallOf(priorEntry)
const repositoryMoved =
installed !== null && !isSamePluginSource(installed.plugin, listing.source)
const result = await installPluginFromMarketplace({
@@ -190,6 +194,15 @@ export class PluginMarketplaceInstaller {
blockedPluginReason: this.blockedPluginReason,
...(options.requireInstalled ? { requireInstalled: true } : {})
})
if (result.ok) {
trackPluginInstalled({
marketplaceUrl: sourceState.source.url,
plugin_key: result.pluginKey,
version: result.version,
kind: pluginInstallKind(priorEntry?.version ?? null, result.version),
trigger: options.trigger ?? 'catalog'
})
}
return result.ok && repositoryMoved ? { ...result, repositoryMoved: true } : result
}
@@ -198,20 +211,18 @@ export class PluginMarketplaceInstaller {
return readPluginLockfile(getUserPluginsDir(this.userDataPath))
}
private async readLockEntry(pluginKey: string): Promise<PluginLockEntry | null> {
return (await this.readInstallLockfile()).plugins[pluginKey] ?? null
}
/** The catalog an installed plugin came from, or null when it was not a marketplace install. */
private async readMarketplaceInstall(
pluginKey: string,
lock?: PluginLockfile
): Promise<{ sourceId: string; plugin: MarketplaceInstallSource['plugin'] } | null> {
const entry = (lock ?? (await this.readInstallLockfile())).plugins[pluginKey]
if (entry?.source.kind !== 'marketplace') {
return null
}
const { marketplace, plugin } = entry.source
return {
sourceId: marketplaceSourceId({ kind: 'git', url: marketplace.url, ref: marketplace.ref }),
plugin
}
): Promise<MarketplaceInstall | null> {
return marketplaceInstallOf(
lock ? (lock.plugins[pluginKey] ?? null) : await this.readLockEntry(pluginKey)
)
}
async previewInstalledUpdate(pluginKey: string): Promise<PluginMarketplaceInstallPreview> {
@@ -0,0 +1,104 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { eventSchemas } from '../../shared/telemetry-events'
import {
pluginInstallKind,
pluginTelemetryCatalog,
trackPluginInstalled,
trackPluginUninstalled
} from './plugin-install-telemetry'
const telemetry = vi.hoisted(() => ({ track: vi.fn() }))
vi.mock('./client', () => telemetry)
const marketplaceSource = (url: string) => ({
kind: 'marketplace' as const,
marketplace: { url, ref: 'main', resolvedCommit: 'f'.repeat(40) },
plugin: { url: 'https://github.com/acme/notes.git', ref: 'main' }
})
beforeEach(() => {
telemetry.track.mockReset()
})
describe('plugin install telemetry', () => {
it('names only the official and community catalogs', () => {
expect(pluginTelemetryCatalog('https://github.com/stablyai/orca-plugins.git')).toBe('official')
expect(pluginTelemetryCatalog('git@github.com:stablyai/orca-plugins.git')).toBe('official')
expect(pluginTelemetryCatalog('https://github.com/stablyai/orca-community-plugins')).toBe(
'community'
)
expect(pluginTelemetryCatalog('https://github.com/acme/private-plugins.git')).toBeNull()
expect(
pluginTelemetryCatalog('https://gitlab.example.com/stablyai/orca-plugins.git')
).toBeNull()
})
it('tells a first install from a reinstall and an update', () => {
expect(pluginInstallKind(null, '1.0.0')).toBe('install')
expect(pluginInstallKind('1.0.0', '1.0.0')).toBe('reinstall')
expect(pluginInstallKind('1.0.0', '1.1.0')).toBe('update')
})
it('sends nothing for a private catalog install', () => {
trackPluginInstalled({
marketplaceUrl: 'https://github.com/acme/private-plugins.git',
plugin_key: 'acme.notes',
version: '1.0.0',
kind: 'install',
trigger: 'catalog'
})
expect(telemetry.track).not.toHaveBeenCalled()
})
it('reports an uninstall only for a public-catalog plugin', () => {
trackPluginUninstalled('acme.notes', { kind: 'local-path', path: '/Users/me/notes' })
trackPluginUninstalled('acme.notes', marketplaceSource('https://github.com/acme/private.git'))
trackPluginUninstalled('acme.notes', undefined)
expect(telemetry.track).not.toHaveBeenCalled()
trackPluginUninstalled(
'acme.notes',
marketplaceSource('https://github.com/stablyai/orca-community-plugins.git')
)
expect(telemetry.track).toHaveBeenCalledWith('plugin_uninstalled', {
plugin_key: 'acme.notes',
catalog: 'community'
})
})
it('never lets a telemetry failure escape', () => {
telemetry.track.mockImplementation(() => {
throw new Error('boom')
})
expect(() =>
trackPluginUninstalled(
'acme.notes',
marketplaceSource('https://github.com/stablyai/orca-plugins.git')
)
).not.toThrow()
})
it('schemas reject anything but a plugin key and enums', () => {
const valid = {
plugin_key: 'acme.notes',
version: '1.2.0-beta.1',
catalog: 'official',
kind: 'install',
trigger: 'catalog'
}
expect(eventSchemas.plugin_installed.safeParse(valid).success).toBe(true)
for (const bad of [
{ ...valid, plugin_key: '/Users/me/dev-plugin' },
{ ...valid, plugin_key: 'notes' },
{ ...valid, catalog: 'dev' },
{ ...valid, version: '1.0.0 /tmp' },
{ ...valid, path: '/Users/me' }
]) {
expect(eventSchemas.plugin_installed.safeParse(bad).success).toBe(false)
}
expect(
eventSchemas.plugin_uninstalled.safeParse({ plugin_key: 'acme.notes', catalog: 'community' })
.success
).toBe(true)
})
})
@@ -0,0 +1,67 @@
/**
* Install and uninstall rows for plugins from Orca's public catalogs, which feed the directory's
* install counts. Anything else (private catalogs, git, local and dev installs) never reports.
*/
import { COMMUNITY_MARKETPLACE_GIT_SOURCE } from '../../shared/plugins/catalog/plugin-install-link'
import {
isOfficialMarketplaceGitSource,
isSameGitRepository
} from '../../shared/plugins/catalog/plugin-marketplace'
import type { PluginInstallSource } from '../../shared/plugins/installation/plugin-install-lockfile'
import type { EventProps } from '../../shared/telemetry-events'
import { track } from './client'
export type PluginInstallTelemetryProps = EventProps<'plugin_installed'>
export type PluginInstallTrigger = PluginInstallTelemetryProps['trigger']
type Catalog = PluginInstallTelemetryProps['catalog']
/** The public catalog a marketplace URL names, or null for any other catalog. */
export function pluginTelemetryCatalog(marketplaceUrl: string): Catalog | null {
if (isOfficialMarketplaceGitSource(marketplaceUrl)) {
return 'official'
}
return isSameGitRepository(marketplaceUrl, COMMUNITY_MARKETPLACE_GIT_SOURCE.url)
? 'community'
: null
}
export function pluginInstallKind(
priorVersion: string | null,
version: string
): PluginInstallTelemetryProps['kind'] {
if (priorVersion === null) {
return 'install'
}
return priorVersion === version ? 'reinstall' : 'update'
}
export function trackPluginInstalled(
input: Omit<PluginInstallTelemetryProps, 'catalog'> & { marketplaceUrl: string }
): void {
const { marketplaceUrl, ...rest } = input
const catalog = pluginTelemetryCatalog(marketplaceUrl)
if (catalog) {
safeTrack(() => track('plugin_installed', { ...rest, catalog }))
}
}
/** Reports only when the removed plugin came from a public catalog. */
export function trackPluginUninstalled(
pluginKey: string,
source: PluginInstallSource | undefined
): void {
const catalog =
source?.kind === 'marketplace' ? pluginTelemetryCatalog(source.marketplace.url) : null
if (catalog) {
safeTrack(() => track('plugin_uninstalled', { plugin_key: pluginKey, catalog }))
}
}
function safeTrack(emit: () => void): void {
try {
// track() applies the consent gate and validates against the strict schema.
emit()
} catch {
// Telemetry never costs an install.
}
}
+2
View File
@@ -189,6 +189,8 @@ export type PluginsApi = {
enableIfData?: true
/** The preview's consentFingerprint; with it, main approves only those exact bytes. */
expectedFingerprint?: string
/** The page was opened from an install link; only labels telemetry. */
fromInstallLink?: true
}
) => Promise<PluginHostInstallResult>
previewMarketplaceUpdate: (args: {
@@ -1,4 +1,4 @@
import { useMemo, useState } from 'react'
import { useCallback, useMemo, useState } from 'react'
import { Blocks } from 'lucide-react'
import type { PluginHostListEntry } from '../../../../../preload/api-types'
import { translate } from '@/i18n/i18n'
@@ -104,11 +104,16 @@ export function PluginManager({
onRefreshInstalled,
onOpenRequested: open
})
// Why: an install counts as from a link only while the page that link opened is showing.
const [linkedKey, setLinkedKey] = useState<string | null>(null)
const installLink = usePluginInstallLink({
sources: catalog.sources,
loading: catalog.loading,
loadMarketplaceData: catalog.loadMarketplaceData,
open
open: (pluginKey) => {
setLinkedKey(pluginKey)
open(pluginKey)
}
})
const openEntry = (pluginKey: string): void => {
open(pluginKey)
@@ -116,9 +121,14 @@ export function PluginManager({
onShowLogs(pluginKey)
}
}
const isFromInstallLink = useCallback(
(pluginKey: string) => pluginKey === linkedKey && openKey === linkedKey,
[linkedKey, openKey]
)
const install = usePluginInstallAction({
fetchPreview: catalog.fetchPreview,
onInstalled
onInstalled,
isFromInstallLink
})
const prefetch = usePluginHoverPrefetch(catalog.fetchPreview)
const orcaVersion = useOrcaVersion()
@@ -60,12 +60,12 @@ const codePreview: PluginMarketplaceHostInstallPreview = {
})
}
function setup() {
function setup(isFromInstallLink?: (pluginKey: string) => boolean) {
const onInstalled = vi.fn().mockResolvedValue(undefined)
const fetchPreview = vi.fn().mockResolvedValue({ ok: true, preview })
const hook: { current: ReturnType<typeof usePluginInstallAction> | null } = { current: null }
function Probe(): null {
hook.current = usePluginInstallAction({ fetchPreview, onInstalled })
hook.current = usePluginInstallAction({ fetchPreview, onInstalled, isFromInstallLink })
return null
}
act(() => createRoot(document.createElement('div')).render(<Probe />))
@@ -99,6 +99,19 @@ beforeEach(() => {
})
describe('usePluginInstallAction', () => {
it('labels an install from a page an install link opened, and only that one', async () => {
const { hook } = setup((pluginKey) => pluginKey === listing.pluginKey)
await act(async () => hook.current?.install(listing))
expect(window.api.plugins.installMarketplacePlugin).toHaveBeenCalledWith(
expect.objectContaining({ fromInstallLink: true })
)
const plain = setup()
await act(async () => plain.hook.current?.install(listing))
expect(window.api.plugins.installMarketplacePlugin).toHaveBeenLastCalledWith(
expect.not.objectContaining({ fromInstallLink: true })
)
})
it('asks main to turn on a content-only release and leaves nothing pending after', async () => {
const { hook, onInstalled } = setup()
await act(async () => hook.current?.install(listing))
@@ -52,11 +52,14 @@ const downloadFailed = (): string =>
*/
export function usePluginInstallAction({
fetchPreview,
onInstalled
onInstalled,
isFromInstallLink
}: {
fetchPreview: (listing: PluginMarketplaceHostListing) => Promise<PluginPagePreview>
/** Reloads the list; opens review on the page for any plugin still waiting for it. */
onInstalled: (pluginKey: string) => Promise<void>
/** Whether this install is on the page an install link opened; only labels telemetry. */
isFromInstallLink?: (pluginKey: string) => boolean
}): {
phaseOf: (pluginKey: string) => PluginInstallPhase | null
install: (
@@ -124,6 +127,7 @@ export function usePluginInstallAction({
marketplaceCommit: preview.marketplaceCommit,
pluginKey,
resolvedCommit: preview.resolvedCommit,
...(isFromInstallLink?.(pluginKey) ? { fromInstallLink: true as const } : {}),
// Why the fingerprint: main turns on only the exact bytes this preview showed.
...(execution === 'data'
? { enableIfData: true as const, expectedFingerprint: preview.consentFingerprint }
@@ -183,7 +187,7 @@ export function usePluginInstallAction({
setPhase(pluginKey, { kind: 'error', message: catalogInstallErrorMessage(cause) })
}
},
[dismiss, fetchPreview, onInstalled, setPhase]
[dismiss, fetchPreview, isFromInstallLink, onInstalled, setPhase]
)
const phaseOf = useCallback(
+5 -1
View File
@@ -103,6 +103,7 @@ import {
setupScriptPromptShownSchema,
workspaceCreateFailedSchema
} from './telemetry-repository-event-schemas'
import { pluginInstalledSchema, pluginUninstalledSchema } from './telemetry-plugin-event-schemas'
import { sshRemoteRuntimeResolvedSchema } from './telemetry-ssh-runtime-event-schemas'
import { wslUsageSnapshotSchema } from './telemetry-wsl-usage-event-schemas'
import {
@@ -234,5 +235,8 @@ export const eventSchemas = {
wsl_move_orchestrator: wslMoveOrchestratorSchema,
wsl_move_undo: wslMoveUndoSchema,
wsl_usage_snapshot: wslUsageSnapshotSchema
wsl_usage_snapshot: wslUsageSnapshotSchema,
plugin_installed: pluginInstalledSchema,
plugin_uninstalled: pluginUninstalledSchema
} as const
@@ -0,0 +1,33 @@
import { z } from 'zod'
import { isQualifiedPluginKey } from './plugins/frames/plugin-tab-key'
// Why only these two: both catalogs are public, so a key from them names nothing private.
// Private catalogs, git URLs, local folders and dev plugins never report.
export const PLUGIN_TELEMETRY_CATALOGS = ['official', 'community'] as const
/** `reinstall` is the same version again; totals count `install` only. */
export const PLUGIN_INSTALL_KINDS = ['install', 'update', 'reinstall'] as const
export const PLUGIN_INSTALL_TRIGGERS = ['catalog', 'deep_link', 'auto_update'] as const
const pluginKeySchema = z.string().max(129).refine(isQualifiedPluginKey)
export const pluginInstalledSchema = z
.object({
plugin_key: pluginKeySchema,
version: z
.string()
.max(64)
.regex(/^[0-9A-Za-z.+-]+$/),
catalog: z.enum(PLUGIN_TELEMETRY_CATALOGS),
kind: z.enum(PLUGIN_INSTALL_KINDS),
trigger: z.enum(PLUGIN_INSTALL_TRIGGERS)
})
.strict()
export const pluginUninstalledSchema = z
.object({
plugin_key: pluginKeySchema,
catalog: z.enum(PLUGIN_TELEMETRY_CATALOGS)
})
.strict()