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
2 changes: 1 addition & 1 deletion bin/brakit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { resolve } from "node:path";
import installCommand from "../src/cli/commands/install.js";
import uninstallCommand from "../src/cli/commands/uninstall.js";
import { trackEvent } from "../src/telemetry/index.js";
import { TELEMETRY_EVENT_CLI_INVOKED } from "../src/constants/config.js";
import { TELEMETRY_EVENT_CLI_INVOKED } from "../src/constants/telemetry.js";

const sub = process.argv[2];
const command = sub === "uninstall" ? "uninstall" : sub === "mcp" ? "mcp" : "install";
Expand Down
37 changes: 32 additions & 5 deletions sdks/python/brakit/_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import logging
import threading
import time
from typing import TYPE_CHECKING

from brakit.constants.events import (
CHANNEL_REQUEST_COMPLETED,
Expand All @@ -25,10 +26,15 @@
from brakit.types.http import TracedRequest
from brakit.types.telemetry import TelemetryEntry

if TYPE_CHECKING:
from brakit.core.registry import ServiceRegistry
from brakit.transport.forwarder import Forwarder

logger = logging.getLogger(LOGGER_NAME)

_init_lock = threading.Lock()
_initialized = False
_detected_stack: dict[str, object] = {"framework": "unknown", "adapters": []}


def _auto_setup() -> None:
Expand All @@ -47,13 +53,17 @@ def _auto_setup() -> None:
adapters = _install_adapters(registry)
logger.debug("adapters: %s", adapters)

_start_transport(registry)
detected_framework = _install_frameworks(registry)

# Initialize telemetry after framework detection
_detected_stack["framework"] = detected_framework
_detected_stack["adapters"] = adapters

from brakit._telemetry import init_session as _init_telemetry
_init_telemetry(framework=detected_framework, adapters=adapters)

# Start transport after stack is known so sdk.hello includes full info
_start_transport(registry)

logger.debug("initialized")


Expand Down Expand Up @@ -99,9 +109,6 @@ def _start_transport(registry: "ServiceRegistry") -> None:
_setup_forwarder(registry, port)
return

# Port file not found — the Node.js server may not have received its first
# request yet (the port file is written on first request, not on startup).
# Retry discovery in a background thread so we don't block import.
def _retry() -> None:
for _ in range(PORT_RETRY_COUNT):
time.sleep(PORT_RETRY_INTERVAL_S)
Expand All @@ -125,6 +132,8 @@ def _setup_forwarder(registry: "ServiceRegistry", port: int) -> None:
forwarder = Forwarder(port=port)
forwarder.start()

_send_hello(forwarder)

registry.bus.on(CHANNEL_REQUEST_COMPLETED, lambda r: _forward_request(forwarder, r))
registry.bus.on(CHANNEL_TELEMETRY_FETCH, lambda e: _forward_telemetry(forwarder, EVENT_TYPE_FETCH, e))
registry.bus.on(CHANNEL_TELEMETRY_LOG, lambda e: _forward_telemetry(forwarder, EVENT_TYPE_LOG, e))
Expand All @@ -140,6 +149,24 @@ def _install_frameworks(registry: "ServiceRegistry") -> str:
return detect_frameworks(registry)


def _send_hello(forwarder: "Forwarder") -> None:
import platform
import sys

event = SDKEvent(
type="sdk.hello", # type: ignore[arg-type]
timestamp=time.time() * 1_000,
data={
"framework": str(_detected_stack["framework"]),
"adapters": list(_detected_stack["adapters"]), # type: ignore[arg-type]
"pythonVersion": platform.python_version(),
"os": f"{sys.platform}-{platform.release()}",
"arch": platform.machine(),
},
)
forwarder.send(event)


def _forward_request(forwarder: "Forwarder", request: TracedRequest) -> None:
raw = dataclasses.asdict(request)
data = {_to_camel(k): v for k, v in raw.items()}
Expand Down
2 changes: 1 addition & 1 deletion sdks/python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "brakit"
version = "0.1.6"
version = "0.1.7"
description = "Zero-config observability for Python web frameworks"
readme = "README.md"
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/uninstall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
import { brakitDebug } from "../../utils/log.js";
import { getErrorMessage } from "../../utils/type-guards.js";
import { trackEvent } from "../../telemetry/index.js";
import { TELEMETRY_EVENT_CLI_UNINSTALL } from "../../constants/config.js";
import { TELEMETRY_EVENT_CLI_UNINSTALL } from "../../constants/telemetry.js";

/**
* Entry point files where brakit may have prepended an import line.
Expand Down
26 changes: 0 additions & 26 deletions src/constants/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,32 +135,6 @@ export const VALID_ISSUE_CATEGORIES = new Set<IssueCategory>(["security", "perfo
export const VALID_AI_FIX_STATUSES = new Set<AiFixStatus>(["fixed", "wont_fix"]);
export const VALID_SECURITY_SEVERITIES = new Set<SecuritySeverity>(["critical", "warning"]);

// ── Telemetry ──

export const TELEMETRY_EVENT_CLI_INVOKED = "cli_invoked" as const;
export const TELEMETRY_EVENT_CLI_UNINSTALL = "cli_uninstall" as const;
export const TELEMETRY_EVENT_SETUP_COMPLETED = "setup_completed" as const;
export const TELEMETRY_EVENT_FIRST_REQUEST = "first_request" as const;
export const TELEMETRY_EVENT_DASHBOARD_VIEWED = "dashboard_viewed" as const;
export const TELEMETRY_EVENT_SESSION = "session" as const;
export const TELEMETRY_EVENT_GRAPH_FEATURE = "graph_feature" as const;

export const EXIT_REASON_CLEAN = "clean" as const;
export const EXIT_REASON_SIGINT = "sigint" as const;
export const EXIT_REASON_SIGTERM = "sigterm" as const;
export const EXIT_REASON_UNKNOWN = "unknown" as const;

/** Max characters for SQL/query detail preview in insight cards. */
export const DETAIL_PREVIEW_LENGTH = 120;

/**
* Known dependency names to check for framework/ORM detection.
* Used for telemetry diagnostics — captures which deps exist in
* the user's package.json (names only, no versions or paths).
*/
export const KNOWN_DEPENDENCY_NAMES = [
"next", "@remix-run/dev", "nuxt", "vite", "astro",
"@nestjs/core", "@adonisjs/core", "sails",
"express", "fastify", "hono", "koa", "@hapi/hapi",
"prisma", "drizzle-orm", "typeorm", "sequelize",
] as const;
74 changes: 74 additions & 0 deletions src/constants/detection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { HookName } from "../types/detection.js";

/** Segment used to identify node_modules paths in require.cache keys. */
export const NODE_MODULES_SEGMENT = "/node_modules/";

/** Hooks installed during Brakit setup. */
export const INSTALLED_HOOKS: readonly HookName[] = ["fetch", "console", "error"];

/**
* Known public package names for stack detection.
* Matched against package.json dependencies and require.cache at runtime.
* Only public npm package names — no versions, paths, or private scopes.
*/
export const KNOWN_DEPENDENCY_NAMES = [
// -- Frameworks (meta) --
"next", "@remix-run/dev", "nuxt", "astro",
// -- Frameworks (backend) --
"@nestjs/core", "@adonisjs/core", "sails",
"express", "fastify", "hono", "koa", "@hapi/hapi",
"elysia", "h3", "nitro", "@trpc/server",
// -- Bundlers --
"vite",
// -- ORM / query builders --
"prisma", "@prisma/client", "drizzle-orm", "typeorm", "sequelize",
"mongoose", "kysely", "knex", "@mikro-orm/core", "objection",
// -- DB drivers --
"pg", "mysql2", "mongodb", "better-sqlite3",
"@libsql/client", "@planetscale/database",
"ioredis", "redis",
// -- Auth --
"lucia", "next-auth", "@auth/core", "passport",
// -- Queues / messaging --
"bullmq", "amqplib", "kafkajs",
// -- Validation --
"zod", "joi", "yup", "arktype", "valibot",
// -- HTTP clients --
"axios", "got", "ky", "undici",
// -- Realtime --
"socket.io", "ws",
// -- CSS / styling --
"tailwindcss",
// -- Testing --
"vitest", "jest", "mocha",
// -- Runtime indicators --
"bun-types", "@types/bun",
] as const;

/**
* Config file paths → tool labels for stack detection.
* Detection checks file existence only (existsSync), never reads contents.
*/
export const KNOWN_CONFIG_FILES = {
"next.config.js": "nextjs",
"next.config.mjs": "nextjs",
"next.config.ts": "nextjs",
"nuxt.config.ts": "nuxt",
"nuxt.config.js": "nuxt",
"astro.config.mjs": "astro",
"astro.config.ts": "astro",
"vite.config.ts": "vite",
"vite.config.js": "vite",
"drizzle.config.ts": "drizzle-orm",
"drizzle.config.js": "drizzle-orm",
"prisma/schema.prisma": "prisma",
"knexfile.js": "knex",
"knexfile.ts": "knex",
"mikro-orm.config.ts": "@mikro-orm/core",
"nest-cli.json": "@nestjs/core",
"tailwind.config.js": "tailwindcss",
"tailwind.config.ts": "tailwindcss",
} as const;

/** Pre-built Set for O(1) lookups during require.cache scanning. */
export const KNOWN_DEPENDENCY_SET: ReadonlySet<string> = new Set(KNOWN_DEPENDENCY_NAMES);
2 changes: 2 additions & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from "./config.js";
export * from "./detection.js";
export * from "./labels.js";
export * from "./features.js";
export * from "./telemetry.js";
21 changes: 7 additions & 14 deletions src/constants/labels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,21 +100,8 @@ export const SDK_EVENT_FETCH = "fetch" as const;
export const SDK_EVENT_LOG = "log" as const;
export const SDK_EVENT_ERROR = "error" as const;
export const SDK_EVENT_AUTH_CHECK = "auth.check" as const;
export const SDK_EVENT_HELLO = "sdk.hello" as const;

// ── Telemetry ──

export const POSTHOG_HOST = "https://us.i.posthog.com";
export const POSTHOG_CAPTURE_PATH = "/i/v0/e/";
export const POSTHOG_REQUEST_TIMEOUT_MS = 3_000;
export const POSTHOG_SPAWN_TIMEOUT_MS = 5_000;
export const SIGNAL_EXIT_SIGINT = 130;
export const SIGNAL_EXIT_SIGTERM = 143;

/**
* Thresholds (in ms) for categorizing endpoint response times
* into human-readable buckets for telemetry reporting.
*/
export const SPEED_BUCKET_THRESHOLDS = [200, 500, 1_000, 2_000, 5_000] as const;

// ── Timeline ──

Expand All @@ -124,3 +111,9 @@ export const TIMELINE_FETCH = "fetch" as const;
export const TIMELINE_LOG = "log" as const;
export const TIMELINE_ERROR = "error" as const;
export const TIMELINE_QUERY = "query" as const;

// ── Unicode symbols ──

export const UNICODE_ARROW = "\u2192";
export const UNICODE_EM_DASH = "\u2014";
export const UNICODE_CHECK_MARK = "\u2713";
37 changes: 37 additions & 0 deletions src/constants/telemetry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// ── PostHog transport ──

export const POSTHOG_HOST = "https://us.i.posthog.com";
export const POSTHOG_CAPTURE_PATH = "/i/v0/e/";
export const POSTHOG_REQUEST_TIMEOUT_MS = 3_000;

// ── Telemetry event names ──

export const TELEMETRY_EVENT_CLI_INVOKED = "cli_invoked" as const;
export const TELEMETRY_EVENT_CLI_UNINSTALL = "cli_uninstall" as const;
export const TELEMETRY_EVENT_SETUP_COMPLETED = "setup_completed" as const;
export const TELEMETRY_EVENT_FIRST_REQUEST = "first_request" as const;
export const TELEMETRY_EVENT_DASHBOARD_VIEWED = "dashboard_viewed" as const;
export const TELEMETRY_EVENT_SESSION = "session" as const;
export const TELEMETRY_EVENT_GRAPH_FEATURE = "graph_feature" as const;

export const TELEMETRY_SDK_NAME = "node" as const;

// ── Exit reasons ──

export const EXIT_REASON_CLEAN = "clean" as const;
export const EXIT_REASON_SIGINT = "sigint" as const;
export const EXIT_REASON_SIGTERM = "sigterm" as const;
export const EXIT_REASON_UNKNOWN = "unknown" as const;

// ── Signal codes ──

export const SIGNAL_EXIT_SIGINT = 130;
export const SIGNAL_EXIT_SIGTERM = 143;

// ── Speed buckets ──

/**
* Thresholds (in ms) for categorizing endpoint response times
* into human-readable buckets for telemetry reporting.
*/
export const SPEED_BUCKET_THRESHOLDS = [200, 500, 1_000, 2_000, 5_000] as const;
11 changes: 11 additions & 0 deletions src/dashboard/api/ingest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { MAX_INGEST_BYTES } from "../../constants/config.js";
import { HTTP_NO_CONTENT, HTTP_BAD_REQUEST, HTTP_METHOD_NOT_ALLOWED, HTTP_PAYLOAD_TOO_LARGE, TIMELINE_FETCH, TIMELINE_LOG, TIMELINE_ERROR, TIMELINE_QUERY } from "../../constants/labels.js";
import { sendJson } from "./shared.js";
import { routeSDKEvent } from "./sdk-event-parser.js";
import { SDK_EVENT_HELLO } from "../../constants/labels.js";
import { session } from "../../telemetry/index.js";

function isBrakitBatch(msg: unknown): msg is TelemetryBatch {
return (
Expand Down Expand Up @@ -81,6 +83,15 @@ export function createIngestHandler(

if (isSDKPayload(body)) {
for (const event of body.events) {
if (event.type === SDK_EVENT_HELLO) {
const d = event.data as Record<string, unknown>;
session.recordPythonStack({
framework: String(d.framework ?? "unknown"),
adapters: Array.isArray(d.adapters) ? d.adapters.map(String) : [],
pythonVersion: String(d.pythonVersion ?? "unknown"),
});
continue;
}
routeSDKEvent(event, stores);
}
res.writeHead(HTTP_NO_CONTENT);
Expand Down
32 changes: 28 additions & 4 deletions src/dashboard/client/views/insights-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,12 @@ export class InsightsView extends BkViewBase {
`)}
</div>

${this.renderSummaryBar(nonStale)}

<div class="insights-list">
${(open.length + regressed.length) > 0 ? html`
<div class="insights-ai-hint">Copy to your AI: <code>"Fix brakit findings"</code></div>
` : nothing}
${open.length === 0 && regressed.length === 0 && verifying.length === 0 && resolved.length === 0 && dismissed.length === 0
? html`<div class="insights-empty"><span class="insights-empty-icon">\u2713</span>${this.filter === "all" ? "All clear \u2014 no issues detected" : `No ${this.filter} issues`}</div>`
: nothing}
Expand Down Expand Up @@ -126,6 +131,23 @@ export class InsightsView extends BkViewBase {
`;
}

private renderSummaryBar(nonStale: StatefulIssue[]) {
const active = nonStale.filter((e) => (e.state === "open" || e.state === "regressed") && e.aiStatus !== "wont_fix");
const critical = active.filter((e) => e.issue.severity === "critical").length;
const warning = active.filter((e) => e.issue.severity === "warning").length;
const resolved = nonStale.filter((e) => e.state === "resolved").length;

if (critical === 0 && warning === 0 && resolved === 0) return nothing;

return html`
<div class="insights-summary">
${critical > 0 ? html`<span class="insights-summary-stat critical">${critical} critical</span>` : nothing}
${warning > 0 ? html`<span class="insights-summary-stat warning">${warning} warning</span>` : nothing}
${resolved > 0 ? html`<span class="insights-summary-stat resolved">${resolved} resolved</span>` : nothing}
</div>
`;
}

private renderIssueCard(entry: StatefulIssue, idx: number) {
const issue = entry.issue;
const severityConfig = SEVERITY_MAP[issue.severity] || SEVERITY_MAP["info"];
Expand All @@ -150,11 +172,13 @@ export class InsightsView extends BkViewBase {
${isResolved ? html`<span class="insights-badge-resolved">resolved</span>` : nothing}
</div>
<div class="insights-card-desc">${issue.desc}</div>
${issue.detail ? html`<div class="insights-card-detail">${issue.detail}</div>` : nothing}
${entry.cleanHitsSinceLastSeen > 0 ? html`
<div class="insights-card-progress">${entry.cleanHitsSinceLastSeen}/${CLEAN_HITS_FOR_RESOLUTION} clean requests</div>
${isExpanded ? html`
${issue.detail ? html`<div class="insights-card-detail">${issue.detail}</div>` : nothing}
${entry.cleanHitsSinceLastSeen > 0 ? html`
<div class="insights-card-progress">${entry.cleanHitsSinceLastSeen}/${CLEAN_HITS_FOR_RESOLUTION} clean requests</div>
` : nothing}
${issue.hint ? html`<div class="insights-card-hint">${issue.hint}</div>` : nothing}
` : nothing}
${isExpanded && issue.hint ? html`<div class="insights-card-hint">${issue.hint}</div>` : nothing}
</div>
${issue.hint ? html`<span class="insights-card-arrow">${isExpanded ? "\u2193" : "\u2192"}</span>` : nothing}
</div>
Expand Down
Loading
Loading