/**
 * Product Analytics (Statsig) & Event Tracking
 *
 * Provides anonymous usage analytics for the CLI using Statsig client SDK.
 * All data is anonymous — identified by a machine UUID only, no PII is ever sent.
 *
 * Telemetry can be disabled via:
 * - `prlt telemetry disable`
 * - `DO_NOT_TRACK=1` environment variable
 * - `PRLT_TELEMETRY_DISABLED=1` environment variable
 *
 * Privacy:
 * - Anonymous machine ID (UUID) stored in ~/.proletariat/telemetry.json
 * - No file paths, usernames, or ticket content ever sent
 * - Events are written to a local queue file and flushed on the next run
 *
 * Write-ahead log:
 * - trackEvent() writes events to ~/.proletariat/telemetry-queue.json synchronously
 * - On the next command run, queued events are flushed to Statsig and the queue is cleared
 * - Events are delayed by at most one command invocation — acceptable for analytics
 * - This adds zero latency and ensures no events are lost regardless of Statsig init timing
 * - Shutdown never flushes the queue — events persist until the next run confirms delivery
 */
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as crypto from 'node:crypto';
import { getMachineConfigDir, ensureMachineConfigDir } from '../machine-config.js';
// Statsig client SDK key (public — safe to embed in open source repos)
const STATSIG_CLIENT_KEY = 'client-kvxMxRhn9NFSmH8orl7e2W9nYTfWVS7Kjf7yRTdIecc';
// Cap the queue size to prevent unbounded growth if events never flush
const MAX_QUEUE_SIZE = 1000;
// Module-level state
let statsigClient = null;
let telemetryConfig = null;
let cliVersion = null;
let initPromise = null;
let analyticsShutdown = false;
// ─── Telemetry Config ────────────────────────────────────────────────────────
function getTelemetryConfigPath() {
    return path.join(getMachineConfigDir(), 'telemetry.json');
}
function readTelemetryConfig() {
    if (telemetryConfig)
        return telemetryConfig;
    const configPath = getTelemetryConfigPath();
    if (fs.existsSync(configPath)) {
        try {
            const content = fs.readFileSync(configPath, 'utf-8');
            telemetryConfig = JSON.parse(content);
            return telemetryConfig;
        }
        catch {
            // Fall through to create new config
        }
    }
    // Create new config with generated machine ID
    telemetryConfig = {
        enabled: true,
        noticeShown: false,
        machineId: crypto.randomUUID(),
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
    };
    writeTelemetryConfig(telemetryConfig);
    return telemetryConfig;
}
function writeTelemetryConfig(config) {
    ensureMachineConfigDir();
    const configPath = getTelemetryConfigPath();
    const tempPath = `${configPath}.tmp.${process.pid}`;
    try {
        fs.writeFileSync(tempPath, JSON.stringify(config, null, 2), 'utf-8');
        fs.renameSync(tempPath, configPath);
    }
    catch {
        try {
            if (fs.existsSync(tempPath))
                fs.unlinkSync(tempPath);
        }
        catch {
            // Ignore cleanup errors
        }
    }
}
// ─── Opt-out Detection ───────────────────────────────────────────────────────
/**
 * Check if telemetry is disabled by environment variables or config.
 */
export function isTelemetryEnabled() {
    // Environment variable opt-outs (standard + prlt-specific)
    if (process.env.DO_NOT_TRACK === '1' || process.env.DO_NOT_TRACK === 'true')
        return false;
    if (process.env.PRLT_TELEMETRY_DISABLED === '1' || process.env.PRLT_TELEMETRY_DISABLED === 'true')
        return false;
    // Config-based opt-out
    const config = readTelemetryConfig();
    return config.enabled;
}
/**
 * Enable telemetry.
 */
export function enableTelemetry() {
    const config = readTelemetryConfig();
    config.enabled = true;
    config.updatedAt = new Date().toISOString();
    telemetryConfig = config;
    writeTelemetryConfig(config);
}
/**
 * Disable telemetry.
 */
export function disableTelemetry() {
    const config = readTelemetryConfig();
    config.enabled = false;
    config.updatedAt = new Date().toISOString();
    telemetryConfig = config;
    writeTelemetryConfig(config);
}
/**
 * Get current telemetry status for display.
 */
export function getTelemetryStatus() {
    const config = readTelemetryConfig();
    // Check for env var overrides
    if (process.env.DO_NOT_TRACK === '1' || process.env.DO_NOT_TRACK === 'true') {
        return { enabled: false, machineId: config.machineId, envOverride: true, envVar: 'DO_NOT_TRACK' };
    }
    if (process.env.PRLT_TELEMETRY_DISABLED === '1' || process.env.PRLT_TELEMETRY_DISABLED === 'true') {
        return { enabled: false, machineId: config.machineId, envOverride: true, envVar: 'PRLT_TELEMETRY_DISABLED' };
    }
    return { enabled: config.enabled, machineId: config.machineId, envOverride: false };
}
// ─── First-Run Notice ────────────────────────────────────────────────────────
/**
 * Show a one-time notice that telemetry is active.
 * Only shown in interactive TTY sessions.
 */
function showTelemetryNotice() {
    const config = readTelemetryConfig();
    if (config.noticeShown)
        return;
    if (!process.stdout.isTTY) {
        // Mark as shown so we don't try again, but don't print in non-TTY
        config.noticeShown = true;
        writeTelemetryConfig(config);
        return;
    }
    console.log('');
    console.log('Proletariat collects anonymous usage analytics to improve the CLI.');
    console.log('Only command names and durations are tracked — no values, arguments, or personal data.');
    console.log('You can opt out at any time:');
    console.log('');
    console.log('  prlt telemetry disable');
    console.log('');
    config.noticeShown = true;
    telemetryConfig = config;
    writeTelemetryConfig(config);
}
// ─── Machine ID ──────────────────────────────────────────────────────────────
/**
 * Get the anonymous machine ID for this installation.
 */
export function getMachineId() {
    return readTelemetryConfig().machineId;
}
// ─── Event Queue (Write-Ahead Log) ───────────────────────────────────────────
function getQueuePath() {
    return path.join(getMachineConfigDir(), 'telemetry-queue.json');
}
/**
 * Append an event to the on-disk queue. Synchronous — adds zero async latency.
 */
function writeEventToQueue(event) {
    try {
        ensureMachineConfigDir();
        const queuePath = getQueuePath();
        let queue = [];
        if (fs.existsSync(queuePath)) {
            try {
                const raw = fs.readFileSync(queuePath, 'utf-8');
                const parsed = JSON.parse(raw);
                if (Array.isArray(parsed))
                    queue = parsed;
            }
            catch {
                // Corrupted file — start fresh
            }
        }
        queue.push(event);
        // Cap queue size to prevent unbounded growth
        if (queue.length > MAX_QUEUE_SIZE) {
            queue = queue.slice(queue.length - MAX_QUEUE_SIZE);
        }
        // Atomic write via temp file + rename
        const tempPath = `${queuePath}.tmp.${process.pid}`;
        fs.writeFileSync(tempPath, JSON.stringify(queue), 'utf-8');
        fs.renameSync(tempPath, queuePath);
    }
    catch {
        // Never let queue errors affect the CLI
    }
}
/**
 * Atomically read and clear the queue file.
 * Uses rename to claim the file so concurrent processes don't double-send.
 */
function readAndClearQueue() {
    const queuePath = getQueuePath();
    const claimPath = `${queuePath}.flush.${process.pid}`;
    try {
        // Atomic rename — only one process can claim the file
        fs.renameSync(queuePath, claimPath);
    }
    catch {
        // File doesn't exist or another process already claimed it
        return [];
    }
    try {
        const raw = fs.readFileSync(claimPath, 'utf-8');
        const parsed = JSON.parse(raw);
        return Array.isArray(parsed) ? parsed : [];
    }
    catch {
        return [];
    }
    finally {
        try {
            fs.unlinkSync(claimPath);
        }
        catch { /* ignore */ }
    }
}
/**
 * Flush queued events to Statsig. Requires an initialized Statsig client.
 * Called during shutdown or at the start of the next command run.
 */
export function flushQueuedEvents() {
    if (!statsigClient)
        return;
    const events = readAndClearQueue();
    for (const event of events) {
        try {
            statsigClient.logEvent(event.name, event.value, event.metadata);
        }
        catch {
            // Drop individual events silently
        }
    }
}
// ─── Statsig Client ──────────────────────────────────────────────────────────
/**
 * Initialize the Statsig SDK and flush any queued events from previous runs.
 * Called from the init hook. The Statsig init is fire-and-forget — event
 * tracking does not depend on it (events go to the disk queue instead).
 * Statsig is still needed for feature flags.
 *
 * No-op if telemetry is disabled.
 */
export function initAnalytics(version) {
    cliVersion = version;
    if (!isTelemetryEnabled())
        return;
    showTelemetryNotice();
    // Start Statsig init in background — not needed for event logging,
    // but enables feature flags and allows queue flush if init completes in time
    initPromise = (async () => {
        try {
            const { StatsigClient: StatsigClientClass } = await import('@statsig/js-client');
            const client = new StatsigClientClass(STATSIG_CLIENT_KEY, { userID: getMachineId(), custom: { cli_version: version } });
            await client.initializeAsync();
            // If shutdown already ran while we were initializing, don't set state
            // or flush — just clean up the client and bail out
            if (analyticsShutdown) {
                try {
                    client.shutdown();
                }
                catch { /* ignore */ }
                return;
            }
            statsigClient = client;
            // Share Statsig client with feature-flags for synchronous gate checks
            const { setStatsigClient } = await import('./feature-flags.js');
            setStatsigClient(client);
            // Statsig is ready — flush any events queued from previous runs
            flushQueuedEvents();
        }
        catch {
            // If Statsig can't initialize, fail silently — analytics should never break the CLI
            statsigClient = null;
        }
    })();
}
// ─── Event Tracking ──────────────────────────────────────────────────────────
/**
 * Track an analytics event. Writes to a local disk queue synchronously —
 * never blocks on network I/O. Events are flushed to Statsig on the next
 * command run (or during this run's shutdown if Statsig initialized in time).
 *
 * @param eventName - Event name (e.g., 'command_run')
 * @param value - Optional numeric or string value
 * @param metadata - Event-specific metadata
 */
export function trackEvent(eventName, value, metadata) {
    if (!isTelemetryEnabled())
        return;
    try {
        // Convert metadata values to strings as required by the client SDK
        const stringMetadata = metadata
            ? Object.fromEntries(Object.entries({ ...metadata, cli_version: cliVersion }).map(([k, v]) => [k, String(v)]))
            : cliVersion
                ? { cli_version: cliVersion }
                : null;
        writeEventToQueue({
            name: eventName,
            value,
            metadata: stringMetadata,
            timestamp: new Date().toISOString(),
        });
    }
    catch {
        // Never let analytics errors affect the CLI
    }
}
/**
 * Track a command execution.
 */
export function trackCommandRun(options) {
    trackEvent('command_run', options.durationMs, {
        command: options.command,
        success: options.success,
        flags_used: (options.flags ?? []).join(','),
    });
}
/**
 * Track an agent being spawned.
 */
export function trackAgentSpawned(options) {
    trackEvent('agent_spawned', null, {
        executor: options.executor,
        environment: options.environment,
        action: options.action,
        ephemeral: String(options.ephemeral),
    });
}
/**
 * Track an orchestrator being started.
 */
export function trackOrchestratorStarted(options) {
    trackEvent('orchestrator_started', null, {
        executor: options.executor,
        name: options.name,
        display_mode: options.displayMode,
    });
}
/**
 * Track work being completed.
 */
export function trackWorkCompleted(options) {
    trackEvent('work_completed', options.durationMs, {
        pr_created: String(options.prCreated),
    });
}
/**
 * Track a PR being created.
 */
export function trackPRCreated(options) {
    trackEvent('pr_created', null, {
        source: options.source,
    });
}
/**
 * Track an MCP tool call.
 */
export function trackMCPToolCalled(options) {
    trackEvent('mcp_tool_called', null, {
        tool_name: options.toolName,
        success: String(options.success),
    });
}
// ─── Shutdown ────────────────────────────────────────────────────────────────
/**
 * Flush queued events (if Statsig is ready) and shut down the Statsig client.
 * No timeout, no waiting for init — events are safely on disk if Statsig
 * didn't initialize in time, and will be flushed on the next command run.
 */
export async function shutdownAnalytics() {
    // Mark as shut down so the in-progress async Statsig init (if any)
    // won't set statsigClient or flush the queue after we're done
    analyticsShutdown = true;
    // Abandon any in-progress init — events are on disk, no need to wait
    initPromise = null;
    // Shut down the Statsig client if it initialized during this run.
    // Do NOT flush the queue here — events written during this command
    // (by trackCommandRun in the postrun hook) are safely on disk and will
    // be flushed on the next command run when Statsig is warm. Flushing
    // here would delete events from disk before we can confirm they were
    // actually sent, violating the write-ahead log guarantee.
    if (statsigClient) {
        try {
            statsigClient.shutdown();
        }
        catch {
            // Never let shutdown errors affect the CLI
        }
        statsigClient = null;
        try {
            const { setStatsigClient } = await import('./feature-flags.js');
            setStatsigClient(null);
        }
        catch {
            // Ignore
        }
    }
}
//# sourceMappingURL=analytics.js.map