From 2264aea59179b9580301223ea212cfe3888ec6d7 Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Thu, 9 Apr 2026 13:58:03 +0800 Subject: [PATCH] Reduce misleading telemetry shims in the open build The open build already treated analytics and tracing as inert, but several empty sink and shutdown modules still made startup and exit paths look like they initialized or flushed telemetry. This trims those dead compatibility layers, updates the surrounding comments to match reality, and adds small regression tests that lock in the inert analytics boundary and disabled transcript sharing behavior. Constraint: Preserve the no-op logEvent/logOTelEvent compatibility surface for existing call sites Constraint: Avoid touching unrelated bridge and session work already in progress in the worktree Rejected: Remove every remaining logEvent/logOTelEvent call site | Too broad for a safe first cleanup pass Rejected: Keep the empty sink/shutdown modules | Continued to mislead future audits and maintenance Confidence: high Scope-risk: narrow Reversibility: clean Directive: Treat remaining analytics and GrowthBook helpers as compatibility surfaces until each call path is individually proven dead Tested: bun test src/services/analytics/index.test.ts src/components/FeedbackSurvey/submitTranscriptShare.test.ts Tested: bun run ./scripts/build.ts Not-tested: bun x tsc --noEmit (repository has pre-existing unrelated type errors) --- src/bridge/bridgeMain.ts | 19 +++-------- src/components/Feedback.tsx | 2 -- .../submitTranscriptShare.test.ts | 14 ++++++++ src/main.tsx | 7 ++-- src/services/analytics/datadog.ts | 9 ------ .../analytics/firstPartyEventLogger.ts | 16 ---------- src/services/analytics/index.test.ts | 32 +++++++++++++++++++ src/services/analytics/sink.ts | 10 ------ src/setup.ts | 9 ++---- src/utils/claudeInChrome/mcpServer.ts | 11 ++----- src/utils/computerUse/mcpServer.ts | 9 ++---- src/utils/errorLogSink.ts | 2 -- src/utils/gracefulShutdown.ts | 19 ++--------- src/utils/sinks.ts | 7 ++-- 14 files changed, 63 insertions(+), 103 deletions(-) create mode 100644 src/components/FeedbackSurvey/submitTranscriptShare.test.ts delete mode 100644 src/services/analytics/datadog.ts delete mode 100644 src/services/analytics/firstPartyEventLogger.ts create mode 100644 src/services/analytics/index.test.ts delete mode 100644 src/services/analytics/sink.ts diff --git a/src/bridge/bridgeMain.ts b/src/bridge/bridgeMain.ts index 1f9fdf9..9e79297 100644 --- a/src/bridge/bridgeMain.ts +++ b/src/bridge/bridgeMain.ts @@ -3,8 +3,6 @@ import { randomUUID } from 'crypto' import { tmpdir } from 'os' import { basename, join, resolve } from 'path' import { getRemoteSessionUrl } from '../constants/product.js' -import { shutdownDatadog } from '../services/analytics/datadog.js' -import { shutdown1PEventLogging } from '../services/analytics/firstPartyEventLogger.js' import { checkGate_CACHED_OR_BLOCKING } from '../services/analytics/growthbook.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, @@ -30,7 +28,7 @@ import { import { formatDuration } from './bridgeStatusUtil.js' import { createBridgeLogger } from './bridgeUI.js' import { createCapacityWake } from './capacityWake.js' -import { describeAxiosError } from './debugUtils.js' +import { describeAxiosError, summarizeBridgeErrorForDebug } from './debugUtils.js' import { createTokenRefreshScheduler } from './jwtUtils.js' import { getPollIntervalConfig } from './pollConfig.js' import { toCompatSessionId, toInfraSessionId } from './sessionIdCompat.js' @@ -2041,16 +2039,15 @@ export async function bridgeMain(args: string[]): Promise { ) enableConfigs() - // Initialize analytics and error reporting sinks. The bridge bypasses the - // setup() init flow, so we call initSinks() directly to attach sinks here. + // Initialize shared sinks. The bridge bypasses setup(), so it attaches the + // local error-log sink directly here. const { initSinks } = await import('../utils/sinks.js') initSinks() // Gate-aware validation: --spawn / --capacity / --create-session-in-dir require // the multi-session gate. parseArgs has already validated flag combinations; // here we only check the gate since that requires an async GrowthBook call. - // Runs after enableConfigs() (GrowthBook cache reads global config) and after - // initSinks() so the denial event can be enqueued. + // Runs after enableConfigs() because GrowthBook cache reads global config. const multiSessionEnabled = await isMultiSessionSpawnEnabled() if (usedMultiSessionFeature && !multiSessionEnabled) { await logEventAsync('tengu_bridge_multi_session_denied', { @@ -2058,14 +2055,6 @@ export async function bridgeMain(args: string[]): Promise { used_capacity: parsedCapacity !== undefined, used_create_session_in_dir: parsedCreateSessionInDir !== undefined, }) - // logEventAsync only enqueues — process.exit() discards buffered events. - // Flush explicitly, capped at 500ms to match gracefulShutdown.ts. - // (sleep() doesn't unref its timer, but process.exit() follows immediately - // so the ref'd timer can't delay shutdown.) - await Promise.race([ - Promise.all([shutdown1PEventLogging(), shutdownDatadog()]), - sleep(500, undefined, { unref: true }), - ]).catch(() => {}) // biome-ignore lint/suspicious/noConsole: intentional error output console.error( 'Error: Multi-session Remote Control is not enabled for your account yet.', diff --git a/src/components/Feedback.tsx b/src/components/Feedback.tsx index dd47629..f6051b5 100644 --- a/src/components/Feedback.tsx +++ b/src/components/Feedback.tsx @@ -2,8 +2,6 @@ import * as React from 'react'; import { useCallback, useEffect, useState } from 'react'; import { readFile, stat } from 'fs/promises'; import { getLastAPIRequest } from 'src/bootstrap/state.js'; -import { logEventTo1P } from 'src/services/analytics/firstPartyEventLogger.js'; -import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; import { getLastAssistantMessage, normalizeMessagesForAPI } from 'src/utils/messages.js'; import type { CommandResultDisplay } from '../commands.js'; import { useTerminalSize } from '../hooks/useTerminalSize.js'; diff --git a/src/components/FeedbackSurvey/submitTranscriptShare.test.ts b/src/components/FeedbackSurvey/submitTranscriptShare.test.ts new file mode 100644 index 0000000..016603c --- /dev/null +++ b/src/components/FeedbackSurvey/submitTranscriptShare.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'bun:test' + +import { submitTranscriptShare } from './submitTranscriptShare.js' + +describe('submitTranscriptShare', () => { + it('returns the disabled result in this build', async () => { + await expect( + submitTranscriptShare([], 'good_feedback_survey', 'appearance-id'), + ).resolves.toEqual({ + success: false, + disabled: true, + }) + }) +}) diff --git a/src/main.tsx b/src/main.tsx index 5e9ef25..e6923de 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -864,11 +864,8 @@ async function run(): Promise { process.title = 'claude'; } - // Attach logging sinks so subcommand handlers can use logEvent/logError. - // Before PR #11106 logEvent dispatched directly; after, events queue until - // a sink attaches. setup() attaches sinks for the default command, but - // subcommands (doctor, mcp, plugin, auth) never call setup() and would - // silently drop events on process.exit(). Both inits are idempotent. + // Attach shared sinks for subcommands that bypass setup(). Today this is + // just the local error-log sink; analytics/event logging is already inert. const { initSinks } = await import('./utils/sinks.js'); diff --git a/src/services/analytics/datadog.ts b/src/services/analytics/datadog.ts deleted file mode 100644 index 61c89c8..0000000 --- a/src/services/analytics/datadog.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Datadog analytics egress is disabled in this build. - * - * Only shutdown compatibility remains for existing cleanup paths. - */ - -export async function shutdownDatadog(): Promise { - return -} diff --git a/src/services/analytics/firstPartyEventLogger.ts b/src/services/analytics/firstPartyEventLogger.ts deleted file mode 100644 index 3ffd889..0000000 --- a/src/services/analytics/firstPartyEventLogger.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Anthropic 1P event logging egress is disabled in this build. - * - * Only the shutdown and feedback call sites still need a local stub. - */ - -export async function shutdown1PEventLogging(): Promise { - return -} - -export function logEventTo1P( - _eventName: string, - _metadata: Record = {}, -): void { - return -} diff --git a/src/services/analytics/index.test.ts b/src/services/analytics/index.test.ts new file mode 100644 index 0000000..fcc91f6 --- /dev/null +++ b/src/services/analytics/index.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'bun:test' + +import { + _resetForTesting, + attachAnalyticsSink, + logEvent, + logEventAsync, +} from './index.js' + +describe('analytics compatibility boundary', () => { + it('stays inert even if a sink is attached', async () => { + let syncCalls = 0 + let asyncCalls = 0 + + attachAnalyticsSink({ + logEvent: () => { + syncCalls += 1 + }, + logEventAsync: async () => { + asyncCalls += 1 + }, + }) + + logEvent('tengu_test_event', {}) + await logEventAsync('tengu_test_event_async', {}) + + expect(syncCalls).toBe(0) + expect(asyncCalls).toBe(0) + + _resetForTesting() + }) +}) diff --git a/src/services/analytics/sink.ts b/src/services/analytics/sink.ts deleted file mode 100644 index 0d54a6b..0000000 --- a/src/services/analytics/sink.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Analytics sink implementation - * - * Telemetry sinks are disabled in this build. The exported functions remain so - * startup code does not need to special-case the open build. - */ - -export function initializeAnalyticsSink(): void { - return -} diff --git a/src/setup.ts b/src/setup.ts index 6c6eab7..23f4482 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -368,13 +368,10 @@ export async function setup( ) // Start team memory sync watcher } } - initSinks() // Attach error log sink and analytics compatibility stubs + initSinks() // Attach the shared error-log sink - // Session-success-rate denominator. Emit immediately after the analytics - // sink is attached — before any parsing, fetching, or I/O that could throw. - // inc-3694 (P0 CHANGELOG crash) threw at checkForReleaseNotes below; every - // event after this point was dead. This beacon is the earliest reliable - // "process started" signal for release health monitoring. + // Keep the startup compatibility event as early as possible, before any + // parsing, fetching, or I/O that could throw. logEvent('tengu_started', {}) void prefetchApiKeyFromApiKeyHelperIfSafe(getIsNonInteractiveSession()) // Prefetch safely - only executes if trust already confirmed diff --git a/src/utils/claudeInChrome/mcpServer.ts b/src/utils/claudeInChrome/mcpServer.ts index 4195d2c..0a17ca1 100644 --- a/src/utils/claudeInChrome/mcpServer.ts +++ b/src/utils/claudeInChrome/mcpServer.ts @@ -6,14 +6,11 @@ import { } from '@ant/claude-for-chrome-mcp' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { format } from 'util' -import { shutdownDatadog } from '../../services/analytics/datadog.js' -import { shutdown1PEventLogging } from '../../services/analytics/firstPartyEventLogger.js' import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, } from '../../services/analytics/index.js' -import { initializeAnalyticsSink } from '../../services/analytics/sink.js' import { getClaudeAIOAuthTokens } from '../auth.js' import { enableConfigs, getGlobalConfig, saveGlobalConfig } from '../config.js' import { logForDebugging } from '../debug.js' @@ -225,7 +222,7 @@ export function createChromeContext( } = {} if (metadata) { for (const [key, value] of Object.entries(metadata)) { - // Rename 'status' to 'bridge_status' to avoid Datadog's reserved field + // Keep the status field namespaced to avoid downstream collisions. const safeKey = key === 'status' ? 'bridge_status' : key if (typeof value === 'boolean' || typeof value === 'number') { safeMetadata[safeKey] = value @@ -247,22 +244,18 @@ export function createChromeContext( export async function runClaudeInChromeMcpServer(): Promise { enableConfigs() - initializeAnalyticsSink() const context = createChromeContext() const server = createClaudeForChromeMcpServer(context) const transport = new StdioServerTransport() // Exit when parent process dies (stdin pipe closes). - // Flush analytics before exiting so final-batch events (e.g. disconnect) aren't lost. let exiting = false - const shutdownAndExit = async (): Promise => { + const shutdownAndExit = (): void => { if (exiting) { return } exiting = true - await shutdown1PEventLogging() - await shutdownDatadog() // eslint-disable-next-line custom-rules/no-process-exit process.exit(0) } diff --git a/src/utils/computerUse/mcpServer.ts b/src/utils/computerUse/mcpServer.ts index d51d80a..21f67a2 100644 --- a/src/utils/computerUse/mcpServer.ts +++ b/src/utils/computerUse/mcpServer.ts @@ -6,9 +6,6 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js' import { homedir } from 'os' -import { shutdownDatadog } from '../../services/analytics/datadog.js' -import { shutdown1PEventLogging } from '../../services/analytics/firstPartyEventLogger.js' -import { initializeAnalyticsSink } from '../../services/analytics/sink.js' import { enableConfigs } from '../config.js' import { logForDebugging } from '../debug.js' import { filterAppsForDescription } from './appNames.js' @@ -80,20 +77,18 @@ export async function createComputerUseMcpServerForCli(): Promise< /** * Subprocess entrypoint for `--computer-use-mcp`. Mirror of * `runClaudeInChromeMcpServer` — stdio transport, exit on stdin close, - * flush analytics before exit. + * and exit promptly when the parent process closes stdin. */ export async function runComputerUseMcpServer(): Promise { enableConfigs() - initializeAnalyticsSink() const server = await createComputerUseMcpServerForCli() const transport = new StdioServerTransport() let exiting = false - const shutdownAndExit = async (): Promise => { + const shutdownAndExit = (): void => { if (exiting) return exiting = true - await Promise.all([shutdown1PEventLogging(), shutdownDatadog()]) // eslint-disable-next-line custom-rules/no-process-exit process.exit(0) } diff --git a/src/utils/errorLogSink.ts b/src/utils/errorLogSink.ts index 0bbdd9b..240e076 100644 --- a/src/utils/errorLogSink.ts +++ b/src/utils/errorLogSink.ts @@ -202,8 +202,6 @@ function logMCPDebugImpl(serverName: string, message: string): void { * Call this during app startup to attach the error logging backend. * Any errors logged before this is called will be queued and drained. * - * Should be called BEFORE initializeAnalyticsSink() in the startup sequence. - * * Idempotent: safe to call multiple times (subsequent calls are no-ops). */ export function initializeErrorLogSink(): void { diff --git a/src/utils/gracefulShutdown.ts b/src/utils/gracefulShutdown.ts index 10d39e5..87e1210 100644 --- a/src/utils/gracefulShutdown.ts +++ b/src/utils/gracefulShutdown.ts @@ -29,8 +29,6 @@ import { supportsTabStatus, wrapForMultiplexer, } from '../ink/termio/osc.js' -import { shutdownDatadog } from '../services/analytics/datadog.js' -import { shutdown1PEventLogging } from '../services/analytics/firstPartyEventLogger.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, @@ -41,7 +39,6 @@ import { logForDebugging } from './debug.js' import { logForDiagnosticsNoPII } from './diagLogs.js' import { isEnvTruthy } from './envUtils.js' import { getCurrentSessionTitle, sessionIdExists } from './sessionStorage.js' -import { sleep } from './sleep.js' import { profileReport } from './startupProfiler.js' /** @@ -413,7 +410,7 @@ export async function gracefulShutdown( // Failsafe: guarantee process exits even if cleanup hangs (e.g., MCP connections). // Runs cleanupTerminalModes first so a hung cleanup doesn't leave the terminal dirty. - // Budget = max(5s, hook budget + 3.5s headroom for cleanup + analytics flush). + // Budget = max(5s, hook budget + 3.5s headroom for remaining cleanup). failsafeTimer = setTimeout( code => { cleanupTerminalModes() @@ -487,7 +484,7 @@ export async function gracefulShutdown( } // Signal to inference that this session's cache can be evicted. - // Fires before analytics flush so the event makes it to the pipeline. + // Emit before the final forced-exit path runs. const lastRequestId = getLastMainRequestId() if (lastRequestId) { logEvent('tengu_cache_eviction_hint', { @@ -498,18 +495,6 @@ export async function gracefulShutdown( }) } - // Flush analytics — capped at 500ms. Previously unbounded: the 1P exporter - // awaits all pending axios POSTs (10s each), eating the full failsafe budget. - // Lost analytics on slow networks are acceptable; a hanging exit is not. - try { - await Promise.race([ - Promise.all([shutdown1PEventLogging(), shutdownDatadog()]), - sleep(500), - ]) - } catch { - // Ignore analytics shutdown errors - } - if (options?.finalMessage) { try { // eslint-disable-next-line custom-rules/no-sync-fs -- must flush before forceExit diff --git a/src/utils/sinks.ts b/src/utils/sinks.ts index ca5b4ec..e8c50d7 100644 --- a/src/utils/sinks.ts +++ b/src/utils/sinks.ts @@ -1,15 +1,12 @@ -import { initializeAnalyticsSink } from '../services/analytics/sink.js' import { initializeErrorLogSink } from './errorLogSink.js' /** - * Attach error log and analytics compatibility sinks. Both inits are - * idempotent. Called from setup() for the default command; other entrypoints - * (subcommands, daemon, bridge) call this directly since they bypass setup(). + * Attach startup sinks used by all entrypoints. The error-log init is + * idempotent, so callers that bypass setup() can safely invoke this too. * * Leaf module — kept out of setup.ts to avoid the setup → commands → bridge * → setup import cycle. */ export function initSinks(): void { initializeErrorLogSink() - initializeAnalyticsSink() }