diff --git a/src/commands/scan.ts b/src/commands/scan.ts index 58bdfb2..c4ff152 100644 --- a/src/commands/scan.ts +++ b/src/commands/scan.ts @@ -5,12 +5,14 @@ import { scanForTargets } from "../discovery.js"; import { runTarget, } from "../index.js"; +import { buildEvent, recordEvent } from "../telemetry.js"; import { TOOL_VERSION } from "../version.js"; import { ANSI, LOGO, c, useColor } from "./helpers.js"; // ── Scan implementation ───────────────────────────────────────────────────── async function runScan(bin: string, configPath: string | undefined, invokeTools: boolean, securityCheck?: boolean): Promise { + const t0 = Date.now(); process.stdout.write(useColor() ? c(ANSI.cyan, LOGO) + ` ${c(ANSI.dim, `v${TOOL_VERSION}`)}\n\n` : LOGO + ` v${TOOL_VERSION}\n\n`); if (configPath) { @@ -153,6 +155,16 @@ async function runScan(bin: string, configPath: string | undefined, invokeTools: } process.stdout.write("\n"); + recordEvent(buildEvent("command_complete", "scan", "cli", { + serversScanned: results.length, + toolsFound: totalTools, + gateResult: failCount === 0 ? "pass" : "fail", + executionMs: Date.now() - t0, + securityFlag: securityCheck, + targetIds: results.map((r) => r.targetId), + installedServers: targets.map((t) => t.config.targetId), + })); + if (failCount > 0) { process.exitCode = 1; } diff --git a/src/commands/score.ts b/src/commands/score.ts index 605d2dc..cef2de6 100644 --- a/src/commands/score.ts +++ b/src/commands/score.ts @@ -8,6 +8,7 @@ import { writeRunArtifact, } from "../index.js"; import { defaultRunsDirectory } from "../storage.js"; +import { buildEvent, recordEvent } from "../telemetry.js"; import { ANSI, c, formatOutput, targetFromCommand, writeOutput } from "./helpers.js"; export function registerScoreCommands(program: Command): void { @@ -22,11 +23,22 @@ export function registerScoreCommands(program: Command): void { .option("--output ", "Write to file instead of stdout.") .option("--no-color", "Disable colored output.") .action(async (commandArgs: string[], options: { format: string; output?: string }) => { + const t0 = Date.now(); const target = targetFromCommand(commandArgs); process.stdout.write(`${c(ANSI.dim, "⟳")} Scoring ${c(ANSI.bold, target.targetId)}...\n\n`); const artifact = await runTarget(target, { invokeTools: true, securityCheck: true }); await writeRunArtifact(artifact, defaultRunsDirectory(process.cwd())); + const toolsCheck = artifact.checks.find(ch => ch.id === "tools"); + recordEvent(buildEvent("command_complete", "score", "cli", { + serversScanned: 1, + toolsFound: toolsCheck?.evidence[0]?.itemCount ?? 0, + gateResult: artifact.gate, + executionMs: Date.now() - t0, + securityFlag: true, + targetIds: [target.targetId], + })); + if (options.format !== "terminal") { const output = formatOutput(artifact, options.format as "json" | "junit" | "sarif" | "markdown" | "html" | "terminal"); await writeOutput(output, options.format, options.output); diff --git a/src/commands/test.ts b/src/commands/test.ts index e6afbcd..c14832c 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -5,6 +5,7 @@ import { writeRunArtifact, } from "../index.js"; import { defaultRunsDirectory } from "../storage.js"; +import { buildEvent, recordEvent } from "../telemetry.js"; import { ANSI, c, targetFromCommand } from "./helpers.js"; export function registerTestCommands(program: Command): void { @@ -16,6 +17,7 @@ export function registerTestCommands(program: Command): void { .option("--security", "Run security analysis on tool schemas.") .option("--no-color", "Disable colored output.") .action(async (commandArgs: string[], options: { security?: boolean }) => { + const t0 = Date.now(); const target = targetFromCommand(commandArgs); process.stdout.write(` ${c(ANSI.dim, "⟳")} Checking ${c(ANSI.bold, target.targetId)}...`); const artifact = await runTarget(target, { securityCheck: options.security }); @@ -39,6 +41,16 @@ export function registerTestCommands(program: Command): void { } process.stdout.write(`\n ${c(ANSI.dim, `Artifact: ${outPath}`)}\n\n`); + + recordEvent(buildEvent("command_complete", "test", "cli", { + serversScanned: 1, + toolsFound: toolCount, + gateResult: artifact.gate, + executionMs: Date.now() - t0, + securityFlag: options.security, + targetIds: [target.targetId], + })); + if (artifact.gate === "fail") { process.exitCode = 1; } diff --git a/src/telemetry.ts b/src/telemetry.ts index 22d8d37..5c80dd1 100644 --- a/src/telemetry.ts +++ b/src/telemetry.ts @@ -14,6 +14,17 @@ export interface TelemetryConfig { statsToken?: string; } +export interface TelemetryEnrichment { + ciProvider?: string; + serversScanned?: number; + toolsFound?: number; + gateResult?: string; + executionMs?: number; + securityFlag?: boolean; + targetIds?: string[]; + installedServers?: string[]; +} + export interface TelemetryEvent { event: string; version: string; @@ -24,6 +35,14 @@ export interface TelemetryEvent { isCI: boolean; ciName?: string | null; transport: "cli" | "mcp"; + ciProvider?: string; + serversScanned?: number; + toolsFound?: number; + gateResult?: string; + executionMs?: number; + securityFlag?: boolean; + targetIds?: string[]; + installedServers?: string[]; } // ── Constants ──────────────────────────────────────────────────────────────── @@ -148,12 +167,27 @@ export function recordEvent(event: TelemetryEvent): void { }); } +// ── CI provider detection ──────────────────────────────────────────────────── + +export function detectCiProvider(): string | undefined { + if (process.env["GITHUB_ACTIONS"]) return "github-actions"; + if (process.env["GITLAB_CI"]) return "gitlab-ci"; + if (process.env["CIRCLECI"]) return "circleci"; + if (process.env["JENKINS_URL"]) return "jenkins"; + if (process.env["BUILDKITE"]) return "buildkite"; + if (process.env["TRAVIS"]) return "travis"; + if (process.env["CODEBUILD_BUILD_ID"]) return "aws-codebuild"; + if (process.env["TF_BUILD"]) return "azure-pipelines"; + return undefined; +} + // ── Convenience: build event from current process state ────────────────────── export function buildEvent( event: string, command: string, transport: "cli" | "mcp", + enrichment?: TelemetryEnrichment, ): TelemetryEvent { const ci = detectCI(); return { @@ -166,5 +200,7 @@ export function buildEvent( isCI: ci.isCI, ciName: ci.ciName, transport, + ciProvider: enrichment?.ciProvider ?? detectCiProvider(), + ...enrichment, }; }