UNPKG

@tanstack/cli

Version:
196 lines (195 loc) 6.47 kB
import { version as nodeVersion } from 'node:process'; import { getTelemetryStatus, markTelemetryNoticeSeen, TELEMETRY_NOTICE_VERSION, } from './telemetry-config.js'; const POSTHOG_API_HOST = 'https://us.i.posthog.com'; const POSTHOG_CAPTURE_ENDPOINT = `${POSTHOG_API_HOST}/capture/`; const POSTHOG_PROJECT_TOKEN = 'phc_xJ2VBahJBzy3BShLhuGpw7EyoSuQtgwXXvhE9BYtHuKQ'; const TELEMETRY_NOTICE = 'TanStack CLI sends anonymous usage telemetry by default. It never sends project names, paths, raw search text, template URLs, add-on config values, or raw error messages. Disable it with `tanstack telemetry disable` or `TANSTACK_CLI_TELEMETRY_DISABLED=1`.'; const TELEMETRY_TIMEOUT_MS = 1200; let telemetryStatusPromise; function getNodeMajorVersion() { return Number.parseInt(nodeVersion.replace(/^v/, '').split('.')[0] || '0', 10); } function cleanProperties(value) { if (Array.isArray(value)) { return value .map((entry) => cleanProperties(entry)) .filter((entry) => entry !== undefined); } if (value && typeof value === 'object') { const cleanedEntries = Object.entries(value) .map(([key, entry]) => [key, cleanProperties(entry)]) .filter(([, entry]) => entry !== undefined); return Object.fromEntries(cleanedEntries); } if (value === undefined) { return undefined; } return value; } function getErrorCode(error) { if (!error || typeof error !== 'object') { return 'unknown_error'; } const message = String(error.message || '').toLowerCase(); if (message.includes('cancel')) { return 'cancelled'; } if (message.includes('invalid')) { return 'invalid_input'; } if (message.includes('not found')) { return 'not_found'; } if (message.includes('timed out')) { return 'timeout'; } if (message.includes('fetch') || message.includes('network') || message.includes('econn')) { return 'network_error'; } if (message.includes('permission') || message.includes('eacces')) { return 'permission_error'; } return 'unknown_error'; } async function fetchTelemetryStatus() { telemetryStatusPromise ?? (telemetryStatusPromise = getTelemetryStatus()); return telemetryStatusPromise; } async function postEvent(event, distinctId, properties) { const controller = new AbortController(); const timeout = setTimeout(() => { controller.abort(); }, TELEMETRY_TIMEOUT_MS); try { await fetch(POSTHOG_CAPTURE_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ api_key: POSTHOG_PROJECT_TOKEN, distinct_id: distinctId, event, properties, }), signal: controller.signal, }); } catch { // Telemetry must never affect CLI behavior. } finally { clearTimeout(timeout); } } export class TelemetryClient { constructor(status) { this.commandProperties = {}; this.pendingSteps = new Map(); this.completedSteps = []; this.disabledBy = status.disabledBy; this.distinctId = status.distinctId; this.enabled = status.enabled && Boolean(status.distinctId); } mergeProperties(properties) { this.commandProperties = { ...this.commandProperties, ...properties, }; } startStep(info) { if (!this.enabled) { return; } this.pendingSteps.set(info.id, { startedAt: Date.now(), type: info.type, }); } finishStep(id) { if (!this.enabled) { return; } const step = this.pendingSteps.get(id); if (!step) { return; } this.pendingSteps.delete(id); this.completedSteps.push({ durationMs: Math.max(Date.now() - step.startedAt, 0), id, type: step.type, }); } async captureCommandStarted(command, properties) { this.mergeProperties(properties); if (!this.enabled || !this.distinctId) { return; } void postEvent('command_started', this.distinctId, cleanProperties({ ...this.baseProperties(), ...this.commandProperties, command, })); } async captureCommandCompleted(command, durationMs) { if (!this.enabled || !this.distinctId) { return; } await postEvent('command_completed', this.distinctId, cleanProperties({ ...this.baseProperties(), ...this.commandProperties, command, duration_ms: durationMs, result: 'success', steps: this.completedSteps.map((step) => ({ duration_ms: step.durationMs, id: step.id, type: step.type, })), })); } async captureCommandFailed(command, durationMs, error) { if (!this.enabled || !this.distinctId) { return; } await postEvent('command_failed', this.distinctId, cleanProperties({ ...this.baseProperties(), ...this.commandProperties, command, duration_ms: durationMs, error_code: getErrorCode(error), result: 'failed', steps: this.completedSteps.map((step) => ({ duration_ms: step.durationMs, id: step.id, type: step.type, })), })); } baseProperties() { return { $lib: 'tanstack-cli', disabled_by: this.disabledBy, node_major: getNodeMajorVersion(), os_arch: process.arch, os_platform: process.platform, }; } } export async function createTelemetryClient(opts) { const status = await fetchTelemetryStatus(); if (status.enabled && status.noticeVersion < TELEMETRY_NOTICE_VERSION && !opts?.json) { console.error(TELEMETRY_NOTICE); await markTelemetryNoticeSeen(); telemetryStatusPromise = undefined; } return new TelemetryClient(await fetchTelemetryStatus()); } export function resetTelemetryStateForTests() { telemetryStatusPromise = undefined; }