diff --git a/scripts/build.ts b/scripts/build.ts index 0a00f2c9..137fadb6 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -9,6 +9,7 @@ */ import { readFileSync } from 'fs' +import { noTelemetryPlugin } from './no-telemetry-plugin' const pkg = JSON.parse(readFileSync('./package.json', 'utf-8')) const version = pkg.version @@ -64,6 +65,7 @@ const result = await Bun.build({ 'MACRO.NATIVE_PACKAGE_URL': 'undefined', }, plugins: [ + noTelemetryPlugin, { name: 'bun-bundle-shim', setup(build) { diff --git a/scripts/no-telemetry-plugin.ts b/scripts/no-telemetry-plugin.ts new file mode 100644 index 00000000..dc83b50f --- /dev/null +++ b/scripts/no-telemetry-plugin.ts @@ -0,0 +1,221 @@ +/** + * No-Telemetry Build Plugin for OpenClaude + * + * Replaces all analytics, telemetry, and phone-home modules with no-op stubs + * at compile time. Zero runtime cost, zero network calls to Anthropic. + * + * This file is NOT tracked upstream — merge conflicts are impossible. + * Only build.ts needs a one-line import + one-line array entry. + * + * Kills: + * - GrowthBook remote feature flags (api.anthropic.com) + * - Datadog event intake + * - 1P event logging (api.anthropic.com/api/event_logging/batch) + * - BigQuery metrics exporter (api.anthropic.com/api/claude_code/metrics) + * - Perfetto / OpenTelemetry session tracing + * - Auto-updater (storage.googleapis.com, npm registry) + * - Plugin fetch telemetry + * - Transcript / feedback sharing + */ + +import type { BunPlugin } from 'bun' + +// Module path (relative to src/, without extension) → stub source +const stubs: Record = { + + // ─── Analytics core ───────────────────────────────────────────── + + 'services/analytics/index': ` +export function stripProtoFields(metadata) { return metadata; } +export function attachAnalyticsSink() {} +export function logEvent() {} +export async function logEventAsync() {} +export function _resetForTesting() {} +`, + + 'services/analytics/growthbook': ` +const noop = () => {}; +export function onGrowthBookRefresh() { return noop; } +export function hasGrowthBookEnvOverride() { return false; } +export function getAllGrowthBookFeatures() { return {}; } +export function getGrowthBookConfigOverrides() { return {}; } +export function setGrowthBookConfigOverride() {} +export function clearGrowthBookConfigOverrides() {} +export function getApiBaseUrlHost() { return undefined; } +export const initializeGrowthBook = async () => null; +export async function getFeatureValue_DEPRECATED(feature, defaultValue) { return defaultValue; } +export function getFeatureValue_CACHED_MAY_BE_STALE(feature, defaultValue) { return defaultValue; } +export function getFeatureValue_CACHED_WITH_REFRESH(feature, defaultValue) { return defaultValue; } +export function checkStatsigFeatureGate_CACHED_MAY_BE_STALE() { return false; } +export async function checkSecurityRestrictionGate() { return false; } +export async function checkGate_CACHED_OR_BLOCKING() { return false; } +export function refreshGrowthBookAfterAuthChange() {} +export function resetGrowthBook() {} +export async function refreshGrowthBookFeatures() {} +export function setupPeriodicGrowthBookRefresh() {} +export function stopPeriodicGrowthBookRefresh() {} +export async function getDynamicConfig_BLOCKS_ON_INIT(configName, defaultValue) { return defaultValue; } +export function getDynamicConfig_CACHED_MAY_BE_STALE(configName, defaultValue) { return defaultValue; } +`, + + 'services/analytics/sink': ` +export function initializeAnalyticsGates() {} +export function initializeAnalyticsSink() {} +`, + + 'services/analytics/config': ` +export function isAnalyticsDisabled() { return true; } +export function isFeedbackSurveyDisabled() { return true; } +`, + + 'services/analytics/datadog': ` +export const initializeDatadog = async () => false; +export async function shutdownDatadog() {} +export async function trackDatadogEvent() {} +`, + + 'services/analytics/firstPartyEventLogger': ` +export function getEventSamplingConfig() { return {}; } +export function shouldSampleEvent() { return null; } +export async function shutdown1PEventLogging() {} +export function is1PEventLoggingEnabled() { return false; } +export function logEventTo1P() {} +export function logGrowthBookExperimentTo1P() {} +export function initialize1PEventLogging() {} +export async function reinitialize1PEventLoggingIfConfigChanged() {} +`, + + 'services/analytics/firstPartyEventLoggingExporter': ` +export class FirstPartyEventLoggingExporter { + constructor() {} + async export(logs, resultCallback) { resultCallback({ code: 0 }); } + async getQueuedEventCount() { return 0; } + async shutdown() {} + async forceFlush() {} +} +`, + + 'services/analytics/metadata': ` +export function sanitizeToolNameForAnalytics(toolName) { return toolName; } +export function isToolDetailsLoggingEnabled() { return false; } +export function isAnalyticsToolDetailsLoggingEnabled() { return false; } +export function mcpToolDetailsForAnalytics() { return {}; } +export function extractMcpToolDetails() { return undefined; } +export function extractSkillName() { return undefined; } +export function extractToolInputForTelemetry() { return undefined; } +export function getFileExtensionForAnalytics() { return undefined; } +export function getFileExtensionsFromBashCommand() { return undefined; } +export async function getEventMetadata() { return {}; } +export function to1PEventFormat() { return {}; } +`, + + // ─── Telemetry subsystems ─────────────────────────────────────── + + 'utils/telemetry/bigqueryExporter': ` +export class BigQueryMetricsExporter { + constructor() {} + async export(metrics, resultCallback) { resultCallback({ code: 0 }); } + async shutdown() {} + async forceFlush() {} + selectAggregationTemporality() { return 0; } +} +`, + + 'utils/telemetry/perfettoTracing': ` +export function initializePerfettoTracing() {} +export function isPerfettoTracingEnabled() { return false; } +export function registerAgent() {} +export function unregisterAgent() {} +export function startLLMRequestPerfettoSpan() { return ''; } +export function endLLMRequestPerfettoSpan() {} +export function startToolPerfettoSpan() { return ''; } +export function endToolPerfettoSpan() {} +export function startUserInputPerfettoSpan() { return ''; } +export function endUserInputPerfettoSpan() {} +export function emitPerfettoInstant() {} +export function emitPerfettoCounter() {} +export function startInteractionPerfettoSpan() { return ''; } +export function endInteractionPerfettoSpan() {} +export function getPerfettoEvents() { return []; } +export function resetPerfettoTracer() {} +export async function triggerPeriodicWriteForTesting() {} +export function evictStaleSpansForTesting() {} +export const MAX_EVENTS_FOR_TESTING = 0; +export function evictOldestEventsForTesting() {} +`, + + 'utils/telemetry/sessionTracing': ` +const noopSpan = { + end() {}, setAttribute() {}, setStatus() {}, + recordException() {}, addEvent() {}, isRecording() { return false; }, +}; +export function isBetaTracingEnabled() { return false; } +export function isEnhancedTelemetryEnabled() { return false; } +export function startInteractionSpan() { return noopSpan; } +export function endInteractionSpan() {} +export function startLLMRequestSpan() { return noopSpan; } +export function endLLMRequestSpan() {} +export function startToolSpan() { return noopSpan; } +export function startToolBlockedOnUserSpan() { return noopSpan; } +export function endToolBlockedOnUserSpan() {} +export function startToolExecutionSpan() { return noopSpan; } +export function endToolExecutionSpan() {} +export function endToolSpan() {} +export function addToolContentEvent() {} +export function getCurrentSpan() { return null; } +export async function executeInSpan(spanName, fn) { return fn(noopSpan); } +export function startHookSpan() { return noopSpan; } +export function endHookSpan() {} +`, + + // ─── Auto-updater (phones home to GCS + npm) ────────────────── + + 'utils/autoUpdater': ` +export async function assertMinVersion() {} +export async function getMaxVersion() { return undefined; } +export async function getMaxVersionMessage() { return undefined; } +export function shouldSkipVersion() { return true; } +export function getLockFilePath() { return '/tmp/openclaude-update.lock'; } +export async function checkGlobalInstallPermissions() { return { hasPermissions: false, npmPrefix: null }; } +export async function getLatestVersion() { return null; } +export async function getNpmDistTags() { return { latest: null, stable: null }; } +export async function getLatestVersionFromGcs() { return null; } +export async function getGcsDistTags() { return { latest: null, stable: null }; } +export async function getVersionHistory() { return []; } +export async function installGlobalPackage() { return 'success'; } +`, + + // ─── Plugin fetch telemetry (not the marketplace itself) ─────── + + 'utils/plugins/fetchTelemetry': ` +export function logPluginFetch() {} +export function classifyFetchError() { return 'disabled'; } +`, + + // ─── Transcript / feedback sharing ───────────────────────────── + + 'components/FeedbackSurvey/submitTranscriptShare': ` +export async function submitTranscriptShare() { return { success: false }; } +`, +} + +export const noTelemetryPlugin: BunPlugin = { + name: 'no-telemetry', + setup(build) { + for (const [modulePath, contents] of Object.entries(stubs)) { + // Build regex that matches the resolved file path on any OS + // e.g. "services/analytics/growthbook" → /services[/\\]analytics[/\\]growthbook\.(ts|js)$/ + const escaped = modulePath + .replace(/\//g, '[/\\\\]') + .replace(/\./g, '\\.') + const filter = new RegExp(`${escaped}\\.(ts|js)$`) + + build.onLoad({ filter }, () => ({ + contents, + loader: 'js', + })) + } + + console.log(` 🔇 no-telemetry: stubbed ${Object.keys(stubs).length} modules`) + }, +}