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() }