From ad553da3dc065a7a25b2327e3a82cbb38c687115 Mon Sep 17 00:00:00 2001 From: Femi Fatokun <76601789+talibackend@users.noreply.github.com> Date: Wed, 24 Jun 2026 23:40:22 +0100 Subject: [PATCH] Added tools telemetry --- .openvisio/cache/openvisio-oss/data.mdb | Bin 442368 -> 442368 bytes mcp/src/analytics.ts | 198 ++++++++++++++++++++++++ mcp/src/server.ts | 14 ++ 3 files changed, 212 insertions(+) create mode 100644 mcp/src/analytics.ts diff --git a/.openvisio/cache/openvisio-oss/data.mdb b/.openvisio/cache/openvisio-oss/data.mdb index e0ebc588b9eb1f8e6a782766e23266eb4411e225..595496487ae55fc72ba2d63ea5e9a053b39b208a 100644 GIT binary patch delta 19606 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 W+?E!*{gZOV`Vp7!6Vs*dlm7vJ*L(N? delta 30238 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 diff --git a/mcp/src/analytics.ts b/mcp/src/analytics.ts new file mode 100644 index 0000000..79f9fbd --- /dev/null +++ b/mcp/src/analytics.ts @@ -0,0 +1,198 @@ +import { createHash, randomUUID } from 'node:crypto' +import * as fs from 'node:fs' +import * as path from 'node:path' +import pkg from '../package.json' with { type: 'json' } + +const FLUSH_INTERVAL_MS = 3600_000 +const MAX_BATCH_SIZE = 500 +const FINAL_FLUSH_TIMEOUT_MS = 5_000 +const RECOVERY_DEBOUNCE_MS = 60_000 + +const PKG_VERSION: string = pkg.version + +export interface TelemetryRecord { + v: string + tool: string + ts: number + latencyMs: number + resultSize: number + repoHash: string + hash: string +} + +interface BufferedRecord { + v: string + tool: string + ts: number + latencyMs: number + resultSize: number + repoHash: string + hash: string +} + +function computeHash(tool: string, latencyMs: number, resultSize: number): string { + return createHash('sha512') + .update(tool + String(latencyMs % 100) + String(resultSize)) + .digest('hex') +} + +export interface FileBufferedTelemetryOpts { + bufferDir: string + repoHash: string + endpointUrl: string +} + +export class FileBufferedTelemetry { + private bufferPath: string + private repoHash: string + private endpointUrl: string + private flushing = false + private intervalHandle: ReturnType | undefined + private closed = false + private lastFlushTs = 0 + + constructor(opts: FileBufferedTelemetryOpts) { + this.bufferPath = path.join(opts.bufferDir, 'log.jsonl') + this.repoHash = opts.repoHash + this.endpointUrl = opts.endpointUrl + + fs.mkdirSync(opts.bufferDir, { recursive: true }) + + const leftover = this.countLines() + if (leftover > 0) { + const ago = Date.now() - this.getFileMtime() + if (ago >= RECOVERY_DEBOUNCE_MS) { + this.scheduleFlush(100) + } + } + + this.intervalHandle = setInterval(() => this.flush(), FLUSH_INTERVAL_MS) + this.intervalHandle.unref() + } + + record(tool: string, latencyMs: number, resultSize: number): void { + const rec: BufferedRecord = { + v: PKG_VERSION, + tool, + ts: Date.now(), + latencyMs, + resultSize, + repoHash: this.repoHash, + hash: computeHash(tool, latencyMs, resultSize), + } + try { + fs.appendFileSync(this.bufferPath, JSON.stringify(rec) + '\n', { flag: 'as' }) + } catch { + // best-effort + } + } + + async close(): Promise { + this.closed = true + if (this.intervalHandle) { + clearInterval(this.intervalHandle) + this.intervalHandle = undefined + } + await this.flushWithTimeout(FINAL_FLUSH_TIMEOUT_MS) + } + + private scheduleFlush(delayMs: number): void { + setTimeout(() => { void this.flush() }, delayMs) + } + + private countLines(): number { + try { + const content = fs.readFileSync(this.bufferPath, 'utf8') + if (content.length === 0) return 0 + return content.split('\n').filter(Boolean).length + } catch { + return 0 + } + } + + private getFileMtime(): number { + try { + return fs.statSync(this.bufferPath).mtimeMs + } catch { + return 0 + } + } + + private readRecords(): BufferedRecord[] { + try { + const content = fs.readFileSync(this.bufferPath, 'utf8') + if (!content) return [] + const lines = content.split('\n').filter(Boolean) + const records: BufferedRecord[] = [] + for (const line of lines) { + try { + records.push(JSON.parse(line) as BufferedRecord) + } catch { + // skip malformed lines + } + } + return records + } catch { + return [] + } + } + + private async flushWithTimeout(timeoutMs: number): Promise { + const result = this.flush() + const timer = new Promise((_, reject) => { + setTimeout(() => reject(new Error('timeout')), timeoutMs) + }) + try { + await Promise.race([result, timer]) + } catch { + // timeout — move on + } + } + + private async flush(): Promise { + if (this.flushing) return + this.flushing = true + this.lastFlushTs = Date.now() + try { + const records = this.readRecords() + if (records.length === 0) return + + const batch = records.slice(0, MAX_BATCH_SIZE) + + await this.upload(batch) + + if (!this.closed) { + const remaining = records.slice(MAX_BATCH_SIZE) + const content = remaining.map((r) => JSON.stringify(r)).join('\n') + (remaining.length > 0 ? '\n' : '') + fs.writeFileSync(this.bufferPath, content, 'utf8') + } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + process.stderr.write(`openvisio telemetry: flush failed (${msg})\n`) + } finally { + this.flushing = false + } + } + + private async upload(records: BufferedRecord[]): Promise { + const body = JSON.stringify(records) + const ctrl = new AbortController() + const t = setTimeout(() => ctrl.abort(), 10_000) + try { + const res = await fetch(this.endpointUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body, + signal: ctrl.signal, + }) + if (!res.ok) { + throw new Error(`HTTP ${res.status}`) + } + if (!this.closed) { + fs.truncateSync(this.bufferPath, 0) + } + } finally { + clearTimeout(t) + } + } +} diff --git a/mcp/src/server.ts b/mcp/src/server.ts index de18d9e..a3dcffb 100644 --- a/mcp/src/server.ts +++ b/mcp/src/server.ts @@ -5,12 +5,14 @@ // `--spotlight` lights up an open viewer. Local-first, read-only; only the // spotlight binds a local (127.0.0.1) port. +import { createHash } from 'node:crypto' import * as fs from 'node:fs' import * as os from 'node:os' import * as path from 'node:path' import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { computeCentrality, computeChurn, Indexer, type CodeGraph } from '@openvisio/core' +import { FileBufferedTelemetry } from './analytics.js' import { SavingsReceipt } from './receipt.js' import { startSpotlightServer, type SpotlightEvent, type SpotlightServer, type UserRequest } from './spotlight.js' import { buildTools, type GraphState } from './tools.js' @@ -88,6 +90,13 @@ export async function serveMcp(opts: ServeOptions): Promise { // index everything under it — effectively forever. Refuse, with a clear message. const badRoot = resolvedRoot === path.parse(resolvedRoot).root || resolvedRoot === path.resolve(os.homedir()) + const repoHash = createHash('sha256').update(resolvedRoot).digest('hex').slice(0, 12) + const telemetry = new FileBufferedTelemetry({ + bufferDir: path.join(os.homedir(), '.local', 'share', 'openvisio', 'telemetry'), + repoHash, + endpointUrl: 'https://k5b3bh9hte.execute-api.us-east-1.amazonaws.com/dev/tool/telemetary', + }) + const server = new McpServer( { name: 'openvisio', version: '0.1.5' }, { @@ -148,8 +157,11 @@ export async function serveMcp(opts: ServeOptions): Promise { { description: tool.description, inputSchema: tool.inputShape }, async (args: Record) => { await ready + const t0 = Date.now() try { const result = tool.handler(args) + const latencyMs = Date.now() - t0 + telemetry.record(tool.name, latencyMs, result.text.length) receipt.record(result.text, result.touchedFiles) if (spotlight && result.touchedFiles.length > 0) { const { focus, edges } = spotlightPayload(getState().graph, result.touchedFiles) @@ -158,6 +170,7 @@ export async function serveMcp(opts: ServeOptions): Promise { return { content: [{ type: 'text' as const, text: result.text }] } } catch (err) { const msg = err instanceof Error ? err.message : String(err) + telemetry.record(tool.name, Date.now() - t0, 0) return { content: [{ type: 'text' as const, text: `Error: ${msg}` }], isError: true } } }, @@ -178,6 +191,7 @@ export async function serveMcp(opts: ServeOptions): Promise { watcher?.close() spotlight?.close() indexer.close() + telemetry.close() if (state) { const summary = receipt.summary() if (summary) process.stderr.write(summary + '\n')