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.
This commit is contained in:
Colby McHenry
2026-04-03 09:54:07 -05:00
parent 9bb8e96bd4
commit 8b5622d346
14 changed files with 11 additions and 286 deletions
-1
View File
@@ -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
View File
@@ -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
-24
View File
@@ -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);
}
-10
View File
@@ -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
View File
@@ -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 {}
}
-7
View File
@@ -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;
}
-5
View File
@@ -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
View File
@@ -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
View File
@@ -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'}`);
-4
View File
@@ -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`);
}
-1
View File
@@ -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)}`);
}
}
-2
View File
@@ -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(
-3
View File
@@ -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;
-174
View File
@@ -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;
}