Download src/utils/telemetry/pluginTelemetry.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 10.5 kB
-
https://huggingface.co/chenbhao/codev/resolve/main/src/utils/telemetry/pluginTelemetry.ts
- Command line
-
hf download hf://chenbhao/codev/src/utils/telemetry/pluginTelemetry.ts
-
curl -L -o pluginTelemetry.ts https://huggingface.co/chenbhao/codev/resolve/main/src/utils/telemetry/pluginTelemetry.ts
10.5 kB
| /** | |
| * Plugin telemetry helpers — shared field builders for plugin lifecycle events. | |
| * | |
| * Implements the twin-column privacy pattern: every user-defined-name field | |
| * emits both a raw value (routed to PII-tagged _PROTO_* BQ columns) and a | |
| * redacted twin (real name iff marketplace ∈ allowlist, else 'third-party'). | |
| * | |
| * plugin_id_hash provides an opaque per-plugin aggregation key with no privacy | |
| * dependency — sha256(name@marketplace + FIXED_SALT) truncated to 16 chars. | |
| * This answers distinct-count and per-plugin-trend questions that the | |
| * redacted column can't, without exposing user-defined names. | |
| */ | |
| import { createHash } from 'crypto' | |
| import { sep } from 'path' | |
| import { | |
| type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, | |
| logEvent, | |
| } from '../../services/analytics/index.js' | |
| import type { | |
| LoadedPlugin, | |
| PluginError, | |
| PluginManifest, | |
| } from '../../types/plugin.js' | |
| import { | |
| isOfficialMarketplaceName, | |
| parsePluginIdentifier, | |
| } from '../plugins/pluginIdentifier.js' | |
| // builtinPlugins.ts:BUILTIN_MARKETPLACE_NAME — inlined to avoid the cycle | |
| // through commands.js. Marketplace schemas.ts enforces 'builtin' is reserved. | |
| const BUILTIN_MARKETPLACE_NAME = 'builtin' | |
| // Fixed salt for plugin_id_hash. Same constant across all repos and emission | |
| // sites. Not per-org, not rotated — per-org salt would defeat cross-org | |
| // distinct-count, rotation would break trend lines. Customers can compute the | |
| // same hash on their known plugin names to reverse-match their own telemetry. | |
| const PLUGIN_ID_HASH_SALT = 'claude-plugin-telemetry-v1' | |
| /** | |
| * Opaque per-plugin aggregation key. Input is the name@marketplace string as | |
| * it appears in enabledPlugins keys, lowercased on the marketplace suffix for | |
| * reproducibility. 16-char truncation keeps BQ GROUP BY cardinality manageable | |
| * while making collisions negligible at projected 10k-plugin scale. Name case | |
| * is preserved in both branches (enabledPlugins keys are case-sensitive). | |
| */ | |
| export function hashPluginId(name: string, marketplace?: string): string { | |
| const key = marketplace ? `${name}@${marketplace.toLowerCase()}` : name | |
| return createHash('sha256') | |
| .update(key + PLUGIN_ID_HASH_SALT) | |
| .digest('hex') | |
| .slice(0, 16) | |
| } | |
| /** | |
| * 4-value scope enum for plugin origin. Distinct from PluginScope | |
| * (managed/user/project/local) which is installation-target — this is | |
| * marketplace-origin. | |
| * | |
| * - official: from an allowlisted Anthropic marketplace | |
| * - default-bundle: ships with product (@builtin), auto-enabled | |
| * - org: enterprise admin-pushed via managed settings (policySettings) | |
| * - user-local: user added marketplace or local plugin | |
| */ | |
| export type TelemetryPluginScope = | |
| | 'official' | |
| | 'org' | |
| | 'user-local' | |
| | 'default-bundle' | |
| export function getTelemetryPluginScope( | |
| name: string, | |
| marketplace: string | undefined, | |
| managedNames: Set<string> | null, | |
| ): TelemetryPluginScope { | |
| if (marketplace === BUILTIN_MARKETPLACE_NAME) return 'default-bundle' | |
| if (isOfficialMarketplaceName(marketplace)) return 'official' | |
| if (managedNames?.has(name)) return 'org' | |
| return 'user-local' | |
| } | |
| /** | |
| * How a plugin arrived in the session. Splits self-selected from org-pushed | |
| * — plugin_scope alone doesn't (an official plugin can be user-installed OR | |
| * org-pushed; both are scope='official'). | |
| */ | |
| export type EnabledVia = | |
| | 'user-install' | |
| | 'org-policy' | |
| | 'default-enable' | |
| | 'seed-mount' | |
| /** How a skill/command invocation was triggered. */ | |
| export type InvocationTrigger = | |
| | 'user-slash' | |
| | 'claude-proactive' | |
| | 'nested-skill' | |
| /** Where a skill invocation executes. */ | |
| export type SkillExecutionContext = 'fork' | 'inline' | 'remote' | |
| /** How a plugin install was initiated. */ | |
| export type InstallSource = | |
| | 'cli-explicit' | |
| | 'ui-discover' | |
| | 'ui-suggestion' | |
| | 'deep-link' | |
| export function getEnabledVia( | |
| plugin: LoadedPlugin, | |
| managedNames: Set<string> | null, | |
| seedDirs: string[], | |
| ): EnabledVia { | |
| if (plugin.isBuiltin) return 'default-enable' | |
| if (managedNames?.has(plugin.name)) return 'org-policy' | |
| // Trailing sep: /opt/plugins must not match /opt/plugins-extra | |
| if ( | |
| seedDirs.some(dir => | |
| plugin.path.startsWith(dir.endsWith(sep) ? dir : dir + sep), | |
| ) | |
| ) { | |
| return 'seed-mount' | |
| } | |
| return 'user-install' | |
| } | |
| /** | |
| * Common plugin telemetry fields keyed off name@marketplace. Returns the | |
| * hash, scope enum, and the redacted-twin columns. Callers add the raw | |
| * _PROTO_* fields separately (those require the PII-tagged marker type). | |
| */ | |
| export function buildPluginTelemetryFields( | |
| name: string, | |
| marketplace: string | undefined, | |
| managedNames: Set<string> | null = null, | |
| ): { | |
| plugin_id_hash: AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS | |
| plugin_scope: AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS | |
| plugin_name_redacted: AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS | |
| marketplace_name_redacted: AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS | |
| is_official_plugin: boolean | |
| } { | |
| const scope = getTelemetryPluginScope(name, marketplace, managedNames) | |
| // Both official marketplaces and builtin plugins are Anthropic-controlled | |
| // — safe to expose real names in the redacted columns. | |
| const isAnthropicControlled = | |
| scope === 'official' || scope === 'default-bundle' | |
| return { | |
| plugin_id_hash: hashPluginId( | |
| name, | |
| marketplace, | |
| ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| plugin_scope: | |
| scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| plugin_name_redacted: (isAnthropicControlled | |
| ? name | |
| : 'third-party') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| marketplace_name_redacted: (isAnthropicControlled && marketplace | |
| ? marketplace | |
| : 'third-party') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| is_official_plugin: isAnthropicControlled, | |
| } | |
| } | |
| /** | |
| * Per-invocation callers (SkillTool, processSlashCommand) pass | |
| * managedNames=null — the session-level tengu_plugin_enabled_for_session | |
| * event carries the authoritative plugin_scope, and per-invocation rows can | |
| * join on plugin_id_hash to recover it. This keeps hot-path call sites free | |
| * of the extra settings read. | |
| */ | |
| export function buildPluginCommandTelemetryFields( | |
| pluginInfo: { pluginManifest: PluginManifest; repository: string }, | |
| managedNames: Set<string> | null = null, | |
| ): ReturnType<typeof buildPluginTelemetryFields> { | |
| const { marketplace } = parsePluginIdentifier(pluginInfo.repository) | |
| return buildPluginTelemetryFields( | |
| pluginInfo.pluginManifest.name, | |
| marketplace, | |
| managedNames, | |
| ) | |
| } | |
| /** | |
| * Emit tengu_plugin_enabled_for_session once per enabled plugin at session | |
| * start. Supplements tengu_skill_loaded (which still fires per-skill) — use | |
| * this for plugin-level aggregates instead of DISTINCT-on-prefix hacks. | |
| * A plugin with 5 skills emits 5 skill_loaded rows but 1 of these. | |
| */ | |
| export function logPluginsEnabledForSession( | |
| plugins: LoadedPlugin[], | |
| managedNames: Set<string> | null, | |
| seedDirs: string[], | |
| ): void { | |
| for (const plugin of plugins) { | |
| const { marketplace } = parsePluginIdentifier(plugin.repository) | |
| logEvent('tengu_plugin_enabled_for_session', { | |
| _PROTO_plugin_name: | |
| plugin.name as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, | |
| ...(marketplace && { | |
| _PROTO_marketplace_name: | |
| marketplace as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, | |
| }), | |
| ...buildPluginTelemetryFields(plugin.name, marketplace, managedNames), | |
| enabled_via: getEnabledVia( | |
| plugin, | |
| managedNames, | |
| seedDirs, | |
| ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| skill_path_count: | |
| (plugin.skillsPath ? 1 : 0) + (plugin.skillsPaths?.length ?? 0), | |
| command_path_count: | |
| (plugin.commandsPath ? 1 : 0) + (plugin.commandsPaths?.length ?? 0), | |
| has_mcp: plugin.manifest.mcpServers !== undefined, | |
| has_hooks: plugin.hooksConfig !== undefined, | |
| ...(plugin.manifest.version && { | |
| version: plugin.manifest | |
| .version as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| }), | |
| }) | |
| } | |
| } | |
| /** | |
| * Bounded-cardinality error bucket for CLI plugin operation failures. | |
| * Maps free-form error messages to 5 stable categories so dashboard | |
| * GROUP BY stays tractable. | |
| */ | |
| export type PluginCommandErrorCategory = | |
| | 'network' | |
| | 'not-found' | |
| | 'permission' | |
| | 'validation' | |
| | 'unknown' | |
| export function classifyPluginCommandError( | |
| error: unknown, | |
| ): PluginCommandErrorCategory { | |
| const msg = String((error as { message?: unknown })?.message ?? error) | |
| if ( | |
| /ENOTFOUND|ECONNREFUSED|EAI_AGAIN|ETIMEDOUT|ECONNRESET|network|Could not resolve|Connection refused|timed out/i.test( | |
| msg, | |
| ) | |
| ) { | |
| return 'network' | |
| } | |
| if (/\b404\b|not found|does not exist|no such plugin/i.test(msg)) { | |
| return 'not-found' | |
| } | |
| if (/\b40[13]\b|EACCES|EPERM|permission denied|unauthorized/i.test(msg)) { | |
| return 'permission' | |
| } | |
| if (/invalid|malformed|schema|validation|parse error/i.test(msg)) { | |
| return 'validation' | |
| } | |
| return 'unknown' | |
| } | |
| /** | |
| * Emit tengu_plugin_load_failed once per error surfaced by session-start | |
| * plugin loading. Pairs with tengu_plugin_enabled_for_session so dashboards | |
| * can compute a load-success rate. PluginError.type is already a bounded | |
| * enum — use it directly as error_category. | |
| */ | |
| export function logPluginLoadErrors( | |
| errors: PluginError[], | |
| managedNames: Set<string> | null, | |
| ): void { | |
| for (const err of errors) { | |
| const { name, marketplace } = parsePluginIdentifier(err.source) | |
| // Not all PluginError variants carry a plugin name (some have pluginId, | |
| // some are marketplace-level). Use the 'plugin' property if present, | |
| // fall back to the name parsed from err.source. | |
| const pluginName = 'plugin' in err && err.plugin ? err.plugin : name | |
| logEvent('tengu_plugin_load_failed', { | |
| error_category: | |
| err.type as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| _PROTO_plugin_name: | |
| pluginName as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, | |
| ...(marketplace && { | |
| _PROTO_marketplace_name: | |
| marketplace as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, | |
| }), | |
| ...buildPluginTelemetryFields(pluginName, marketplace, managedNames), | |
| }) | |
| } | |
| } | |