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:
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 {}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
@@ -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'}`);
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
|
||||
@@ -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)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user