diff --git a/CLAUDE.md b/CLAUDE.md index c7f891e..eee14d4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -78,7 +78,6 @@ src/ │ ├── index.ts # MCPServer class │ ├── tools.ts # MCP tool definitions │ └── transport.ts # Stdio transport -├── sentry.ts # Error tracking/reporting └── bin/codegraph.ts # CLI entry point ``` diff --git a/README.md b/README.md index 2e6b9a4..2fd7a43 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,6 @@ The interactive installer will: - Prompt to install `codegraph` globally (needed for hooks & MCP server to work) - Configure the MCP server in `~/.claude.json` - Set up auto-allow permissions for CodeGraph tools -- Ask about [anonymous error reporting](#-telemetry) (opt-out available) - Add global instructions to `~/.claude/CLAUDE.md` (teaches Claude how to use CodeGraph) - Install Claude Code hooks for automatic index syncing - Optionally initialize your current project @@ -311,11 +310,10 @@ The installer will: 1. Prompt to install `codegraph` globally (needed for hooks & MCP server) 2. Ask for installation location (global `~/.claude` or local `./.claude`) 3. Optionally set up auto-allow permissions -4. Ask about [anonymous error reporting](#-telemetry) (opt-out available) -5. Configure the MCP server in `claude.json` -6. Add global instructions to `~/.claude/CLAUDE.md` (teaches Claude how to use CodeGraph) -7. Install Claude Code hooks for automatic index syncing -8. For local installs: initialize and index the current project +4. Configure the MCP server in `claude.json` +5. Add global instructions to `~/.claude/CLAUDE.md` (teaches Claude how to use CodeGraph) +6. Install Claude Code hooks for automatic index syncing +7. For local installs: initialize and index the current project ### `codegraph init [path]` @@ -711,27 +709,6 @@ Run `codegraph init` in your project directory first. - Check if the file's language is supported - Verify the file isn't excluded by config patterns -## 📡 Telemetry - -CodeGraph collects anonymous error reports via [Sentry](https://sentry.io) to help diagnose and fix bugs. This is **enabled by default** in production environments (disabled in development/test). - -**What is collected:** -- Error type, message, and stack trace (includes local file paths in the trace) -- CodeGraph version and process name (CLI or MCP server) - -**What is NOT collected:** -- Source code contents -- File contents or repository data -- Personal information or environment variables - -**To opt out**, set the environment variable before running CodeGraph: - -```bash -export CODEGRAPH_TELEMETRY=off -``` - ---- - ## 📄 License MIT diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts index 9b45971..811a4e9 100644 --- a/src/bin/codegraph.ts +++ b/src/bin/codegraph.ts @@ -28,7 +28,6 @@ import * as path from 'path'; import * as fs from 'fs'; import { spawn } from 'child_process'; import { getCodeGraphDir, findNearestCodeGraphRoot, isInitialized } from '../directory'; -import { initSentry, captureException } from '../sentry'; // Lazy-load heavy modules (CodeGraph, runInstaller) to keep CLI startup fast. async function loadCodeGraph(): Promise { @@ -47,19 +46,10 @@ async function loadCodeGraph(): Promise { type IndexProgress = import('../index').IndexProgress; // Check if running with no arguments - run installer -// Read version for Sentry release tag -const pkgVersion = (() => { - try { - return JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf-8')).version; - } catch { return undefined; } -})(); -initSentry({ processName: 'codegraph-cli', version: pkgVersion }); - if (process.argv.length === 2) { import('../installer').then(({ runInstaller }) => runInstaller() ).catch((err) => { - captureException(err); console.error('Installation failed:', err instanceof Error ? err.message : String(err)); process.exit(1); }); @@ -69,12 +59,10 @@ if (process.argv.length === 2) { } process.on('uncaughtException', (error) => { - captureException(error); console.error('[CodeGraph] Uncaught exception:', error); }); process.on('unhandledRejection', (reason) => { - captureException(reason); console.error('[CodeGraph] Unhandled rejection:', reason); }); @@ -296,7 +284,6 @@ program cg.destroy(); } catch (err) { - captureException(err); error(`Failed to initialize: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); } @@ -342,7 +329,6 @@ program success(`Removed CodeGraph from ${projectPath}`); } catch (err) { - captureException(err); error(`Failed to uninitialize: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); } @@ -411,7 +397,6 @@ program cg.destroy(); } catch (err) { - captureException(err); error(`Failed to index: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); } @@ -469,7 +454,6 @@ program cg.destroy(); } catch (err) { - captureException(err); if (!options.quiet) { error(`Failed to sync: ${err instanceof Error ? err.message : String(err)}`); } @@ -581,7 +565,6 @@ program cg.destroy(); } catch (err) { - captureException(err); error(`Failed to get status: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); } @@ -644,7 +627,6 @@ program cg.destroy(); } catch (err) { - captureException(err); error(`Search failed: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); } @@ -770,7 +752,6 @@ program console.log(); cg.destroy(); } catch (err) { - captureException(err); error(`Failed to list files: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); } @@ -898,7 +879,6 @@ program cg.destroy(); } catch (err) { - captureException(err); error(`Failed to build context: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); } @@ -949,7 +929,6 @@ program console.error(chalk.cyan(' codegraph_status') + ' - Get index status'); } } catch (err) { - captureException(err); error(`Failed to start server: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); } @@ -1013,7 +992,6 @@ program process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); } catch (err) { - captureException(err); error(`Failed to start visualizer: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); } @@ -1126,7 +1104,6 @@ program fs.unlinkSync(lockPath); success('Removed lock file. You can now run indexing again.'); } catch (err) { - captureException(err); error(`Failed to remove lock: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); } @@ -1265,7 +1242,6 @@ program cg.destroy(); } catch (err) { - captureException(err); error(`Affected analysis failed: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); } diff --git a/src/db/queries.ts b/src/db/queries.ts index e3cea84..8d39fd4 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -240,16 +240,6 @@ export class QueryBuilder { updatedAt: node.updatedAt ?? Date.now(), }); } catch (error) { - const { captureException } = require('../sentry'); - captureException(error, { - operation: 'insertNode', - nodeId: node.id, - nodeKind: node.kind, - nodeName: node.name, - filePath: node.filePath, - language: node.language, - startLine: node.startLine, - }); throw error; } } diff --git a/src/errors.ts b/src/errors.ts index 1a9a811..acd440c 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -233,13 +233,8 @@ export function logWarn(message: string, context?: Record): voi } /** - * Log an error message (also sends to Sentry if initialized) + * Log an error message */ export function logError(message: string, context?: Record): void { currentLogger.error(message, context); - // Lazy import to avoid circular deps - try { - const { captureMessage } = require('./sentry'); - captureMessage(message, context); - } catch {} } diff --git a/src/extraction/index.ts b/src/extraction/index.ts index e42117b..b009cf0 100644 --- a/src/extraction/index.ts +++ b/src/extraction/index.ts @@ -20,7 +20,6 @@ import { QueryBuilder } from '../db/queries'; import { extractFromSource } from './tree-sitter'; import { detectLanguage, isLanguageSupported, initGrammars, loadGrammarsForLanguages } from './grammars'; import { logDebug, logWarn } from '../errors'; -import { captureException } from '../sentry'; import { validatePathWithinRoot, normalizePath } from '../utils'; import picomatch from 'picomatch'; @@ -257,7 +256,6 @@ function scanDirectoryWalk( try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (error) { - captureException(error, { operation: 'walk-directory', dir }); logDebug('Skipping unreadable directory', { dir, error: String(error) }); return; } @@ -548,7 +546,6 @@ export class ExtractionOrchestrator { stats = await fsp.stat(fullPath); content = await fsp.readFile(fullPath, 'utf-8'); } catch (error) { - captureException(error, { operation: 'extract-file', filePath: fullPath }); return { nodes: [], edges: [], @@ -744,7 +741,6 @@ export class ExtractionOrchestrator { try { content = fs.readFileSync(fullPath, 'utf-8'); } catch (error) { - captureException(error, { operation: 'sync-read-file', filePath }); logDebug('Skipping unreadable file during sync', { filePath, error: String(error) }); continue; } @@ -796,7 +792,6 @@ export class ExtractionOrchestrator { try { content = fs.readFileSync(fullPath, 'utf-8'); } catch (error) { - captureException(error, { operation: 'sync-read-file', filePath }); logDebug('Skipping unreadable file during sync', { filePath, error: String(error) }); continue; } @@ -876,7 +871,6 @@ export class ExtractionOrchestrator { try { content = fs.readFileSync(fullPath, 'utf-8'); } catch (error) { - captureException(error, { operation: 'detect-changes-read-file', filePath }); logDebug('Skipping unreadable file while detecting changes', { filePath, error: String(error) }); continue; } @@ -927,7 +921,6 @@ export class ExtractionOrchestrator { try { content = fs.readFileSync(fullPath, 'utf-8'); } catch (error) { - captureException(error, { operation: 'detect-changes-read-file', filePath }); logDebug('Skipping unreadable file while detecting changes', { filePath, error: String(error) }); continue; } diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 183fae8..220e353 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -17,7 +17,6 @@ import { UnresolvedReference, } from '../types'; import { getParser, detectLanguage, isLanguageSupported } from './grammars'; -import { captureException } from '../sentry'; /** * Generate a unique node ID @@ -961,7 +960,6 @@ export class TreeSitterExtractor { this.visitNode(this.tree.rootNode); this.nodeStack.pop(); } catch (error) { - captureException(error, { operation: 'tree-sitter-parse', filePath: this.filePath, language: this.language }); this.errors.push({ message: `Parse error: ${error instanceof Error ? error.message : String(error)}`, severity: 'error', @@ -2514,7 +2512,6 @@ export class LiquidExtractor { // Extract assign statements as variables this.extractAssignments(fileNode.id); } catch (error) { - captureException(error, { operation: 'liquid-extraction', filePath: this.filePath }); this.errors.push({ message: `Liquid extraction error: ${error instanceof Error ? error.message : String(error)}`, severity: 'error', @@ -2856,7 +2853,6 @@ export class SvelteExtractor { this.processScriptBlock(block, componentNode.id); } } catch (error) { - captureException(error, { operation: 'svelte-extraction', filePath: this.filePath }); this.errors.push({ message: `Svelte extraction error: ${error instanceof Error ? error.message : String(error)}`, severity: 'error', @@ -3047,7 +3043,6 @@ export class DfmExtractor { const fileNode = this.createFileNode(); this.parseComponents(fileNode.id); } catch (error) { - captureException(error, { operation: 'dfm-extraction', filePath: this.filePath }); this.errors.push({ message: `DFM extraction error: ${error instanceof Error ? error.message : String(error)}`, severity: 'error', diff --git a/src/installer/config-writer.ts b/src/installer/config-writer.ts index dc70f92..2d9e8ab 100644 --- a/src/installer/config-writer.ts +++ b/src/installer/config-writer.ts @@ -100,22 +100,18 @@ function writeJsonFile(filePath: string, data: Record): void { /** * Get the MCP server configuration */ -function getMcpServerConfig(options?: { telemetry?: boolean }): Record { - const config: Record = { +function getMcpServerConfig(): Record { + return { type: 'stdio', command: 'codegraph', args: ['serve', '--mcp'], }; - if (options?.telemetry === false) { - config.env = { CODEGRAPH_TELEMETRY: 'off' }; - } - return config; } /** * Write the MCP server configuration to claude.json */ -export function writeMcpConfig(location: InstallLocation, options?: { telemetry?: boolean }): void { +export function writeMcpConfig(location: InstallLocation): void { const claudeJsonPath = getClaudeJsonPath(location); const config = readJsonFile(claudeJsonPath); @@ -125,7 +121,7 @@ export function writeMcpConfig(location: InstallLocation, options?: { telemetry? } // Add or update codegraph server - config.mcpServers.codegraph = getMcpServerConfig(options); + config.mcpServers.codegraph = getMcpServerConfig(); writeJsonFile(claudeJsonPath, config); } diff --git a/src/installer/index.ts b/src/installer/index.ts index 81a33f4..4b92d14 100644 --- a/src/installer/index.ts +++ b/src/installer/index.ts @@ -55,21 +55,9 @@ export async function runInstaller(): Promise { const autoAllow = await promptAutoAllow(); console.log(); - // Step 4: Ask about anonymous error reporting - console.log(chalk.bold(' Send anonymous error reports?') + chalk.dim(' (Helps fix bugs — no source code collected)')); - console.log(); - const enableTelemetry = await promptConfirm('Enable anonymous error reporting', true); - - if (!enableTelemetry) { - info('Telemetry disabled'); - } else { - success('Anonymous error reporting enabled'); - } - console.log(); - - // Step 5: Write MCP configuration (includes telemetry env if opted out) + // Step 4: Write MCP configuration const alreadyHasMcp = hasMcpConfig(location); - writeMcpConfig(location, { telemetry: enableTelemetry }); + writeMcpConfig(location); if (alreadyHasMcp) { success(`Updated MCP server in ${location === 'global' ? '~/.claude.json' : './.claude.json'}`); diff --git a/src/mcp/index.ts b/src/mcp/index.ts index a49c94d..ddcf828 100644 --- a/src/mcp/index.ts +++ b/src/mcp/index.ts @@ -19,9 +19,6 @@ import * as path from 'path'; import CodeGraph, { findNearestCodeGraphRoot } from '../index'; import { StdioTransport, JsonRpcRequest, JsonRpcNotification, ErrorCodes } from './transport'; import { tools, ToolHandler } from './tools'; -import { initSentry, captureException } from '../sentry'; - -initSentry({ processName: 'codegraph-mcp' }); /** * Convert a file:// URI to a filesystem path. @@ -121,7 +118,6 @@ export class MCPServer { this.toolHandler.setDefaultCodeGraph(this.cg); } catch (err) { // Log the error so transient failures are diagnosable (see issue #47) - captureException(err); const msg = err instanceof Error ? err.message : String(err); process.stderr.write(`[CodeGraph MCP] Failed to open project at ${resolvedRoot}: ${msg}\n`); } diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts index 7d3e46c..d490a1b 100644 --- a/src/mcp/tools.ts +++ b/src/mcp/tools.ts @@ -370,7 +370,6 @@ export class ToolHandler { return this.errorResult(`Unknown tool: ${toolName}`); } } catch (err) { - try { const { captureException } = require('../sentry'); captureException(err, { tool: toolName }); } catch {} return this.errorResult(`Tool execution failed: ${err instanceof Error ? err.message : String(err)}`); } } diff --git a/src/mcp/transport.ts b/src/mcp/transport.ts index 6c21257..4403891 100644 --- a/src/mcp/transport.ts +++ b/src/mcp/transport.ts @@ -5,7 +5,6 @@ */ import * as readline from 'readline'; -import { captureException } from '../sentry'; /** * JSON-RPC 2.0 Request @@ -163,7 +162,6 @@ export class StdioTransport { try { await this.messageHandler(parsed as JsonRpcRequest | JsonRpcNotification); } catch (err) { - captureException(err, { operation: 'mcp-message-handler' }); const message = parsed as JsonRpcRequest; if ('id' in message) { this.sendError( diff --git a/src/resolution/index.ts b/src/resolution/index.ts index 3047101..f9752a8 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -8,7 +8,6 @@ import * as fs from 'fs'; import * as path from 'path'; import { Node, UnresolvedReference, Edge } from '../types'; import { QueryBuilder } from '../db/queries'; -import { captureException } from '../sentry'; import { UnresolvedRef, ResolvedRef, @@ -179,7 +178,6 @@ export class ReferenceResolver { try { return fs.existsSync(fullPath); } catch (error) { - captureException(error, { operation: 'resolution-file-exists', filePath }); logDebug('Error checking file existence', { filePath, error: String(error) }); return false; } @@ -196,7 +194,6 @@ export class ReferenceResolver { this.fileCache.set(filePath, content); return content; } catch (error) { - captureException(error, { operation: 'resolution-read-file', filePath }); logDebug('Failed to read file for resolution', { filePath, error: String(error) }); this.fileCache.set(filePath, null); return null; diff --git a/src/sentry.ts b/src/sentry.ts deleted file mode 100644 index bc2230d..0000000 --- a/src/sentry.ts +++ /dev/null @@ -1,174 +0,0 @@ -/** - * Lightweight Sentry client for CodeGraph — uses the HTTP envelope API directly. - * No @sentry/node dependency, works in any Node.js environment. - */ - -import { createHash } from 'crypto'; - -const DSN = 'https://9591f8aca69bcf98e9feb31544200b47@o1181972.ingest.us.sentry.io/4510840133713920'; -const DSN_PARTS = DSN.match(/^https:\/\/([^@]+)@([^/]+)\/(.+)$/); -const PUBLIC_KEY = DSN_PARTS![1]; -const HOST = DSN_PARTS![2]; -const PROJECT_ID = DSN_PARTS![3]; -const STORE_URL = `https://${HOST}/api/${PROJECT_ID}/envelope/`; - -let _enabled = false; -let _release = 'codegraph@unknown'; -let _tags: Record = {}; - -/** - * Initialize Sentry error reporting. - * Safe to call multiple times — subsequent calls update tags/release. - * - * Opt out by setting the environment variable CODEGRAPH_TELEMETRY=off - */ -export function initSentry({ processName, version }: { processName: string; version?: string }) { - // Skip in development/test environments - if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test' || process.env.VITEST) { - return; - } - // Respect user opt-out - const telemetry = process.env.CODEGRAPH_TELEMETRY?.toLowerCase(); - if (telemetry === 'off' || telemetry === '0' || telemetry === 'false') { - return; - } - _enabled = true; - _release = `codegraph@${version ?? process.env.npm_package_version ?? 'unknown'}`; - _tags = { processName }; -} - -/** - * Send an error to Sentry with full stack trace and context. - * Fire-and-forget — never throws, never blocks. - */ -export function captureException(error: unknown, extra?: Record) { - if (!_enabled) return; - - try { - const err = error instanceof Error ? error : new Error(String(error)); - const msg = err.message.toLowerCase(); - - // Filter non-actionable noise - if (msg.includes('pty') || msg.includes('terminal session')) return; - if ((msg.includes('econnrefused') || msg.includes('econnreset')) && msg.includes('127.0.0.1')) return; - - const eventId = createHash('md5').update(`${Date.now()}-${Math.random()}`).digest('hex'); - const timestamp = new Date().toISOString(); - - // Attach CodeGraphError context if available - const errorContext: Record = { ...extra }; - if ('code' in err && typeof (err as any).code === 'string') { - errorContext.errorCode = (err as any).code; - } - if ('context' in err && typeof (err as any).context === 'object') { - Object.assign(errorContext, (err as any).context); - } - - const event: Record = { - event_id: eventId, - timestamp, - platform: 'node', - level: 'error', - release: _release, - tags: _tags, - exception: { - values: [{ - type: err.name, - value: err.message, - stacktrace: parseStack(err.stack), - }], - }, - }; - - if (Object.keys(errorContext).length > 0) { - event.extra = errorContext; - } - - const payload = JSON.stringify(event); - const envelope = [ - JSON.stringify({ event_id: eventId, sent_at: timestamp, dsn: DSN }), - JSON.stringify({ type: 'event', length: payload.length }), - payload, - ].join('\n') + '\n'; - - fetch(STORE_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-sentry-envelope', - 'X-Sentry-Auth': `Sentry sentry_version=7, sentry_key=${PUBLIC_KEY}`, - }, - body: envelope, - }).catch(() => {}); - } catch { - // Never throw from error reporting - } -} - -/** - * Send a message-level event to Sentry (for logged errors without Error objects). - */ -export function captureMessage(message: string, context?: Record) { - if (!_enabled) return; - - try { - const eventId = createHash('md5').update(`${Date.now()}-${Math.random()}`).digest('hex'); - const timestamp = new Date().toISOString(); - - const event: Record = { - event_id: eventId, - timestamp, - platform: 'node', - level: 'error', - release: _release, - tags: _tags, - message: { formatted: message }, - }; - - if (context && Object.keys(context).length > 0) { - event.extra = context; - } - - const payload = JSON.stringify(event); - const envelope = [ - JSON.stringify({ event_id: eventId, sent_at: timestamp, dsn: DSN }), - JSON.stringify({ type: 'event', length: payload.length }), - payload, - ].join('\n') + '\n'; - - fetch(STORE_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-sentry-envelope', - 'X-Sentry-Auth': `Sentry sentry_version=7, sentry_key=${PUBLIC_KEY}`, - }, - body: envelope, - }).catch(() => {}); - } catch { - // Never throw from error reporting - } -} - -/** - * Parse a Node.js Error.stack string into Sentry's stacktrace format. - */ -function parseStack(stack?: string): { frames: Array<{ filename: string; function: string; lineno?: number; colno?: number }> } | undefined { - if (!stack) return undefined; - - const frames = stack - .split('\n') - .slice(1) - .map((line) => { - const match = line.match(/^\s+at\s+(?:(.+?)\s+\()?(.*?):(\d+):(\d+)\)?$/); - if (!match || !match[2] || !match[3] || !match[4]) return null; - return { - function: match[1] || '', - filename: match[2], - lineno: parseInt(match[3], 10), - colno: parseInt(match[4], 10), - }; - }) - .filter((f): f is NonNullable => f !== null) - .reverse(); - - return frames.length > 0 ? { frames } : undefined; -}