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:
@@ -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.',
|
||||
|
||||
@@ -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';
|
||||
|
||||
14
src/components/FeedbackSurvey/submitTranscriptShare.test.ts
Normal file
14
src/components/FeedbackSurvey/submitTranscriptShare.test.ts
Normal 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,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
32
src/services/analytics/index.test.ts
Normal file
32
src/services/analytics/index.test.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user