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)
This commit is contained in:
2026-04-09 13:58:03 +08:00
parent 86e7dbd1ab
commit 2264aea591
14 changed files with 63 additions and 103 deletions

View File

@@ -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<void> {
)
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<void> {
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.',

View File

@@ -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';

View File

@@ -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,
})
})
})

View File

@@ -864,11 +864,8 @@ async function run(): Promise<CommanderCommand> {
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');

View File

@@ -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<void> {
return
}

View File

@@ -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<void> {
return
}
export function logEventTo1P(
_eventName: string,
_metadata: Record<string, number | boolean | undefined> = {},
): void {
return
}

View File

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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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<void> {
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<void> => {
const shutdownAndExit = (): void => {
if (exiting) {
return
}
exiting = true
await shutdown1PEventLogging()
await shutdownDatadog()
// eslint-disable-next-line custom-rules/no-process-exit
process.exit(0)
}

View File

@@ -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<void> {
enableConfigs()
initializeAnalyticsSink()
const server = await createComputerUseMcpServerForCli()
const transport = new StdioServerTransport()
let exiting = false
const shutdownAndExit = async (): Promise<void> => {
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)
}

View File

@@ -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 {

View File

@@ -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

View File

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