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:
@@ -44,7 +44,6 @@ import { WorkerPendingPermission } from '../components/permissions/WorkerPending
|
||||
import { injectUserMessageToTeammate, getAllInProcessTeammateTasks } from '../tasks/InProcessTeammateTask/InProcessTeammateTask.js';
|
||||
import { isLocalAgentTask, queuePendingMessage, appendMessageToLocalAgent, type LocalAgentTaskState } from '../tasks/LocalAgentTask/LocalAgentTask.js';
|
||||
import { registerLeaderToolUseConfirmQueue, unregisterLeaderToolUseConfirmQueue, registerLeaderSetToolPermissionContext, unregisterLeaderSetToolPermissionContext } from '../utils/swarm/leaderPermissionBridge.js';
|
||||
import { endInteractionSpan } from '../utils/telemetry/sessionTracing.js';
|
||||
import { useLogMessages } from '../hooks/useLogMessages.js';
|
||||
import { useReplBridge } from '../hooks/useReplBridge.js';
|
||||
import { type Command, type CommandResultDisplay, type ResumeEntrypoint, getCommandName, isCommandEnabled } from '../commands.js';
|
||||
@@ -1579,7 +1578,6 @@ export function REPL({
|
||||
setSpinnerColor(null);
|
||||
setSpinnerShimmerColor(null);
|
||||
pickNewSpinnerTip();
|
||||
endInteractionSpan();
|
||||
// Speculative bash classifier checks are only valid for the current
|
||||
// turn's commands — clear after each turn to avoid accumulating
|
||||
// Promise chains for unconsumed checks (denied/aborted paths).
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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' }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user