Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/commands/scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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) {
Expand Down Expand Up @@ -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;
}
Expand Down
12 changes: 12 additions & 0 deletions src/commands/score.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -22,11 +23,22 @@ export function registerScoreCommands(program: Command): void {
.option("--output <file>", "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);
Expand Down
12 changes: 12 additions & 0 deletions src/commands/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 });
Expand All @@ -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;
}
Expand Down
36 changes: 36 additions & 0 deletions src/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 ────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -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 {
Expand All @@ -166,5 +200,7 @@ export function buildEvent(
isCI: ci.isCI,
ciName: ci.ciName,
transport,
ciProvider: enrichment?.ciProvider ?? detectCiProvider(),
...enrichment,
};
}
Loading