UNPKG

@fission-ai/openspec

Version:

AI-native system for spec-driven development

145 lines 4.13 kB
/** * Telemetry module for anonymous usage analytics. * * Privacy-first design: * - Only tracks command name and version * - No arguments, file paths, or content * - Opt-out via OPENSPEC_TELEMETRY=0 or DO_NOT_TRACK=1 * - Auto-disabled in CI environments * - Anonymous ID is a random UUID with no relation to the user */ import { PostHog } from 'posthog-node'; import { randomUUID } from 'crypto'; import { getTelemetryConfig, updateTelemetryConfig } from './config.js'; // PostHog API key - public key for client-side analytics // This is safe to embed as it only allows sending events, not reading data const POSTHOG_API_KEY = 'phc_Hthu8YvaIJ9QaFKyTG4TbVwkbd5ktcAFzVTKeMmoW2g'; // Using reverse proxy to avoid ad blockers and keep traffic on our domain const POSTHOG_HOST = 'https://edge.openspec.dev'; let posthogClient = null; let anonymousId = null; /** * Check if telemetry is enabled. * * Disabled when: * - OPENSPEC_TELEMETRY=0 * - DO_NOT_TRACK=1 * - CI=true (any CI environment) */ export function isTelemetryEnabled() { // Check explicit opt-out if (process.env.OPENSPEC_TELEMETRY === '0') { return false; } // Respect DO_NOT_TRACK standard if (process.env.DO_NOT_TRACK === '1') { return false; } // Auto-disable in CI environments if (process.env.CI === 'true') { return false; } return true; } /** * Get or create the anonymous user ID. * Lazily generates a UUID on first call and persists it. */ export async function getOrCreateAnonymousId() { // Return cached value if available if (anonymousId) { return anonymousId; } // Try to load from config const config = await getTelemetryConfig(); if (config.anonymousId) { anonymousId = config.anonymousId; return anonymousId; } // Generate new UUID and persist anonymousId = randomUUID(); await updateTelemetryConfig({ anonymousId }); return anonymousId; } /** * Get the PostHog client instance. * Creates it on first call with CLI-optimized settings. */ function getClient() { if (!posthogClient) { posthogClient = new PostHog(POSTHOG_API_KEY, { host: POSTHOG_HOST, flushAt: 1, // Send immediately, don't batch flushInterval: 0, // No timer-based flushing }); } return posthogClient; } /** * Track a command execution. * * @param commandName - The command name (e.g., 'init', 'change:apply') * @param version - The OpenSpec version */ export async function trackCommand(commandName, version) { if (!isTelemetryEnabled()) { return; } try { const userId = await getOrCreateAnonymousId(); const client = getClient(); client.capture({ distinctId: userId, event: 'command_executed', properties: { command: commandName, version: version, surface: 'cli', $ip: null, // Explicitly disable IP tracking }, }); } catch { // Silent failure - telemetry should never break CLI } } /** * Show first-run telemetry notice if not already seen. */ export async function maybeShowTelemetryNotice() { if (!isTelemetryEnabled()) { return; } try { const config = await getTelemetryConfig(); if (config.noticeSeen) { return; } // Display notice console.log('Note: OpenSpec collects anonymous usage stats. Opt out: OPENSPEC_TELEMETRY=0'); // Mark as seen await updateTelemetryConfig({ noticeSeen: true }); } catch { // Silent failure - telemetry should never break CLI } } /** * Shutdown the PostHog client and flush pending events. * Call this before CLI exit. */ export async function shutdown() { if (!posthogClient) { return; } try { await posthogClient.shutdown(); } catch { // Silent failure - telemetry should never break CLI exit } finally { posthogClient = null; } } //# sourceMappingURL=index.js.map