Forráskód Böngészése

Remove Sentry telemetry system from CodeGraph

Eliminates anonymous error reporting functionality that was collecting stack traces and error context via Sentry. Removes all telemetry-related code, configuration options, and documentation references.
Colby McHenry 2 hónapja
szülő
commit
8b5622d346

+ 0 - 1
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
 ```
 

+ 4 - 27
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

+ 0 - 24
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<typeof import('../index')> {
@@ -47,19 +46,10 @@ async function loadCodeGraph(): Promise<typeof import('../index')> {
 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);
     }

+ 0 - 10
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;
     }
   }

+ 1 - 6
src/errors.ts

@@ -233,13 +233,8 @@ export function logWarn(message: string, context?: Record<string, unknown>): voi
 }
 
 /**
- * Log an error message (also sends to Sentry if initialized)
+ * Log an error message
  */
 export function logError(message: string, context?: Record<string, unknown>): void {
   currentLogger.error(message, context);
-  // Lazy import to avoid circular deps
-  try {
-    const { captureMessage } = require('./sentry');
-    captureMessage(message, context);
-  } catch {}
 }

+ 0 - 7
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;
       }

+ 0 - 5
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',

+ 4 - 8
src/installer/config-writer.ts

@@ -100,22 +100,18 @@ function writeJsonFile(filePath: string, data: Record<string, any>): void {
 /**
  * Get the MCP server configuration
  */
-function getMcpServerConfig(options?: { telemetry?: boolean }): Record<string, any> {
-  const config: Record<string, any> = {
+function getMcpServerConfig(): Record<string, any> {
+  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);
 }

+ 2 - 14
src/installer/index.ts

@@ -55,21 +55,9 @@ export async function runInstaller(): Promise<void> {
     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'}`);

+ 0 - 4
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`);
     }

+ 0 - 1
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)}`);
     }
   }

+ 0 - 2
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(

+ 0 - 3
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;

+ 0 - 174
src/sentry.ts

@@ -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<string, string> = {};
-
-/**
- * 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<string, unknown>) {
-  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<string, unknown> = { ...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<string, unknown> = {
-      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<string, unknown>) {
-  if (!_enabled) return;
-
-  try {
-    const eventId = createHash('md5').update(`${Date.now()}-${Math.random()}`).digest('hex');
-    const timestamp = new Date().toISOString();
-
-    const event: Record<string, unknown> = {
-      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] || '<anonymous>',
-        filename: match[2],
-        lineno: parseInt(match[3], 10),
-        colno: parseInt(match[4], 10),
-      };
-    })
-    .filter((f): f is NonNullable<typeof f> => f !== null)
-    .reverse();
-
-  return frames.length > 0 ? { frames } : undefined;
-}