UNPKG

@vfarcic/dot-ai

Version:

AI-powered development productivity platform that enhances software development workflows through intelligent automation and AI-driven assistance

283 lines (282 loc) 9.24 kB
"use strict"; /** * PostHog Telemetry Client * * Provides anonymous usage telemetry using PostHog. * Follows a singleton pattern with lazy initialization. * PRD #343: Uses plugin system for K8s operations instead of direct @kubernetes/client-node. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.getTelemetry = getTelemetry; exports.shutdownTelemetry = shutdownTelemetry; const posthog_node_1 = require("posthog-node"); const crypto_1 = require("crypto"); const config_1 = require("./config"); const plugin_registry_1 = require("../plugin-registry"); /** * Global telemetry instance (singleton pattern with lazy initialization) */ let telemetryInstance = null; /** * Generate anonymous instance ID from Kubernetes cluster UID * * Uses SHA-256 hash of the kube-system namespace UID to create a stable, * anonymous identifier that's unique per cluster but doesn't reveal cluster identity. * PRD #359: Uses unified plugin registry for kubectl operations. */ async function generateInstanceId() { // PRD #359: Use unified plugin registry for K8s operations if ((0, plugin_registry_1.isPluginInitialized)()) { try { const response = await (0, plugin_registry_1.invokePluginTool)('agentic-tools', 'kubectl_get_resource_json', { resource: 'namespace/kube-system', field: 'metadata' }); if (response.success && response.result) { // Parse the metadata to get UID let metadata; if (typeof response.result === 'string') { metadata = JSON.parse(response.result); } else if (typeof response.result === 'object') { const result = response.result; // Handle nested {success, data} format if (result.data) { metadata = JSON.parse(result.data); } else { metadata = result; } } else { metadata = {}; } if (metadata.uid) { // Hash the UID for anonymity const hash = (0, crypto_1.createHash)('sha256').update(metadata.uid).digest('hex'); return `cluster_${hash.substring(0, 16)}`; } } } catch { // Plugin not available or failed - fall through to random ID } } // Fallback: generate random ID for non-cluster environments or when plugin unavailable const randomId = (0, crypto_1.createHash)('sha256') .update(`${Date.now()}-${Math.random()}`) .digest('hex'); return `local_${randomId.substring(0, 16)}`; } /** * PostHog Telemetry implementation */ class PostHogTelemetry { client = null; config; instanceId = null; initialized = false; initializationPromise = null; constructor(config) { this.config = config; } /** * Initialize the PostHog client (called lazily on first use) */ async initialize() { if (this.initialized) { return; } // Prevent multiple concurrent initializations if (this.initializationPromise) { return this.initializationPromise; } this.initializationPromise = this.doInitialize(); return this.initializationPromise; } async doInitialize() { if (!this.config.enabled) { if (this.config.debug) { console.log('[Telemetry] Telemetry is disabled, skipping initialization'); } this.initialized = true; return; } try { // Generate instance ID this.instanceId = await generateInstanceId(); // Initialize PostHog client this.client = new posthog_node_1.PostHog(this.config.posthogKey, { host: this.config.posthogHost, flushAt: 10, // Batch up to 10 events before sending flushInterval: 10000, // Or send every 10 seconds }); this.initialized = true; if (this.config.debug) { console.log('[Telemetry] PostHog initialized successfully', { instanceId: this.instanceId, posthogHost: this.config.posthogHost, }); } } catch (error) { console.error('[Telemetry] Failed to initialize PostHog:', error); // Don't throw - telemetry failures should never break the app this.initialized = true; } } /** * Check if telemetry is enabled */ isEnabled() { return this.config.enabled; } /** * Detect if this is an internal/test instance * Used by PostHog to filter out internal users from analytics */ isInternalInstance() { // Test environments if (process.env.NODE_ENV === 'test') return true; // CI environments (CI can be 'true', '1', or any truthy value) if ((process.env.CI && process.env.CI !== 'false') || process.env.GITHUB_ACTIONS) return true; return false; } /** * Get base properties included in all events */ getBaseProperties() { return { dot_ai_version: this.config.dotAiVersion, ai_provider: this.config.aiProvider, is_internal: this.isInternalInstance(), }; } /** * Track a telemetry event (fire-and-forget, async) */ trackEvent(event, properties) { if (!this.config.enabled) { return; } // Fire-and-forget initialization and capture this.initialize() .then(() => { if (this.client && this.instanceId) { this.client.capture({ distinctId: this.instanceId, event, properties, }); if (this.config.debug) { console.log('[Telemetry] Event captured:', { event, properties }); } } }) .catch((error) => { // Silently fail - telemetry should never break the app if (this.config.debug) { console.error('[Telemetry] Failed to capture event:', error); } }); } /** * Track tool execution */ trackToolExecution(tool, success, durationMs, mcpClient) { const properties = { ...this.getBaseProperties(), tool, success, duration_ms: durationMs, ...(mcpClient && { mcp_client: mcpClient.name, mcp_client_version: mcpClient.version, }), }; this.trackEvent('tool_executed', properties); } /** * Track tool error */ trackToolError(tool, errorType, mcpClient) { const properties = { ...this.getBaseProperties(), tool, error_type: errorType, ...(mcpClient && { mcp_client: mcpClient.name, mcp_client_version: mcpClient.version, }), }; this.trackEvent('tool_error', properties); } /** * Track MCP client connection */ trackClientConnected(mcpClient) { const properties = { ...this.getBaseProperties(), mcp_client: mcpClient.name, mcp_client_version: mcpClient.version, }; this.trackEvent('client_connected', properties); } /** * Track server start */ trackServerStart(k8sVersion, deploymentMethod) { const properties = { ...this.getBaseProperties(), k8s_version: k8sVersion, deployment_method: deploymentMethod, }; this.trackEvent('server_started', properties); } /** * Track server stop */ trackServerStop(uptimeSeconds) { const properties = { ...this.getBaseProperties(), uptime_seconds: uptimeSeconds, }; this.trackEvent('server_stopped', properties); } /** * Flush pending events and shutdown */ async shutdown() { if (this.client) { if (this.config.debug) { console.log('[Telemetry] Shutting down PostHog client...'); } await this.client.shutdown(); this.client = null; if (this.config.debug) { console.log('[Telemetry] PostHog client shut down successfully'); } } } } /** * Get or create the global telemetry instance */ function getTelemetry() { if (!telemetryInstance) { const config = (0, config_1.loadTelemetryConfig)(); telemetryInstance = new PostHogTelemetry(config); } return telemetryInstance; } /** * Shutdown the global telemetry instance */ async function shutdownTelemetry() { if (telemetryInstance) { await telemetryInstance.shutdown(); telemetryInstance = null; } }