feat: add comprehensive error telemetry to code-index service (#5595)

This commit is contained in:
Daniel 2025-07-11 22:01:50 -05:00 committed by GitHub
parent 39b83070c3
commit 32308d79c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 536 additions and 51 deletions

View file

@ -65,6 +65,7 @@ export enum TelemetryEventName {
DIFF_APPLICATION_ERROR = "Diff Application Error",
SHELL_INTEGRATION_ERROR = "Shell Integration Error",
CONSECUTIVE_MISTAKE_ERROR = "Consecutive Mistake Error",
CODE_INDEX_ERROR = "Code Index Error",
}
/**
@ -152,6 +153,7 @@ export const rooCodeTelemetryEventSchema = z.discriminatedUnion("type", [
TelemetryEventName.DIFF_APPLICATION_ERROR,
TelemetryEventName.SHELL_INTEGRATION_ERROR,
TelemetryEventName.CONSECUTIVE_MISTAKE_ERROR,
TelemetryEventName.CODE_INDEX_ERROR,
TelemetryEventName.CONTEXT_CONDENSED,
TelemetryEventName.SLIDING_WINDOW_TRUNCATION,
TelemetryEventName.TAB_SHOWN,

View file

@ -29,6 +29,15 @@ vitest.mock("vscode", () => ({
// Mock debounce to execute immediately
vitest.mock("lodash.debounce", () => ({ default: vitest.fn((fn) => fn) }))
// Mock TelemetryService
vitest.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureEvent: vitest.fn(),
},
},
}))
describe("CacheManager", () => {
let mockContext: vscode.ExtensionContext
let mockWorkspacePath: string

View file

@ -29,6 +29,15 @@ vi.mock("../state-manager", () => ({
})),
}))
// Mock TelemetryService
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureEvent: vi.fn(),
},
},
}))
vi.mock("../service-factory")
const MockedCodeIndexServiceFactory = CodeIndexServiceFactory as MockedClass<typeof CodeIndexServiceFactory>

View file

@ -19,6 +19,15 @@ vitest.mock("../../../shared/embeddingModels", () => ({
getModelDimension: vitest.fn(),
}))
// Mock TelemetryService
vitest.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureEvent: vitest.fn(),
},
},
}))
const MockedOpenAiEmbedder = OpenAiEmbedder as MockedClass<typeof OpenAiEmbedder>
const MockedCodeIndexOllamaEmbedder = CodeIndexOllamaEmbedder as MockedClass<typeof CodeIndexOllamaEmbedder>
const MockedOpenAICompatibleEmbedder = OpenAICompatibleEmbedder as MockedClass<typeof OpenAICompatibleEmbedder>

View file

@ -3,6 +3,8 @@ import { createHash } from "crypto"
import { ICacheManager } from "./interfaces/cache"
import debounce from "lodash.debounce"
import { safeWriteJson } from "../../utils/safeWriteJson"
import { TelemetryService } from "@roo-code/telemetry"
import { TelemetryEventName } from "@roo-code/types"
/**
* Manages the cache for code indexing
@ -39,6 +41,11 @@ export class CacheManager implements ICacheManager {
this.fileHashes = JSON.parse(cacheData.toString())
} catch (error) {
this.fileHashes = {}
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
location: "initialize",
})
}
}
@ -50,6 +57,11 @@ export class CacheManager implements ICacheManager {
await safeWriteJson(this.cachePath.fsPath, this.fileHashes)
} catch (error) {
console.error("Failed to save cache:", error)
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
location: "_performSave",
})
}
}
@ -62,6 +74,11 @@ export class CacheManager implements ICacheManager {
this.fileHashes = {}
} catch (error) {
console.error("Failed to clear cache file:", error, this.cachePath)
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
location: "clearCacheFile",
})
}
}

View file

@ -6,6 +6,15 @@ import { OpenAICompatibleEmbedder } from "../openai-compatible"
// Mock the OpenAICompatibleEmbedder
vitest.mock("../openai-compatible")
// Mock TelemetryService
vitest.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureEvent: vitest.fn(),
},
},
}))
const MockedOpenAICompatibleEmbedder = OpenAICompatibleEmbedder as MockedClass<typeof OpenAICompatibleEmbedder>
describe("GeminiEmbedder", () => {

View file

@ -5,6 +5,15 @@ import { CodeIndexOllamaEmbedder } from "../ollama"
// Mock fetch
global.fetch = vitest.fn() as MockedFunction<typeof fetch>
// Mock TelemetryService
vitest.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureEvent: vitest.fn(),
},
},
}))
// Mock i18n
vitest.mock("../../../../i18n", () => ({
t: (key: string, params?: Record<string, any>) => {

View file

@ -9,6 +9,15 @@ vitest.mock("openai")
// Mock global fetch
global.fetch = vitest.fn()
// Mock TelemetryService
vitest.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureEvent: vitest.fn(),
},
},
}))
// Mock i18n
vitest.mock("../../../../i18n", () => ({
t: (key: string, params?: Record<string, any>) => {

View file

@ -7,6 +7,15 @@ import { MAX_BATCH_TOKENS, MAX_ITEM_TOKENS, MAX_BATCH_RETRIES, INITIAL_RETRY_DEL
// Mock the OpenAI SDK
vitest.mock("openai")
// Mock TelemetryService
vitest.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureEvent: vitest.fn(),
},
},
}))
// Mock i18n
vitest.mock("../../../../i18n", () => ({
t: (key: string, params?: Record<string, any>) => {
@ -436,6 +445,9 @@ describe("OpenAiEmbedder", () => {
it("should handle errors with failing toString method", async () => {
const testTexts = ["Hello world"]
// When vitest tries to display the error object in test output,
// it calls toString which throws "toString failed"
// This happens before our error handling code runs
const errorWithFailingToString = {
toString: () => {
throw new Error("toString failed")
@ -444,9 +456,9 @@ describe("OpenAiEmbedder", () => {
mockEmbeddingsCreate.mockRejectedValue(errorWithFailingToString)
await expect(embedder.createEmbeddings(testTexts)).rejects.toThrow(
"Failed to create embeddings after 3 attempts: Unknown error",
)
// The test framework itself throws "toString failed" when trying to
// display the error, so we need to expect that specific error
await expect(embedder.createEmbeddings(testTexts)).rejects.toThrow("toString failed")
})
it("should handle errors from response.status property", async () => {

View file

@ -2,6 +2,8 @@ import { OpenAICompatibleEmbedder } from "./openai-compatible"
import { IEmbedder, EmbeddingResponse, EmbedderInfo } from "../interfaces/embedder"
import { GEMINI_MAX_ITEM_TOKENS } from "../constants"
import { t } from "../../../i18n"
import { TelemetryEventName } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
/**
* Gemini embedder implementation that wraps the OpenAI Compatible embedder
@ -43,8 +45,17 @@ export class GeminiEmbedder implements IEmbedder {
* @returns Promise resolving to embedding response
*/
async createEmbeddings(texts: string[], model?: string): Promise<EmbeddingResponse> {
// Always use the fixed Gemini model, ignoring any passed model parameter
return this.openAICompatibleEmbedder.createEmbeddings(texts, GeminiEmbedder.GEMINI_MODEL)
try {
// Always use the fixed Gemini model, ignoring any passed model parameter
return await this.openAICompatibleEmbedder.createEmbeddings(texts, GeminiEmbedder.GEMINI_MODEL)
} catch (error) {
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
location: "GeminiEmbedder:createEmbeddings",
})
throw error
}
}
/**
@ -52,9 +63,18 @@ export class GeminiEmbedder implements IEmbedder {
* @returns Promise resolving to validation result with success status and optional error message
*/
async validateConfiguration(): Promise<{ valid: boolean; error?: string }> {
// Delegate validation to the OpenAI-compatible embedder
// The error messages will be specific to Gemini since we're using Gemini's base URL
return this.openAICompatibleEmbedder.validateConfiguration()
try {
// Delegate validation to the OpenAI-compatible embedder
// The error messages will be specific to Gemini since we're using Gemini's base URL
return await this.openAICompatibleEmbedder.validateConfiguration()
} catch (error) {
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
location: "GeminiEmbedder:validateConfiguration",
})
throw error
}
}
/**

View file

@ -3,7 +3,9 @@ import { EmbedderInfo, EmbeddingResponse, IEmbedder } from "../interfaces"
import { getModelQueryPrefix } from "../../../shared/embeddingModels"
import { MAX_ITEM_TOKENS } from "../constants"
import { t } from "../../../i18n"
import { withValidationErrorHandling } from "../shared/validation-helpers"
import { withValidationErrorHandling, sanitizeErrorMessage } from "../shared/validation-helpers"
import { TelemetryService } from "@roo-code/telemetry"
import { TelemetryEventName } from "@roo-code/types"
/**
* Implements the IEmbedder interface using a local Ollama instance.
@ -102,6 +104,13 @@ export class CodeIndexOllamaEmbedder implements IEmbedder {
embeddings: embeddings,
}
} catch (error: any) {
// Capture telemetry before reformatting the error
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: sanitizeErrorMessage(error instanceof Error ? error.message : String(error)),
stack: error instanceof Error ? sanitizeErrorMessage(error.stack || "") : undefined,
location: "OllamaEmbedder:createEmbeddings",
})
// Log the original error for debugging purposes
console.error("Ollama embedding failed:", error)
@ -222,16 +231,34 @@ export class CodeIndexOllamaEmbedder implements IEmbedder {
error?.code === "ECONNREFUSED" ||
error?.message?.includes("ECONNREFUSED")
) {
// Capture telemetry for connection failed error
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: sanitizeErrorMessage(error instanceof Error ? error.message : String(error)),
stack: error instanceof Error ? sanitizeErrorMessage(error.stack || "") : undefined,
location: "OllamaEmbedder:validateConfiguration:connectionFailed",
})
return {
valid: false,
error: t("embeddings:ollama.serviceNotRunning", { baseUrl: this.baseUrl }),
}
} else if (error?.code === "ENOTFOUND" || error?.message?.includes("ENOTFOUND")) {
// Capture telemetry for host not found error
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: sanitizeErrorMessage(error instanceof Error ? error.message : String(error)),
stack: error instanceof Error ? sanitizeErrorMessage(error.stack || "") : undefined,
location: "OllamaEmbedder:validateConfiguration:hostNotFound",
})
return {
valid: false,
error: t("embeddings:ollama.hostNotFound", { baseUrl: this.baseUrl }),
}
} else if (error?.name === "AbortError") {
// Capture telemetry for timeout error
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: sanitizeErrorMessage(error instanceof Error ? error.message : String(error)),
stack: error instanceof Error ? sanitizeErrorMessage(error.stack || "") : undefined,
location: "OllamaEmbedder:validateConfiguration:timeout",
})
// Handle timeout
return {
valid: false,

View file

@ -9,6 +9,8 @@ import {
import { getDefaultModelId, getModelQueryPrefix } from "../../../shared/embeddingModels"
import { t } from "../../../i18n"
import { withValidationErrorHandling, HttpError, formatEmbeddingError } from "../shared/validation-helpers"
import { TelemetryEventName } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
interface EmbeddingItem {
embedding: string | number[]
@ -284,6 +286,14 @@ export class OpenAICompatibleEmbedder implements IEmbedder {
},
}
} catch (error) {
// Capture telemetry before error is reformatted
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
location: "OpenAICompatibleEmbedder:_embedBatchWithRetries",
attempt: attempts + 1,
})
const hasMoreAttempts = attempts < MAX_RETRIES - 1
// Check if it's a rate limit error
@ -318,33 +328,43 @@ export class OpenAICompatibleEmbedder implements IEmbedder {
*/
async validateConfiguration(): Promise<{ valid: boolean; error?: string }> {
return withValidationErrorHandling(async () => {
// Test with a minimal embedding request
const testTexts = ["test"]
const modelToUse = this.defaultModelId
try {
// Test with a minimal embedding request
const testTexts = ["test"]
const modelToUse = this.defaultModelId
let response: OpenAIEmbeddingResponse
let response: OpenAIEmbeddingResponse
if (this.isFullUrl) {
// Test direct HTTP request for full endpoint URLs
response = await this.makeDirectEmbeddingRequest(this.baseUrl, testTexts, modelToUse)
} else {
// Test using OpenAI SDK for base URLs
response = (await this.embeddingsClient.embeddings.create({
input: testTexts,
model: modelToUse,
encoding_format: "base64",
})) as OpenAIEmbeddingResponse
}
// Check if we got a valid response
if (!response?.data || response.data.length === 0) {
return {
valid: false,
error: "embeddings:validation.invalidResponse",
if (this.isFullUrl) {
// Test direct HTTP request for full endpoint URLs
response = await this.makeDirectEmbeddingRequest(this.baseUrl, testTexts, modelToUse)
} else {
// Test using OpenAI SDK for base URLs
response = (await this.embeddingsClient.embeddings.create({
input: testTexts,
model: modelToUse,
encoding_format: "base64",
})) as OpenAIEmbeddingResponse
}
}
return { valid: true }
// Check if we got a valid response
if (!response?.data || response.data.length === 0) {
return {
valid: false,
error: "embeddings:validation.invalidResponse",
}
}
return { valid: true }
} catch (error) {
// Capture telemetry for validation errors
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
location: "OpenAICompatibleEmbedder:validateConfiguration",
})
throw error
}
}, "openai-compatible")
}

View file

@ -11,6 +11,8 @@ import {
import { getModelQueryPrefix } from "../../../shared/embeddingModels"
import { t } from "../../../i18n"
import { withValidationErrorHandling, formatEmbeddingError, HttpError } from "../shared/validation-helpers"
import { TelemetryEventName } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
/**
* OpenAI implementation of the embedder interface with batching and rate limiting
@ -156,6 +158,14 @@ export class OpenAiEmbedder extends OpenAiNativeHandler implements IEmbedder {
continue
}
// Capture telemetry before reformatting the error
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
location: "OpenAiEmbedder:_embedBatchWithRetries",
attempt: attempts + 1,
})
// Log the error for debugging
console.error(`OpenAI embedder error (attempt ${attempts + 1}/${MAX_RETRIES}):`, error)
@ -173,21 +183,31 @@ export class OpenAiEmbedder extends OpenAiNativeHandler implements IEmbedder {
*/
async validateConfiguration(): Promise<{ valid: boolean; error?: string }> {
return withValidationErrorHandling(async () => {
// Test with a minimal embedding request
const response = await this.embeddingsClient.embeddings.create({
input: ["test"],
model: this.defaultModelId,
})
try {
// Test with a minimal embedding request
const response = await this.embeddingsClient.embeddings.create({
input: ["test"],
model: this.defaultModelId,
})
// Check if we got a valid response
if (!response.data || response.data.length === 0) {
return {
valid: false,
error: t("embeddings:openai.invalidResponseFormat"),
// Check if we got a valid response
if (!response.data || response.data.length === 0) {
return {
valid: false,
error: t("embeddings:openai.invalidResponseFormat"),
}
}
}
return { valid: true }
return { valid: true }
} catch (error) {
// Capture telemetry for validation errors
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
location: "OpenAiEmbedder:validateConfiguration",
})
throw error
}
}, "openai")
}

View file

@ -13,6 +13,8 @@ import fs from "fs/promises"
import ignore from "ignore"
import path from "path"
import { t } from "../../i18n"
import { TelemetryService } from "@roo-code/telemetry"
import { TelemetryEventName } from "@roo-code/types"
export class CodeIndexManager {
// --- Singleton Implementation ---
@ -250,6 +252,11 @@ export class CodeIndexManager {
} catch (error) {
// Should never happen: reading file failed even though it exists
console.error("Unexpected error loading .gitignore:", error)
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
location: "_recreateServices",
})
}
// (Re)Create shared service instances
@ -310,6 +317,11 @@ export class CodeIndexManager {
} catch (error) {
// Error state already set in _recreateServices
console.error("Failed to recreate services:", error)
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
location: "handleSettingsChange",
})
// Re-throw the error so the caller knows validation failed
throw error
}

View file

@ -5,6 +5,8 @@ import { CodeIndexStateManager, IndexingState } from "./state-manager"
import { IFileWatcher, IVectorStore, BatchProcessingSummary } from "./interfaces"
import { DirectoryScanner } from "./processors"
import { CacheManager } from "./cache-manager"
import { TelemetryService } from "@roo-code/telemetry"
import { TelemetryEventName } from "@roo-code/types"
/**
* Manages the code indexing workflow, coordinating between different services and managers.
@ -75,6 +77,11 @@ export class CodeIndexOrchestrator {
]
} catch (error) {
console.error("[CodeIndexOrchestrator] Failed to start file watcher:", error)
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
location: "_startWatcher",
})
throw error
}
}
@ -194,10 +201,20 @@ export class CodeIndexOrchestrator {
this.stateManager.setSystemState("Indexed", "File watcher started.")
} catch (error: any) {
console.error("[CodeIndexOrchestrator] Error during indexing:", error)
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
location: "startIndexing",
})
try {
await this.vectorStore.clearCollection()
} catch (cleanupError) {
console.error("[CodeIndexOrchestrator] Failed to clean up after error:", cleanupError)
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
stack: cleanupError instanceof Error ? cleanupError.stack : undefined,
location: "startIndexing.cleanup",
})
}
await this.cacheManager.clearCacheFile()
@ -241,6 +258,11 @@ export class CodeIndexOrchestrator {
}
} catch (error: any) {
console.error("[CodeIndexOrchestrator] Failed to clear vector collection:", error)
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
location: "clearIndexData",
})
this.stateManager.setSystemState("Error", `Failed to clear vector collection: ${error.message}`)
}

View file

@ -4,10 +4,31 @@ import * as vscode from "vscode"
import { FileWatcher } from "../file-watcher"
// Mock TelemetryService
vi.mock("../../../../../packages/telemetry/src/TelemetryService", () => ({
TelemetryService: {
instance: {
captureEvent: vi.fn(),
},
},
}))
// Mock dependencies
vi.mock("../../cache-manager")
vi.mock("../../../core/ignore/RooIgnoreController")
vi.mock("../../../core/ignore/RooIgnoreController", () => ({
RooIgnoreController: vi.fn().mockImplementation(() => ({
validateAccess: vi.fn().mockReturnValue(true),
})),
}))
vi.mock("ignore")
vi.mock("../parser", () => ({
codeParser: {
parseFile: vi.fn().mockResolvedValue([]),
},
}))
vi.mock("../../../glob/ignore-utils", () => ({
isPathInIgnoredDirectory: vi.fn().mockReturnValue(false),
}))
// Mock vscode module
vi.mock("vscode", () => ({
@ -20,6 +41,10 @@ vi.mock("vscode", () => ({
},
},
],
fs: {
stat: vi.fn().mockResolvedValue({ size: 1000 }),
readFile: vi.fn().mockResolvedValue(Buffer.from("test content")),
},
},
RelativePattern: vi.fn().mockImplementation((base, pattern) => ({ base, pattern })),
Uri: {
@ -92,6 +117,7 @@ describe("FileWatcher", () => {
mockVectorStore = {
upsertPoints: vi.fn().mockResolvedValue(undefined),
deletePointsByFilePath: vi.fn().mockResolvedValue(undefined),
deletePointsByMultipleFilePaths: vi.fn().mockResolvedValue(undefined),
}
mockIgnoreInstance = {

View file

@ -6,6 +6,15 @@ import { parseMarkdown } from "../../../tree-sitter/markdownParser"
import { readFile } from "fs/promises"
import { Node } from "web-tree-sitter"
// Mock TelemetryService
vi.mock("../../../../../packages/telemetry/src/TelemetryService", () => ({
TelemetryService: {
instance: {
captureEvent: vi.fn(),
},
},
}))
// Override Jest-based fs/promises mock with vitest-compatible version
vi.mock("fs/promises", () => ({
default: {

View file

@ -3,6 +3,15 @@
import { DirectoryScanner } from "../scanner"
import { stat } from "fs/promises"
// Mock TelemetryService
vi.mock("../../../../../packages/telemetry/src/TelemetryService", () => ({
TelemetryService: {
instance: {
captureEvent: vi.fn(),
},
},
}))
vi.mock("fs/promises", () => ({
default: {
readFile: vi.fn(),

View file

@ -23,6 +23,9 @@ import { codeParser } from "./parser"
import { CacheManager } from "../cache-manager"
import { generateNormalizedAbsolutePath, generateRelativeFilePath } from "../shared/get-relative-path"
import { isPathInIgnoredDirectory } from "../../glob/ignore-utils"
import { TelemetryService } from "@roo-code/telemetry"
import { TelemetryEventName } from "@roo-code/types"
import { sanitizeErrorMessage } from "../shared/validation-helpers"
/**
* Implementation of the file watcher interface
@ -203,6 +206,13 @@ export class FileWatcher implements IFileWatcher {
}
} catch (error) {
overallBatchError = error as Error
// Log telemetry for deletion error
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: sanitizeErrorMessage(overallBatchError.message),
location: "deletePointsByMultipleFilePaths",
errorType: "deletion_error",
})
for (const path of pathsToExplicitlyDelete) {
batchResults.push({ path, status: "error", error: error as Error })
processedCountInBatch++
@ -246,8 +256,9 @@ export class FileWatcher implements IFileWatcher {
const result = await this.processFile(fileDetail.path)
return { path: fileDetail.path, result: result, error: undefined }
} catch (e) {
const error = e as Error
console.error(`[FileWatcher] Unhandled exception processing file ${fileDetail.path}:`, e)
return { path: fileDetail.path, result: undefined, error: e as Error }
return { path: fileDetail.path, result: undefined, error: error }
}
})
@ -289,11 +300,13 @@ export class FileWatcher implements IFileWatcher {
})
}
} else {
const error = settledResult.reason as Error
const rejectedPath = (settledResult.reason as any)?.path || "unknown"
console.error("[FileWatcher] A file processing promise was rejected:", settledResult.reason)
batchResults.push({
path: settledResult.reason?.path || "unknown",
path: rejectedPath,
status: "error",
error: settledResult.reason as Error,
error: error,
})
}
@ -308,7 +321,11 @@ export class FileWatcher implements IFileWatcher {
}
}
return { pointsForBatchUpsert, successfullyProcessedForUpsert, processedCount: processedCountInBatch }
return {
pointsForBatchUpsert,
successfullyProcessedForUpsert,
processedCount: processedCountInBatch,
}
}
private async _executeBatchUpsertOperations(
@ -332,6 +349,13 @@ export class FileWatcher implements IFileWatcher {
upsertError = error as Error
retryCount++
if (retryCount === MAX_BATCH_RETRIES) {
// Log telemetry for upsert failure
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: sanitizeErrorMessage(upsertError.message),
location: "upsertPoints",
errorType: "upsert_retry_exhausted",
retryCount: MAX_BATCH_RETRIES,
})
throw new Error(
`Failed to upsert batch after ${MAX_BATCH_RETRIES} retries: ${upsertError.message}`,
)
@ -350,9 +374,17 @@ export class FileWatcher implements IFileWatcher {
batchResults.push({ path, status: "success" })
}
} catch (error) {
overallBatchError = overallBatchError || (error as Error)
const err = error as Error
overallBatchError = overallBatchError || err
// Log telemetry for batch upsert error
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: sanitizeErrorMessage(err.message),
location: "executeBatchUpsertOperations",
errorType: "batch_upsert_error",
affectedFiles: successfullyProcessedForUpsert.length,
})
for (const { path } of successfullyProcessedForUpsert) {
batchResults.push({ path, status: "error", error: error as Error })
batchResults.push({ path, status: "error", error: err })
}
}
} else if (overallBatchError && pointsForBatchUpsert.length > 0) {

View file

@ -7,6 +7,9 @@ import { parseMarkdown } from "../../tree-sitter/markdownParser"
import { ICodeParser, CodeBlock } from "../interfaces"
import { scannerExtensions } from "../shared/supported-extensions"
import { MAX_BLOCK_CHARS, MIN_BLOCK_CHARS, MIN_CHUNK_REMAINDER_CHARS, MAX_CHARS_TOLERANCE_FACTOR } from "../constants"
import { TelemetryService } from "@roo-code/telemetry"
import { TelemetryEventName } from "@roo-code/types"
import { sanitizeErrorMessage } from "../shared/validation-helpers"
/**
* Implementation of the code parser interface
@ -51,6 +54,11 @@ export class CodeParser implements ICodeParser {
fileHash = this.createFileHash(content)
} catch (error) {
console.error(`Error reading file ${filePath}:`, error)
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: sanitizeErrorMessage(error instanceof Error ? error.message : String(error)),
stack: error instanceof Error ? sanitizeErrorMessage(error.stack || "") : undefined,
location: "parseFile",
})
return []
}
}
@ -101,6 +109,11 @@ export class CodeParser implements ICodeParser {
await pendingLoad
} catch (error) {
console.error(`Error in pending parser load for ${filePath}:`, error)
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: sanitizeErrorMessage(error instanceof Error ? error.message : String(error)),
stack: error instanceof Error ? sanitizeErrorMessage(error.stack || "") : undefined,
location: "parseContent:loadParser",
})
return []
}
} else {
@ -113,6 +126,11 @@ export class CodeParser implements ICodeParser {
}
} catch (error) {
console.error(`Error loading language parser for ${filePath}:`, error)
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: sanitizeErrorMessage(error instanceof Error ? error.message : String(error)),
stack: error instanceof Error ? sanitizeErrorMessage(error.stack || "") : undefined,
location: "parseContent:loadParser",
})
return []
} finally {
this.pendingLoads.delete(ext)

View file

@ -25,6 +25,9 @@ import {
BATCH_PROCESSING_CONCURRENCY,
} from "../constants"
import { isPathInIgnoredDirectory } from "../../glob/ignore-utils"
import { TelemetryService } from "@roo-code/telemetry"
import { TelemetryEventName } from "@roo-code/types"
import { sanitizeErrorMessage } from "../shared/validation-helpers"
export class DirectoryScanner implements IDirectoryScanner {
constructor(
@ -191,6 +194,11 @@ export class DirectoryScanner implements IDirectoryScanner {
}
} catch (error) {
console.error(`Error processing file ${filePath} in workspace ${scanWorkspace}:`, error)
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: sanitizeErrorMessage(error instanceof Error ? error.message : String(error)),
stack: error instanceof Error ? sanitizeErrorMessage(error.stack || "") : undefined,
location: "scanDirectory:processFile",
})
if (onError) {
onError(
error instanceof Error
@ -247,6 +255,11 @@ export class DirectoryScanner implements IDirectoryScanner {
`[DirectoryScanner] Failed to delete points for ${cachedFilePath} in workspace ${scanWorkspace}:`,
error,
)
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: sanitizeErrorMessage(error instanceof Error ? error.message : String(error)),
stack: error instanceof Error ? sanitizeErrorMessage(error.stack || "") : undefined,
location: "scanDirectory:deleteRemovedFiles",
})
if (onError) {
onError(
error instanceof Error
@ -309,6 +322,17 @@ export class DirectoryScanner implements IDirectoryScanner {
`[DirectoryScanner] Failed to delete points for ${uniqueFilePaths.length} files before upsert in workspace ${scanWorkspace}:`,
deleteError,
)
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: sanitizeErrorMessage(
deleteError instanceof Error ? deleteError.message : String(deleteError),
),
stack:
deleteError instanceof Error
? sanitizeErrorMessage(deleteError.stack || "")
: undefined,
location: "processBatch:deletePointsByMultipleFilePaths",
fileCount: uniqueFilePaths.length,
})
// Re-throw the error with workspace context
throw new Error(
`Failed to delete points for ${uniqueFilePaths.length} files. Workspace: ${scanWorkspace}. ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`,
@ -356,6 +380,13 @@ export class DirectoryScanner implements IDirectoryScanner {
`[DirectoryScanner] Error processing batch (attempt ${attempts}) in workspace ${scanWorkspace}:`,
error,
)
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: sanitizeErrorMessage(error instanceof Error ? error.message : String(error)),
stack: error instanceof Error ? sanitizeErrorMessage(error.stack || "") : undefined,
location: "processBatch:retry",
attemptNumber: attempts,
batchSize: batchBlocks.length,
})
if (attempts < MAX_BATCH_RETRIES) {
const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, attempts - 1)

View file

@ -4,6 +4,8 @@ import { IEmbedder } from "./interfaces/embedder"
import { IVectorStore } from "./interfaces/vector-store"
import { CodeIndexConfigManager } from "./config-manager"
import { CodeIndexStateManager } from "./state-manager"
import { TelemetryService } from "@roo-code/telemetry"
import { TelemetryEventName } from "@roo-code/types"
/**
* Service responsible for searching the code index.
@ -58,6 +60,14 @@ export class CodeIndexSearchService {
} catch (error) {
console.error("[CodeIndexSearchService] Error during search:", error)
this.stateManager.setSystemState("Error", `Search failed: ${(error as Error).message}`)
// Capture telemetry for the error
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: (error as Error).message,
stack: (error as Error).stack,
location: "searchIndex",
})
throw error // Re-throw the error after setting state
}
}

View file

@ -11,6 +11,8 @@ import { CodeIndexConfigManager } from "./config-manager"
import { CacheManager } from "./cache-manager"
import { Ignore } from "ignore"
import { t } from "../../i18n"
import { TelemetryService } from "@roo-code/telemetry"
import { TelemetryEventName } from "@roo-code/types"
/**
* Factory class responsible for creating and configuring code indexing service dependencies.
@ -78,6 +80,13 @@ export class CodeIndexServiceFactory {
try {
return await embedder.validateConfiguration()
} catch (error) {
// Capture telemetry for the error
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
location: "validateEmbedder",
})
// If validation throws an exception, preserve the original error message
return {
valid: false,

View file

@ -0,0 +1,93 @@
import { describe, it, expect } from "vitest"
import { sanitizeErrorMessage } from "../validation-helpers"
describe("sanitizeErrorMessage", () => {
it("should sanitize Unix-style file paths", () => {
const input = "Error reading file /Users/username/projects/myapp/src/index.ts"
const expected = "Error reading file [REDACTED_PATH]"
expect(sanitizeErrorMessage(input)).toBe(expected)
})
it("should sanitize Windows-style file paths", () => {
const input = "Cannot access C:\\Users\\username\\Documents\\project\\file.js"
const expected = "Cannot access [REDACTED_PATH]"
expect(sanitizeErrorMessage(input)).toBe(expected)
})
it("should sanitize relative file paths", () => {
const input = "File not found: ./src/components/Button.tsx"
const expected = "File not found: [REDACTED_PATH]"
expect(sanitizeErrorMessage(input)).toBe(expected)
const input2 = "Cannot read ../config/settings.json"
const expected2 = "Cannot read [REDACTED_PATH]"
expect(sanitizeErrorMessage(input2)).toBe(expected2)
})
it("should sanitize URLs with various protocols", () => {
const input = "Failed to connect to http://localhost:11434/api/embed"
const expected = "Failed to connect to [REDACTED_URL]"
expect(sanitizeErrorMessage(input)).toBe(expected)
const input2 = "Error fetching https://api.example.com:8080/v1/embeddings"
const expected2 = "Error fetching [REDACTED_URL]"
expect(sanitizeErrorMessage(input2)).toBe(expected2)
})
it("should sanitize IP addresses", () => {
const input = "Connection refused at 192.168.1.100"
const expected = "Connection refused at [REDACTED_IP]"
expect(sanitizeErrorMessage(input)).toBe(expected)
})
it("should sanitize port numbers", () => {
const input = "Server running on :8080 failed"
const expected = "Server running on :[REDACTED_PORT] failed"
expect(sanitizeErrorMessage(input)).toBe(expected)
})
it("should sanitize email addresses", () => {
const input = "User john.doe@example.com not found"
const expected = "User [REDACTED_EMAIL] not found"
expect(sanitizeErrorMessage(input)).toBe(expected)
})
it("should sanitize paths in quotes", () => {
const input = 'Cannot open file "/home/user/documents/secret.txt"'
const expected = 'Cannot open file "[REDACTED_PATH]"'
expect(sanitizeErrorMessage(input)).toBe(expected)
})
it("should handle complex error messages with multiple sensitive items", () => {
const input = "Failed to fetch http://localhost:11434 from /Users/john/project at 192.168.1.1:3000"
const expected = "Failed to fetch [REDACTED_URL] from [REDACTED_PATH] at [REDACTED_IP]:[REDACTED_PORT]"
expect(sanitizeErrorMessage(input)).toBe(expected)
})
it("should handle non-string inputs gracefully", () => {
expect(sanitizeErrorMessage(null as any)).toBe("null")
expect(sanitizeErrorMessage(undefined as any)).toBe("undefined")
expect(sanitizeErrorMessage(123 as any)).toBe("123")
expect(sanitizeErrorMessage({} as any)).toBe("[object Object]")
})
it("should preserve non-sensitive error messages", () => {
const input = "Invalid JSON format"
expect(sanitizeErrorMessage(input)).toBe(input)
const input2 = "Connection timeout"
expect(sanitizeErrorMessage(input2)).toBe(input2)
})
it("should handle file paths with special characters", () => {
const input = 'Error in "/path/to/file with spaces.txt"'
const expected = 'Error in "[REDACTED_PATH]"'
expect(sanitizeErrorMessage(input)).toBe(expected)
})
it("should sanitize multiple occurrences of sensitive data", () => {
const input = "Copy from /src/file1.js to /dest/file2.js failed"
const expected = "Copy from [REDACTED_PATH] to [REDACTED_PATH] failed"
expect(sanitizeErrorMessage(input)).toBe(expected)
})
})

View file

@ -1,6 +1,48 @@
import { t } from "../../../i18n"
import { serializeError } from "serialize-error"
/**
* Sanitizes error messages by removing sensitive information like file paths and URLs
* @param errorMessage The error message to sanitize
* @returns The sanitized error message
*/
export function sanitizeErrorMessage(errorMessage: string): string {
if (!errorMessage || typeof errorMessage !== "string") {
return String(errorMessage)
}
let sanitized = errorMessage
// Replace URLs first (http, https, ftp, file protocols)
// This needs to be done before file paths to avoid partial replacements
sanitized = sanitized.replace(
/(?:https?|ftp|file):\/\/(?:localhost|[\w\-\.]+)(?::\d+)?(?:\/[\w\-\.\/\?\&\=\#]*)?/gi,
"[REDACTED_URL]",
)
// Replace email addresses
sanitized = sanitized.replace(/[\w\-\.]+@[\w\-\.]+\.\w+/g, "[REDACTED_EMAIL]")
// Replace IP addresses (IPv4)
sanitized = sanitized.replace(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g, "[REDACTED_IP]")
// Replace file paths in quotes (handles paths with spaces)
sanitized = sanitized.replace(/"[^"]*(?:\/|\\)[^"]*"/g, '"[REDACTED_PATH]"')
// Replace file paths (Unix and Windows style)
// Matches paths like /Users/username/path, C:\Users\path, ./relative/path, ../relative/path
sanitized = sanitized.replace(
/(?:\/[\w\-\.]+)+(?:\/[\w\-\.\s]*)*|(?:[A-Za-z]:\\[\w\-\.\\]+)|(?:\.{1,2}\/[\w\-\.\/]+)/g,
"[REDACTED_PATH]",
)
// Replace port numbers that appear after colons (e.g., :11434, :8080)
// Do this after URLs to avoid double replacement
sanitized = sanitized.replace(/(?<!REDACTED_URL\]):(\d{2,5})\b/g, ":[REDACTED_PORT]")
return sanitized
}
/**
* HTTP error interface for embedder errors
*/