mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: add comprehensive error telemetry to code-index service (#5595)
This commit is contained in:
parent
39b83070c3
commit
32308d79c4
25 changed files with 536 additions and 51 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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>) => {
|
||||
|
|
|
|||
|
|
@ -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>) => {
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue