Remove the remaining no-op tracing and telemetry-only helpers

The build no longer ships telemetry egress, so the next cleanup pass deletes the remaining tracing compatibility layer and the helper modules whose only job was to shape telemetry payloads. This removes the dead session/beta/perfetto tracing files, drops telemetry-only file-operation and plugin-fetch helpers, and rewires the affected callers to keep only their real product behavior.

Constraint: Preserve existing user-visible behavior and feature-gated product logic while removing inert tracing/reporting scaffolding
Constraint: Leave GrowthBook in place for now because it functions as the repo's local feature-flag adapter, not a live reporting path
Rejected: Delete growthbook.ts in the same pass | Its call surface is wide and now tied to local product behavior rather than telemetry export
Rejected: Leave no-op tracing and helper modules in place | They continued to create audit noise and implied behavior that no longer existed
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Remaining analytics-named code should be treated as either local compatibility calls or feature-gate infrastructure unless a concrete egress path is reintroduced
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 14:26:11 +08:00
parent 5af8acb2bb
commit 9ba783f10b
22 changed files with 11 additions and 1072 deletions

File diff suppressed because one or more lines are too long

View File

@@ -209,11 +209,6 @@ import {
stopSessionActivity,
} from '../../utils/sessionActivity.js'
import { jsonStringify } from '../../utils/slowOperations.js'
import {
isBetaTracingEnabled,
type LLMRequestNewContext,
startLLMRequestSpan,
} from '../../utils/telemetry/sessionTracing.js'
/* eslint-enable @typescript-eslint/no-require-imports */
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
@@ -1379,9 +1374,6 @@ async function* queryModel(
})
const useBetas = betas.length > 0
// Build minimal context for detailed tracing (when beta tracing is enabled)
// Note: The actual new_context message extraction is done in sessionTracing.ts using
// hash-based tracking per querySource (agent) from the messagesForAPI array
const extraToolSchemas = [...(options.extraToolSchemas ?? [])]
if (advisorModel) {
// Server tools must be in the tools array by API contract. Appended after
@@ -1485,23 +1477,6 @@ async function* queryModel(
})
}
const newContext: LLMRequestNewContext | undefined = isBetaTracingEnabled()
? {
systemPrompt: systemPrompt.join('\n\n'),
querySource: options.querySource,
tools: jsonStringify(allTools),
}
: undefined
// Capture the span so we can pass it to endLLMRequestSpan later
// This ensures responses are matched to the correct request when multiple requests run in parallel
const llmSpan = startLLMRequestSpan(
options.model,
newContext,
messagesForAPI,
isFastMode,
)
const startIncludingRetries = Date.now()
let start = Date.now()
let attemptNumber = 0
@@ -2730,7 +2705,6 @@ async function* queryModel(
didFallBackToNonStreaming,
queryTracking: options.queryTracking,
querySource: options.querySource,
llmSpan,
fastMode: isFastModeRequest,
previousRequestId,
})
@@ -2786,7 +2760,6 @@ async function* queryModel(
didFallBackToNonStreaming,
queryTracking: options.queryTracking,
querySource: options.querySource,
llmSpan,
fastMode: isFastModeRequest,
previousRequestId,
})
@@ -2874,10 +2847,7 @@ async function* queryModel(
costUSD,
queryTracking: options.queryTracking,
permissionMode: permissionContext.mode,
// Pass newMessages for beta tracing - extraction happens in logging.ts
// only when beta tracing is enabled
newMessages,
llmSpan,
globalCacheStrategy,
requestSetupMs: start - startIncludingRetries,
attemptStartTimes,

View File

@@ -22,11 +22,6 @@ import { logError } from 'src/utils/log.js'
import { getAPIProviderForStatsig } from 'src/utils/model/providers.js'
import type { PermissionMode } from 'src/utils/permissions/PermissionMode.js'
import { jsonStringify } from 'src/utils/slowOperations.js'
import {
endLLMRequestSpan,
isBetaTracingEnabled,
type Span,
} from 'src/utils/telemetry/sessionTracing.js'
import type { NonNullableUsage } from '../../entrypoints/sdk/sdkUtilityTypes.js'
import { consumeInvokingRequestId } from '../../utils/agentContext.js'
import {
@@ -246,7 +241,6 @@ export function logAPIError({
headers,
queryTracking,
querySource,
llmSpan,
fastMode,
previousRequestId,
}: {
@@ -265,8 +259,6 @@ export function logAPIError({
headers?: globalThis.Headers
queryTracking?: QueryChainTracking
querySource?: string
/** The span from startLLMRequestSpan - pass this to correctly match responses to requests */
llmSpan?: Span
fastMode?: boolean
previousRequestId?: string | null
}): void {
@@ -363,14 +355,6 @@ export function logAPIError({
...getAnthropicEnvMetadata(),
})
// Pass the span to correctly match responses to requests when beta tracing is enabled
endLLMRequestSpan(llmSpan, {
success: false,
statusCode: status ? parseInt(status) : undefined,
error: errStr,
attempt,
})
// Log first error for teleported sessions (reliability tracking)
const teleportInfo = getTeleportedSessionInfo()
if (teleportInfo?.isTeleported && !teleportInfo.hasLoggedFirstMessage) {
@@ -586,7 +570,6 @@ export function logAPISuccessAndDuration({
queryTracking,
permissionMode,
newMessages,
llmSpan,
globalCacheStrategy,
requestSetupMs,
attemptStartTimes,
@@ -611,11 +594,7 @@ export function logAPISuccessAndDuration({
costUSD: number
queryTracking?: QueryChainTracking
permissionMode?: PermissionMode
/** Assistant messages from the response - used to extract model_output and thinking_output
* when beta tracing is enabled */
newMessages?: AssistantMessage[]
/** The span from startLLMRequestSpan - pass this to correctly match responses to requests */
llmSpan?: Span
/** Strategy used for global prompt caching: 'tool_based', 'system_prompt', or 'none' */
globalCacheStrategy?: GlobalCacheStrategy
/** Time spent in pre-request setup before the successful attempt */
@@ -703,56 +682,6 @@ export function logAPISuccessAndDuration({
previousRequestId,
betas,
})
// Extract model output, thinking output, and tool call flag when beta tracing is enabled
let modelOutput: string | undefined
let thinkingOutput: string | undefined
let hasToolCall: boolean | undefined
if (isBetaTracingEnabled() && newMessages) {
// Model output - visible to all users
modelOutput =
newMessages
.flatMap(m =>
m.message.content
.filter(c => c.type === 'text')
.map(c => (c as { type: 'text'; text: string }).text),
)
.join('\n') || undefined
// Thinking output - Ant-only (build-time gated)
if (process.env.USER_TYPE === 'ant') {
thinkingOutput =
newMessages
.flatMap(m =>
m.message.content
.filter(c => c.type === 'thinking')
.map(c => (c as { type: 'thinking'; thinking: string }).thinking),
)
.join('\n') || undefined
}
// Check if any tool_use blocks were in the output
hasToolCall = newMessages.some(m =>
m.message.content.some(c => c.type === 'tool_use'),
)
}
// Pass the span to correctly match responses to requests when beta tracing is enabled
endLLMRequestSpan(llmSpan, {
success: true,
inputTokens: usage.input_tokens,
outputTokens: usage.output_tokens,
cacheReadTokens: usage.cache_read_input_tokens,
cacheCreationTokens: usage.cache_creation_input_tokens,
attempt,
modelOutput,
thinkingOutput,
hasToolCall,
ttftMs: ttftMs ?? undefined,
requestSetupMs,
attemptStartTimes,
})
// Log first successful message for teleported sessions (reliability tracking)
const teleportInfo = getTeleportedSessionInfo()
if (teleportInfo?.isTeleported && !teleportInfo.hasLoggedFirstMessage) {

View File

@@ -6,7 +6,6 @@ import { clearSpeculativeChecks } from '../../tools/BashTool/bashPermissions.js'
import { clearClassifierApprovals } from '../../utils/classifierApprovals.js'
import { resetGetMemoryFilesCache } from '../../utils/claudemd.js'
import { clearSessionMessagesCache } from '../../utils/sessionStorage.js'
import { clearBetaTracingState } from '../../utils/telemetry/betaSessionTracing.js'
import { resetMicrocompactState } from './microCompact.js'
/**
@@ -67,7 +66,6 @@ export function runPostCompactCleanup(querySource?: QuerySource): void {
// model still has SkillTool in schema, invoked_skills preserves used
// skills, and dynamic additions are handled by skillChangeDetector /
// cacheUtils resets. See compactConversation() for full rationale.
clearBetaTracingState()
if (feature('COMMIT_ATTRIBUTION')) {
void import('../../utils/attributionHooks.js').then(m =>
m.sweepFileContentCache(),

View File

@@ -11,7 +11,6 @@ import {
import {
extractMcpToolDetails,
extractSkillName,
extractToolInputForTelemetry,
getFileExtensionForAnalytics,
getFileExtensionsFromBashCommand,
isToolDetailsLoggingEnabled,
@@ -87,16 +86,6 @@ import {
} from '../../utils/sessionActivity.js'
import { jsonStringify } from '../../utils/slowOperations.js'
import { Stream } from '../../utils/stream.js'
import {
addToolContentEvent,
endToolBlockedOnUserSpan,
endToolExecutionSpan,
endToolSpan,
isBetaTracingEnabled,
startToolBlockedOnUserSpan,
startToolExecutionSpan,
startToolSpan,
} from '../../utils/telemetry/sessionTracing.js'
import {
formatError,
formatZodValidationError,
@@ -203,7 +192,7 @@ function ruleSourceToOTelSource(
* Without it, we fall back conservatively: allow → user_temporary,
* deny → user_reject.
*/
function decisionReasonToOTelSource(
function decisionReasonToSource(
reason: PermissionDecisionReason | undefined,
behavior: 'allow' | 'deny',
): string {
@@ -889,29 +878,6 @@ async function checkPermissionsAndCallTool(
}
}
const toolAttributes: Record<string, string | number | boolean> = {}
if (processedInput && typeof processedInput === 'object') {
if (tool.name === FILE_READ_TOOL_NAME && 'file_path' in processedInput) {
toolAttributes.file_path = String(processedInput.file_path)
} else if (
(tool.name === FILE_EDIT_TOOL_NAME ||
tool.name === FILE_WRITE_TOOL_NAME) &&
'file_path' in processedInput
) {
toolAttributes.file_path = String(processedInput.file_path)
} else if (tool.name === BASH_TOOL_NAME && 'command' in processedInput) {
const bashInput = processedInput as BashToolInput
toolAttributes.full_command = bashInput.command
}
}
startToolSpan(
tool.name,
toolAttributes,
isBetaTracingEnabled() ? jsonStringify(processedInput) : undefined,
)
startToolBlockedOnUserSpan()
// Check whether we have permission to use the tool,
// and ask the user for permission if we don't
const permissionMode = toolUseContext.getAppState().toolPermissionContext.mode
@@ -952,15 +918,14 @@ async function checkPermissionsAndCallTool(
) {
// Increment code-edit tool decision counter for headless mode
if (isCodeEditingTool(tool.name)) {
const source = decisionReasonToOTelSource(
permissionDecision.decisionReason,
permissionDecision.behavior,
)
void buildCodeEditToolAttributes(
tool,
processedInput,
decision,
source,
decisionReasonToSource(
permissionDecision.decisionReason,
permissionDecision.behavior,
),
).then(attributes => getCodeEditToolDecisionCounter()?.add(1, attributes))
}
}
@@ -983,10 +948,6 @@ async function checkPermissionsAndCallTool(
if (permissionDecision.behavior !== 'allow') {
logForDebugging(`${tool.name} tool permission denied`)
const decisionInfo = toolUseContext.toolDecisions?.get(toolUseID)
endToolBlockedOnUserSpan('reject', decisionInfo?.source || 'unknown')
endToolSpan()
logEvent('tengu_tool_use_can_use_tool_rejected', {
messageID:
messageId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
@@ -1120,10 +1081,6 @@ async function checkPermissionsAndCallTool(
processedInput = permissionDecision.updatedInput
}
// Prepare tool parameters for logging in tool_result event.
// Gated by OTEL_LOG_TOOL_DETAILS — tool parameters can contain sensitive
// content (bash commands, MCP server names, etc.) so they're opt-in only.
const telemetryToolInput = extractToolInputForTelemetry(processedInput)
let toolParameters: Record<string, unknown> = {}
if (isToolDetailsLoggingEnabled()) {
if (tool.name === BASH_TOOL_NAME && 'command' in processedInput) {
@@ -1157,13 +1114,6 @@ async function checkPermissionsAndCallTool(
}
}
const decisionInfo = toolUseContext.toolDecisions?.get(toolUseID)
endToolBlockedOnUserSpan(
decisionInfo?.decision || 'unknown',
decisionInfo?.source || 'unknown',
)
startToolExecutionSpan()
const startTime = Date.now()
startSessionActivity('tool_exec')
@@ -1212,51 +1162,6 @@ async function checkPermissionsAndCallTool(
const durationMs = Date.now() - startTime
addToToolDuration(durationMs)
// Log tool content/output as span event if enabled
if (result.data && typeof result.data === 'object') {
const contentAttributes: Record<string, string | number | boolean> = {}
// Read tool: capture file_path and content
if (tool.name === FILE_READ_TOOL_NAME && 'content' in result.data) {
if ('file_path' in processedInput) {
contentAttributes.file_path = String(processedInput.file_path)
}
contentAttributes.content = String(result.data.content)
}
// Edit/Write tools: capture file_path and diff
if (
(tool.name === FILE_EDIT_TOOL_NAME ||
tool.name === FILE_WRITE_TOOL_NAME) &&
'file_path' in processedInput
) {
contentAttributes.file_path = String(processedInput.file_path)
// For Edit, capture the actual changes made
if (tool.name === FILE_EDIT_TOOL_NAME && 'diff' in result.data) {
contentAttributes.diff = String(result.data.diff)
}
// For Write, capture the written content
if (tool.name === FILE_WRITE_TOOL_NAME && 'content' in processedInput) {
contentAttributes.content = String(processedInput.content)
}
}
// Bash tool: capture command
if (tool.name === BASH_TOOL_NAME && 'command' in processedInput) {
const bashInput = processedInput as BashToolInput
contentAttributes.bash_command = bashInput.command
// Also capture output if available
if ('output' in result.data) {
contentAttributes.output = String(result.data.output)
}
}
if (Object.keys(contentAttributes).length > 0) {
addToolContentEvent('tool.output', contentAttributes)
}
}
// Capture structured output from tool result if present
if (typeof result === 'object' && 'structured_output' in result) {
// Store the structured output in an attachment message
@@ -1268,14 +1173,6 @@ async function checkPermissionsAndCallTool(
})
}
endToolExecutionSpan({ success: true })
// Pass tool result for new_context logging
const toolResultStr =
result.data && typeof result.data === 'object'
? jsonStringify(result.data)
: String(result.data ?? '')
endToolSpan(toolResultStr)
// Map the tool result to API format once and cache it. This block is reused
// by addToolResult (skipping the remap) and measured here for analytics.
const mappedToolResultBlock = tool.mapToolResultToToolResultBlockParam(
@@ -1562,12 +1459,6 @@ async function checkPermissionsAndCallTool(
const durationMs = Date.now() - startTime
addToToolDuration(durationMs)
endToolExecutionSpan({
success: false,
error: errorMessage(error),
})
endToolSpan()
// Handle MCP auth errors by updating the client status to 'needs-auth'
// This updates the /mcp display to show the server needs re-authorization
if (error instanceof McpAuthError) {

View File

@@ -72,11 +72,6 @@ import {
asSystemPrompt,
type SystemPrompt,
} from '../../utils/systemPromptType.js'
import {
isPerfettoTracingEnabled,
registerAgent as registerPerfettoAgent,
unregisterAgent as unregisterPerfettoAgent,
} from '../../utils/telemetry/perfettoTracing.js'
import type { ContentReplacementState } from '../../utils/toolResultStorage.js'
import { createAgentId } from '../../utils/uuid.js'
import { resolveAgentTools } from './agentToolUtils.js'
@@ -352,12 +347,6 @@ export async function* runAgent({
setAgentTranscriptSubdir(agentId, transcriptSubdir)
}
// Register agent in Perfetto trace for hierarchy visualization
if (isPerfettoTracingEnabled()) {
const parentId = toolUseContext.agentId ?? getSessionId()
registerPerfettoAgent(agentId, agentDefinition.agentType, parentId)
}
// Log API calls path for subagents (ant-only)
if (process.env.USER_TYPE === 'ant') {
logForDebugging(
@@ -828,8 +817,6 @@ export async function* runAgent({
agentToolUseContext.readFileState.clear()
// Release the cloned fork context messages
initialMessages.length = 0
// Release perfetto agent registry entry
unregisterPerfettoAgent(agentId)
// Release transcript subdir mapping
clearAgentTranscriptSubdir(agentId)
// Release this agent's todos entry. Without this, every subagent that

View File

@@ -29,7 +29,6 @@ import {
fileHistoryEnabled,
fileHistoryTrackEdit,
} from '../../utils/fileHistory.js'
import { logFileOperation } from '../../utils/fileOperationAnalytics.js'
import {
type LineEndingType,
readFileSyncWithMetadata,
@@ -530,12 +529,6 @@ export const FileEditTool = buildTool({
}
countLinesChanged(patch)
logFileOperation({
operation: 'edit',
tool: 'FileEditTool',
filePath: absoluteFilePath,
})
logEvent('tengu_edit_string_lengths', {
oldStringBytes: Buffer.byteLength(old_string, 'utf8'),
newStringBytes: Buffer.byteLength(new_string, 'utf8'),

View File

@@ -37,7 +37,6 @@ import {
getFileModificationTimeAsync,
suggestPathUnderCwd,
} from '../../utils/file.js'
import { logFileOperation } from '../../utils/fileOperationAnalytics.js'
import { formatFileSize } from '../../utils/format.js'
import { getFsImplementation } from '../../utils/fsOperations.js'
import {
@@ -852,13 +851,6 @@ async function callInner(
file: { filePath: file_path, cells },
}
logFileOperation({
operation: 'read',
tool: 'FileReadTool',
filePath: fullFilePath,
content: cellsJson,
})
return { data }
}
@@ -869,13 +861,6 @@ async function callInner(
const data = await readImageWithTokenBudget(resolvedFilePath, maxTokens)
context.nestedMemoryAttachmentTriggers?.add(fullFilePath)
logFileOperation({
operation: 'read',
tool: 'FileReadTool',
filePath: fullFilePath,
content: data.file.base64,
})
const metadataText = data.file.dimensions
? createImageMetadataText(data.file.dimensions)
: null
@@ -907,12 +892,6 @@ async function callInner(
fileSize: extractResult.data.file.originalSize,
hasPageRange: true,
})
logFileOperation({
operation: 'read',
tool: 'FileReadTool',
filePath: fullFilePath,
content: `PDF pages ${pages}`,
})
const entries = await readdir(extractResult.data.file.outputDir)
const imageFiles = entries.filter(f => f.endsWith('.jpg')).sort()
const imageBlocks = await Promise.all(
@@ -989,13 +968,6 @@ async function callInner(
throw new Error(readResult.error.message)
}
const pdfData = readResult.data
logFileOperation({
operation: 'read',
tool: 'FileReadTool',
filePath: fullFilePath,
content: pdfData.file.base64,
})
return {
data: pdfData,
newMessages: [
@@ -1057,13 +1029,6 @@ async function callInner(
memoryFileMtimes.set(data, mtimeMs)
}
logFileOperation({
operation: 'read',
tool: 'FileReadTool',
filePath: fullFilePath,
content,
})
const sessionFileType = detectSessionFileType(fullFilePath)
const analyticsExt = getFileExtensionForAnalytics(fullFilePath)
logEvent('tengu_session_file_read', {

View File

@@ -24,7 +24,6 @@ import {
fileHistoryEnabled,
fileHistoryTrackEdit,
} from '../../utils/fileHistory.js'
import { logFileOperation } from '../../utils/fileOperationAnalytics.js'
import { readFileSyncWithMetadata } from '../../utils/fileRead.js'
import { getFsImplementation } from '../../utils/fsOperations.js'
import {
@@ -380,13 +379,6 @@ export const FileWriteTool = buildTool({
// Track lines added and removed for file updates, right before yielding result
countLinesChanged(patch)
logFileOperation({
operation: 'write',
tool: 'FileWriteTool',
filePath: fullFilePath,
type: 'update',
})
return {
data,
}
@@ -404,13 +396,6 @@ export const FileWriteTool = buildTool({
// For creation of new files, count all lines as additions, right before yielding the result
countLinesChanged([], content)
logFileOperation({
operation: 'write',
tool: 'FileWriteTool',
filePath: fullFilePath,
type: 'create',
})
return {
data,
}

View File

@@ -1,71 +0,0 @@
import { createHash } from 'crypto'
import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from 'src/services/analytics/index.js'
import { logEvent } from 'src/services/analytics/index.js'
/**
* Creates a truncated SHA256 hash (16 chars) for file paths
* Used for privacy-preserving analytics on file operations
*/
function hashFilePath(
filePath: string,
): AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS {
return createHash('sha256')
.update(filePath)
.digest('hex')
.slice(0, 16) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
}
/**
* Creates a full SHA256 hash (64 chars) for file contents
* Used for deduplication and change detection analytics
*/
function hashFileContent(
content: string,
): AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS {
return createHash('sha256')
.update(content)
.digest('hex') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
}
// Maximum content size to hash (100KB)
// Prevents memory exhaustion when hashing large files (e.g., base64-encoded images)
const MAX_CONTENT_HASH_SIZE = 100 * 1024
/**
* Logs file operation analytics to Statsig
*/
export function logFileOperation(params: {
operation: 'read' | 'write' | 'edit'
tool: 'FileReadTool' | 'FileWriteTool' | 'FileEditTool'
filePath: string
content?: string
type?: 'create' | 'update'
}): void {
const metadata: Record<
string,
| AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
| number
| boolean
> = {
operation:
params.operation as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
tool: params.tool as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
filePathHash: hashFilePath(params.filePath),
}
// Only hash content if it's provided and below size limit
// This prevents memory exhaustion from hashing large files (e.g., base64-encoded images)
if (
params.content !== undefined &&
params.content.length <= MAX_CONTENT_HASH_SIZE
) {
metadata.contentHash = hashFileContent(params.content)
}
if (params.type !== undefined) {
metadata.type =
params.type as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
}
logEvent('tengu_file_operation', metadata)
}

View File

@@ -56,11 +56,6 @@ import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
} from 'src/services/analytics/index.js'
import { ALLOWED_OFFICIAL_MARKETPLACE_NAMES } from './plugins/schemas.js'
import {
startHookSpan,
endHookSpan,
isBetaTracingEnabled,
} from './telemetry/sessionTracing.js'
import {
hookJSONOutputSchema,
promptRequestSchema,
@@ -2065,19 +2060,6 @@ async function* executeHooks({
return
}
// Collect hook definitions for beta tracing telemetry
const hookDefinitionsJson = isBetaTracingEnabled()
? jsonStringify(getHookDefinitionsForTelemetry(matchingHooks))
: '[]'
// Start hook span for beta tracing
const hookSpan = startHookSpan(
hookEvent,
hookName,
matchingHooks.length,
hookDefinitionsJson,
)
// Yield progress messages for each hook before execution
for (const { hook } of matchingHooks) {
yield {
@@ -2930,13 +2912,6 @@ async function* executeHooks({
totalDurationMs,
})
// End hook span for beta tracing
endHookSpan(hookSpan, {
numSuccess: outcomes.success,
numBlocking: outcomes.blocking,
numNonBlockingError: outcomes.non_blocking_error,
numCancelled: outcomes.cancelled,
})
}
export type HookOutsideReplResult = {
@@ -4969,22 +4944,3 @@ export async function executeWorktreeRemoveHook(
return true
}
function getHookDefinitionsForTelemetry(
matchedHooks: MatchedHook[],
): Array<{ type: string; command?: string; prompt?: string; name?: string }> {
return matchedHooks.map(({ hook }) => {
if (hook.type === 'command') {
return { type: 'command', command: hook.command }
} else if (hook.type === 'prompt') {
return { type: 'prompt', prompt: hook.prompt }
} else if (hook.type === 'http') {
return { type: 'http', command: hook.url }
} else if (hook.type === 'function') {
return { type: 'function', name: 'function' }
} else if (hook.type === 'callback') {
return { type: 'callback', name: 'callback' }
}
return { type: 'unknown' }
})
}

View File

@@ -1,135 +0,0 @@
/**
* Telemetry for plugin/marketplace fetches that hit the network.
*
* Added for inc-5046 (GitHub complained about claude-plugins-official load).
* Before this, fetch operations only had logForDebugging — no way to measure
* actual network volume. This surfaces what's hitting GitHub vs GCS vs
* user-hosted so we can see the GCS migration take effect and catch future
* hot-path regressions before GitHub emails us again.
*
* Volume: these fire at startup (install-counts 24h-TTL)
* and on explicit user action (install/update). NOT per-interaction. Similar
* envelope to tengu_binary_download_*.
*/
import {
logEvent,
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS as SafeString,
} from '../../services/analytics/index.js'
import { OFFICIAL_MARKETPLACE_NAME } from './officialMarketplace.js'
export type PluginFetchSource =
| 'install_counts'
| 'marketplace_clone'
| 'marketplace_pull'
| 'marketplace_url'
| 'plugin_clone'
| 'mcpb'
export type PluginFetchOutcome = 'success' | 'failure' | 'cache_hit'
// Allowlist of public hosts we report by name. Anything else (enterprise
// git, self-hosted, internal) is bucketed as 'other' — we don't want
// internal hostnames (git.mycorp.internal) landing in telemetry. Bounded
// cardinality also keeps the dashboard host-breakdown tractable.
const KNOWN_PUBLIC_HOSTS = new Set([
'github.com',
'raw.githubusercontent.com',
'objects.githubusercontent.com',
'gist.githubusercontent.com',
'gitlab.com',
'bitbucket.org',
'codeberg.org',
'dev.azure.com',
'ssh.dev.azure.com',
'storage.googleapis.com', // GCS — where Dickson's migration points
])
/**
* Extract hostname from a URL or git spec and bucket to the allowlist.
* Handles `https://host/...`, `git@host:path`, `ssh://host/...`.
* Returns a known public host, 'other' (parseable but not allowlisted —
* don't leak private hostnames), or 'unknown' (unparseable / local path).
*/
function extractHost(urlOrSpec: string): string {
let host: string
const scpMatch = /^[^@/]+@([^:/]+):/.exec(urlOrSpec)
if (scpMatch) {
host = scpMatch[1]!
} else {
try {
host = new URL(urlOrSpec).hostname
} catch {
return 'unknown'
}
}
const normalized = host.toLowerCase()
return KNOWN_PUBLIC_HOSTS.has(normalized) ? normalized : 'other'
}
/**
* True if the URL/spec points at anthropics/claude-plugins-official — the
* repo GitHub complained about. Lets the dashboard separate "our problem"
* traffic from user-configured marketplaces.
*/
function isOfficialRepo(urlOrSpec: string): boolean {
return urlOrSpec.includes(`anthropics/${OFFICIAL_MARKETPLACE_NAME}`)
}
export function logPluginFetch(
source: PluginFetchSource,
urlOrSpec: string | undefined,
outcome: PluginFetchOutcome,
durationMs: number,
errorKind?: string,
): void {
// String values are bounded enums / hostname-only — no code, no paths,
// no raw error messages. Same privacy envelope as tengu_web_fetch_host.
logEvent('tengu_plugin_remote_fetch', {
source: source as SafeString,
host: (urlOrSpec ? extractHost(urlOrSpec) : 'unknown') as SafeString,
is_official: urlOrSpec ? isOfficialRepo(urlOrSpec) : false,
outcome: outcome as SafeString,
duration_ms: Math.round(durationMs),
...(errorKind && { error_kind: errorKind as SafeString }),
})
}
/**
* Classify an error into a stable bucket for the error_kind field. Keeps
* cardinality bounded — raw error messages would explode dashboard grouping.
*
* Handles both axios Error objects (Node.js error codes like ENOTFOUND) and
* git stderr strings (human phrases like "Could not resolve host"). DNS
* checked BEFORE timeout because gitClone's error enhancement at
* marketplaceManager.ts:~950 rewrites DNS failures to include the word
* "timeout" — ordering the other way would misclassify git DNS as timeout.
*/
export function classifyFetchError(error: unknown): string {
const msg = String((error as { message?: unknown })?.message ?? error)
if (
/ENOTFOUND|ECONNREFUSED|EAI_AGAIN|Could not resolve host|Connection refused/i.test(
msg,
)
) {
return 'dns_or_refused'
}
if (/ETIMEDOUT|timed out|timeout/i.test(msg)) return 'timeout'
if (
/ECONNRESET|socket hang up|Connection reset by peer|remote end hung up/i.test(
msg,
)
) {
return 'conn_reset'
}
if (/403|401|authentication|permission denied/i.test(msg)) return 'auth'
if (/404|not found|repository not found/i.test(msg)) return 'not_found'
if (/certificate|SSL|TLS|unable to get local issuer/i.test(msg)) return 'tls'
// Schema validation throws "Invalid response format" (install_counts) —
// distinguish from true unknowns so the dashboard can
// see "server sent garbage" separately.
if (/Invalid response format|Invalid marketplace schema/i.test(msg)) {
return 'invalid_schema'
}
return 'other'
}

View File

@@ -17,7 +17,6 @@ import { errorMessage, getErrnoCode } from '../errors.js'
import { getFsImplementation } from '../fsOperations.js'
import { logError } from '../log.js'
import { jsonParse, jsonStringify } from '../slowOperations.js'
import { classifyFetchError, logPluginFetch } from './fetchTelemetry.js'
import { getPluginsDirectory } from './pluginDirectories.js'
const INSTALL_COUNTS_CACHE_VERSION = 1
@@ -196,21 +195,8 @@ async function fetchInstallCountsFromGitHub(): Promise<
throw new Error('Invalid response format from install counts API')
}
logPluginFetch(
'install_counts',
INSTALL_COUNTS_URL,
'success',
performance.now() - started,
)
return response.data.plugins
} catch (error) {
logPluginFetch(
'install_counts',
INSTALL_COUNTS_URL,
'failure',
performance.now() - started,
classifyFetchError(error),
)
throw error
}
}
@@ -227,7 +213,6 @@ export async function getInstallCounts(): Promise<Map<string, number> | null> {
const cache = await loadInstallCountsCache()
if (cache) {
logForDebugging('Using cached install counts')
logPluginFetch('install_counts', INSTALL_COUNTS_URL, 'cache_hit', 0)
const map = new Map<string, number>()
for (const entry of cache.counts) {
map.set(entry.plugin, entry.unique_installs)

View File

@@ -53,7 +53,6 @@ import {
getAddDirExtraMarketplaces,
} from './addDirPluginSettings.js'
import { markPluginVersionOrphaned } from './cacheUtils.js'
import { classifyFetchError, logPluginFetch } from './fetchTelemetry.js'
import { removeAllPluginsForMarketplace } from './installedPluginsManager.js'
import {
extractHostFromSource,
@@ -1110,13 +1109,7 @@ async function cacheMarketplaceFromGit(
disableCredentialHelper: options?.disableCredentialHelper,
sparsePaths,
})
logPluginFetch(
'marketplace_pull',
gitUrl,
pullResult.code === 0 ? 'success' : 'failure',
performance.now() - pullStarted,
pullResult.code === 0 ? undefined : classifyFetchError(pullResult.stderr),
)
void pullStarted
if (pullResult.code === 0) return
logForDebugging(`git pull failed, will re-clone: ${pullResult.stderr}`, {
level: 'warn',
@@ -1156,13 +1149,7 @@ async function cacheMarketplaceFromGit(
)
const cloneStarted = performance.now()
const result = await gitClone(gitUrl, cachePath, ref, sparsePaths)
logPluginFetch(
'marketplace_clone',
gitUrl,
result.code === 0 ? 'success' : 'failure',
performance.now() - cloneStarted,
result.code === 0 ? undefined : classifyFetchError(result.stderr),
)
void cloneStarted
if (result.code !== 0) {
// Clean up any partial directory created by the failed clone so the next
// attempt starts fresh. Best-effort: if this fails, the stale dir will be
@@ -1284,13 +1271,6 @@ async function cacheMarketplaceFromUrl(
headers,
})
} catch (error) {
logPluginFetch(
'marketplace_url',
url,
'failure',
performance.now() - fetchStarted,
classifyFetchError(error),
)
if (axios.isAxiosError(error)) {
if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
throw new Error(
@@ -1317,25 +1297,13 @@ async function cacheMarketplaceFromUrl(
// Validate the response is a valid marketplace
const result = PluginMarketplaceSchema().safeParse(response.data)
if (!result.success) {
logPluginFetch(
'marketplace_url',
url,
'failure',
performance.now() - fetchStarted,
'invalid_schema',
)
throw new ConfigParseError(
`Invalid marketplace schema from URL: ${result.error.issues.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`,
redactedUrl,
response.data,
)
}
logPluginFetch(
'marketplace_url',
url,
'success',
performance.now() - fetchStarted,
)
void fetchStarted
safeCallProgress(onProgress, 'Saving marketplace to cache')
// Ensure cache directory exists

View File

@@ -20,7 +20,6 @@ import {
} from '../settings/settings.js'
import { jsonParse, jsonStringify } from '../slowOperations.js'
import { getSystemDirectories } from '../systemDirectories.js'
import { classifyFetchError, logPluginFetch } from './fetchTelemetry.js'
/**
* User configuration values for MCPB
*/
@@ -490,7 +489,6 @@ async function downloadMcpb(
}
const started = performance.now()
let fetchTelemetryFired = false
try {
const response = await axios.get(url, {
timeout: 120000, // 2 minute timeout
@@ -507,11 +505,6 @@ async function downloadMcpb(
})
const data = new Uint8Array(response.data)
// Fire telemetry before writeFile — the event measures the network
// fetch, not disk I/O. A writeFile EACCES would otherwise match
// classifyFetchError's /permission denied/ → misreport as auth.
logPluginFetch('mcpb', url, 'success', performance.now() - started)
fetchTelemetryFired = true
// Save to disk (binary data)
await writeFile(destPath, Buffer.from(data))
@@ -523,15 +516,7 @@ async function downloadMcpb(
return data
} catch (error) {
if (!fetchTelemetryFired) {
logPluginFetch(
'mcpb',
url,
'failure',
performance.now() - started,
classifyFetchError(error),
)
}
void started
const errorMsg = errorMessage(error)
const fullError = new Error(
`Failed to download MCPB file from ${url}: ${errorMsg}`,

View File

@@ -85,7 +85,6 @@ import { SettingsSchema } from '../settings/types.js'
import { jsonParse, jsonStringify } from '../slowOperations.js'
import { getAddDirEnabledPlugins } from './addDirPluginSettings.js'
import { verifyAndDemote } from './dependencyResolver.js'
import { classifyFetchError, logPluginFetch } from './fetchTelemetry.js'
import { checkGitAvailable } from './gitAvailability.js'
import { getInMemoryInstalledPlugins } from './installedPluginsManager.js'
import { getManagedPluginNames } from './managedPlugins.js'
@@ -563,13 +562,6 @@ export async function gitClone(
const cloneResult = await execFileNoThrow(gitExe(), args)
if (cloneResult.code !== 0) {
logPluginFetch(
'plugin_clone',
gitUrl,
'failure',
performance.now() - cloneStarted,
classifyFetchError(cloneResult.stderr),
)
throw new Error(`Failed to clone repository: ${cloneResult.stderr}`)
}
@@ -595,13 +587,6 @@ export async function gitClone(
)
if (unshallowResult.code !== 0) {
logPluginFetch(
'plugin_clone',
gitUrl,
'failure',
performance.now() - cloneStarted,
classifyFetchError(unshallowResult.stderr),
)
throw new Error(
`Failed to fetch commit ${sha}: ${unshallowResult.stderr}`,
)
@@ -616,27 +601,12 @@ export async function gitClone(
)
if (checkoutResult.code !== 0) {
logPluginFetch(
'plugin_clone',
gitUrl,
'failure',
performance.now() - cloneStarted,
classifyFetchError(checkoutResult.stderr),
)
throw new Error(
`Failed to checkout commit ${sha}: ${checkoutResult.stderr}`,
)
}
}
// Fire success only after ALL network ops (clone + optional SHA fetch)
// complete — same telemetry-scope discipline as mcpb and marketplace_url.
logPluginFetch(
'plugin_clone',
gitUrl,
'success',
performance.now() - cloneStarted,
)
void cloneStarted
}
/**

View File

@@ -9,7 +9,6 @@ import type {
import { logEvent } from '../../services/analytics/index.js'
import type { PermissionMode } from '../../types/permissions.js'
import { createUserMessage } from '../messages.js'
import { startInteractionSpan } from '../telemetry/sessionTracing.js'
import {
matchesKeepGoingKeyword,
matchesNegativeKeyword,
@@ -34,7 +33,6 @@ export function processTextPrompt(
typeof input === 'string'
? input
: input.find(block => block.type === 'text')?.text || ''
startInteractionSpan(userPromptText)
const isNegative = matchesNegativeKeyword(userPromptText)
const isKeepGoing = matchesKeepGoingKeyword(userPromptText)

View File

@@ -96,7 +96,6 @@ import {
readMailbox,
writeToMailbox,
} from '../teammateMailbox.js'
import { unregisterAgent as unregisterPerfettoAgent } from '../telemetry/perfettoTracing.js'
import { createContentReplacementState } from '../toolResultStorage.js'
import { TEAM_LEAD_NAME } from './constants.js'
import {
@@ -1460,7 +1459,6 @@ export async function runInProcessTeammate(
})
}
unregisterPerfettoAgent(identity.agentId)
return { success: true, messages: allMessages }
} catch (error) {
const errorMessage =
@@ -1524,7 +1522,6 @@ export async function runInProcessTeammate(
},
)
unregisterPerfettoAgent(identity.agentId)
return {
success: false,
error: errorMessage,

View File

@@ -35,11 +35,6 @@ import {
STOPPED_DISPLAY_MS,
} from '../task/framework.js'
import { createTeammateContext } from '../teammateContext.js'
import {
isPerfettoTracingEnabled,
registerAgent as registerPerfettoAgent,
unregisterAgent as unregisterPerfettoAgent,
} from '../telemetry/perfettoTracing.js'
import { removeMemberByAgentId } from './teamHelpers.js'
type SetAppStateFn = (updater: (prev: AppState) => AppState) => void
@@ -146,11 +141,6 @@ export async function spawnInProcessTeammate(
abortController,
})
// Register agent in Perfetto trace for hierarchy visualization
if (isPerfettoTracingEnabled()) {
registerPerfettoAgent(agentId, name, parentSessionId)
}
// Create task state
const description = `${name}: ${prompt.substring(0, 50)}${prompt.length > 50 ? '...' : ''}`
@@ -319,10 +309,5 @@ export function killInProcessTeammate(
)
}
// Release perfetto agent registry entry
if (agentId) {
unregisterPerfettoAgent(agentId)
}
return killed
}

View File

@@ -1,86 +0,0 @@
/**
* Detailed beta tracing egress is disabled in this build.
*
* The exported helpers remain for compile-time compatibility, but do not
* retain tracing state or emit tracing attributes.
*/
type AttributeValue = string | number | boolean
export interface SpanAttributeWriter {
setAttribute?(_key: string, _value: AttributeValue): void
setAttributes?(_attributes: Record<string, AttributeValue>): void
}
export interface LLMRequestNewContext {
systemPrompt?: string
querySource?: string
tools?: string
}
const MAX_CONTENT_SIZE = 60 * 1024
export function clearBetaTracingState(): void {
return
}
export function isBetaTracingEnabled(): boolean {
return false
}
export function truncateContent(
content: string,
maxSize: number = MAX_CONTENT_SIZE,
): { content: string; truncated: boolean } {
if (content.length <= maxSize) {
return { content, truncated: false }
}
return {
content:
content.slice(0, maxSize) +
'\n\n[TRUNCATED - Content exceeds 60KB limit]',
truncated: true,
}
}
export function addBetaInteractionAttributes(
_span: SpanAttributeWriter,
_userPrompt: string,
): void {
return
}
export function addBetaLLMRequestAttributes(
_span: SpanAttributeWriter,
_newContext?: LLMRequestNewContext,
_messagesForAPI?: unknown[],
): void {
return
}
export function addBetaLLMResponseAttributes(
_attributes: Record<string, AttributeValue>,
_metadata?: {
modelOutput?: string
thinkingOutput?: string
},
): void {
return
}
export function addBetaToolInputAttributes(
_span: SpanAttributeWriter,
_toolName: string,
_toolInput: string,
): void {
return
}
export function addBetaToolResultAttributes(
_attributes: Record<string, AttributeValue>,
_toolName: string | number | boolean,
_toolResult: string,
): void {
return
}

View File

@@ -1,157 +0,0 @@
/**
* Perfetto tracing is disabled in this build.
*
* The original implementation wrote detailed local trace files containing
* request, tool, and interaction metadata. This compatibility layer keeps the
* API surface intact while ensuring no trace files are created.
*/
export type TraceEventPhase =
| 'B'
| 'E'
| 'X'
| 'i'
| 'C'
| 'b'
| 'n'
| 'e'
| 'M'
export type TraceEvent = {
name: string
cat: string
ph: TraceEventPhase
ts: number
pid: number
tid: number
dur?: number
args?: Record<string, unknown>
id?: string
scope?: string
}
export function initializePerfettoTracing(): void {
return
}
export function isPerfettoTracingEnabled(): boolean {
return false
}
export function registerAgent(
_agentId: string,
_agentName: string,
_parentAgentId?: string,
): void {
return
}
export function unregisterAgent(_agentId: string): void {
return
}
export function startLLMRequestPerfettoSpan(_args: {
model: string
promptTokens?: number
messageId?: string
isSpeculative?: boolean
querySource?: string
}): string {
return ''
}
export function endLLMRequestPerfettoSpan(
_spanId: string,
_metadata: {
ttftMs?: number
ttltMs?: number
promptTokens?: number
outputTokens?: number
cacheReadTokens?: number
cacheCreationTokens?: number
messageId?: string
success?: boolean
error?: string
requestSetupMs?: number
attemptStartTimes?: number[]
},
): void {
return
}
export function startToolPerfettoSpan(
_toolName: string,
_args?: Record<string, unknown>,
): string {
return ''
}
export function endToolPerfettoSpan(
_spanId: string,
_metadata?: {
success?: boolean
error?: string
resultTokens?: number
},
): void {
return
}
export function startUserInputPerfettoSpan(_context?: string): string {
return ''
}
export function endUserInputPerfettoSpan(
_spanId: string,
_metadata?: {
decision?: string
source?: string
},
): void {
return
}
export function emitPerfettoInstant(
_name: string,
_category: string,
_args?: Record<string, unknown>,
): void {
return
}
export function emitPerfettoCounter(
_name: string,
_values: Record<string, number>,
): void {
return
}
export function startInteractionPerfettoSpan(_userPrompt?: string): string {
return ''
}
export function endInteractionPerfettoSpan(_spanId: string): void {
return
}
export function getPerfettoEvents(): TraceEvent[] {
return []
}
export function resetPerfettoTracer(): void {
return
}
export async function triggerPeriodicWriteForTesting(): Promise<void> {
return
}
export function evictStaleSpansForTesting(): void {
return
}
export const MAX_EVENTS_FOR_TESTING = 0
export function evictOldestEventsForTesting(): void {
return
}

View File

@@ -1,172 +0,0 @@
/**
* OpenTelemetry session tracing is disabled in this build.
*
* This module preserves the tracing API surface for callers, but all exported
* operations are local no-ops and never collect or forward tracing data.
*/
export { isBetaTracingEnabled, type LLMRequestNewContext } from './betaSessionTracing.js'
export interface Span {
end(): void
setAttribute(
_key: string,
_value: string | number | boolean,
): void
setAttributes(
_attributes: Record<string, string | number | boolean>,
): void
addEvent(
_eventName: string,
_attributes?: Record<string, string | number | boolean>,
): void
recordException(_error: Error): void
}
class NoopSpan implements Span {
end(): void {}
setAttribute(
_key: string,
_value: string | number | boolean,
): void {}
setAttributes(
_attributes: Record<string, string | number | boolean>,
): void {}
addEvent(
_eventName: string,
_attributes?: Record<string, string | number | boolean>,
): void {}
recordException(_error: Error): void {}
}
const NOOP_SPAN: Span = new NoopSpan()
type LLMRequestMetadata = {
inputTokens?: number
outputTokens?: number
cacheReadTokens?: number
cacheCreationTokens?: number
success?: boolean
statusCode?: number
error?: string
attempt?: number
modelResponse?: string
modelOutput?: string
thinkingOutput?: string
hasToolCall?: boolean
ttftMs?: number
requestSetupMs?: number
attemptStartTimes?: number[]
}
type HookSpanMetadata = {
numSuccess?: number
numBlocking?: number
numNonBlockingError?: number
numCancelled?: number
}
export function isEnhancedTelemetryEnabled(): boolean {
return false
}
export function startInteractionSpan(_userPrompt: string): Span {
return NOOP_SPAN
}
export function endInteractionSpan(): void {
return
}
export function startLLMRequestSpan(
_model: string,
_newContext?: import('./betaSessionTracing.js').LLMRequestNewContext,
_messagesForAPI?: unknown[],
_fastMode?: boolean,
): Span {
return NOOP_SPAN
}
export function endLLMRequestSpan(
_span?: Span,
_metadata?: LLMRequestMetadata,
): void {
return
}
export function startToolSpan(
_toolName: string,
_toolAttributes?: Record<string, string | number | boolean>,
_toolInput?: string,
): Span {
return NOOP_SPAN
}
export function startToolBlockedOnUserSpan(): Span {
return NOOP_SPAN
}
export function endToolBlockedOnUserSpan(
_decision?: string,
_source?: string,
): void {
return
}
export function startToolExecutionSpan(): Span {
return NOOP_SPAN
}
export function endToolExecutionSpan(metadata?: {
success?: boolean
error?: string
}): void {
void metadata
return
}
export function endToolSpan(
_toolResult?: string,
_resultTokens?: number,
): void {
return
}
export function addToolContentEvent(
_eventName: string,
_attributes: Record<string, string | number | boolean>,
): void {
return
}
export function getCurrentSpan(): Span | null {
return null
}
export async function executeInSpan<T>(
_spanName: string,
fn: (span: Span) => Promise<T>,
_attributes?: Record<string, string | number | boolean>,
): Promise<T> {
return fn(NOOP_SPAN)
}
export function startHookSpan(
_hookEvent: string,
_hookName: string,
_numHooks: number,
_hookDefinitions: string,
): Span {
return NOOP_SPAN
}
export function endHookSpan(
_span: Span,
_metadata?: HookSpanMetadata,
): void {
return
}