Skip to content

Commit 5a9d11f

Browse files
authored
Update telemetry to capture details that tells what failed when user used brakit (#48)
1 parent 987f744 commit 5a9d11f

25 files changed

Lines changed: 717 additions & 455 deletions

File tree

bin/brakit.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { resolve } from "node:path";
44
import installCommand from "../src/cli/commands/install.js";
55
import uninstallCommand from "../src/cli/commands/uninstall.js";
66
import { trackEvent } from "../src/telemetry/index.js";
7-
import { TELEMETRY_EVENT_CLI_INVOKED } from "../src/constants/config.js";
7+
import { TELEMETRY_EVENT_CLI_INVOKED } from "../src/constants/telemetry.js";
88

99
const sub = process.argv[2];
1010
const command = sub === "uninstall" ? "uninstall" : sub === "mcp" ? "mcp" : "install";

sdks/python/brakit/_setup.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import logging
66
import threading
77
import time
8+
from typing import TYPE_CHECKING
89

910
from brakit.constants.events import (
1011
CHANNEL_REQUEST_COMPLETED,
@@ -25,10 +26,15 @@
2526
from brakit.types.http import TracedRequest
2627
from brakit.types.telemetry import TelemetryEntry
2728

29+
if TYPE_CHECKING:
30+
from brakit.core.registry import ServiceRegistry
31+
from brakit.transport.forwarder import Forwarder
32+
2833
logger = logging.getLogger(LOGGER_NAME)
2934

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

3339

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

50-
_start_transport(registry)
5156
detected_framework = _install_frameworks(registry)
5257

53-
# Initialize telemetry after framework detection
58+
_detected_stack["framework"] = detected_framework
59+
_detected_stack["adapters"] = adapters
60+
5461
from brakit._telemetry import init_session as _init_telemetry
5562
_init_telemetry(framework=detected_framework, adapters=adapters)
5663

64+
# Start transport after stack is known so sdk.hello includes full info
65+
_start_transport(registry)
66+
5767
logger.debug("initialized")
5868

5969

@@ -99,9 +109,6 @@ def _start_transport(registry: "ServiceRegistry") -> None:
99109
_setup_forwarder(registry, port)
100110
return
101111

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

135+
_send_hello(forwarder)
136+
128137
registry.bus.on(CHANNEL_REQUEST_COMPLETED, lambda r: _forward_request(forwarder, r))
129138
registry.bus.on(CHANNEL_TELEMETRY_FETCH, lambda e: _forward_telemetry(forwarder, EVENT_TYPE_FETCH, e))
130139
registry.bus.on(CHANNEL_TELEMETRY_LOG, lambda e: _forward_telemetry(forwarder, EVENT_TYPE_LOG, e))
@@ -140,6 +149,24 @@ def _install_frameworks(registry: "ServiceRegistry") -> str:
140149
return detect_frameworks(registry)
141150

142151

152+
def _send_hello(forwarder: "Forwarder") -> None:
153+
import platform
154+
import sys
155+
156+
event = SDKEvent(
157+
type="sdk.hello", # type: ignore[arg-type]
158+
timestamp=time.time() * 1_000,
159+
data={
160+
"framework": str(_detected_stack["framework"]),
161+
"adapters": list(_detected_stack["adapters"]), # type: ignore[arg-type]
162+
"pythonVersion": platform.python_version(),
163+
"os": f"{sys.platform}-{platform.release()}",
164+
"arch": platform.machine(),
165+
},
166+
)
167+
forwarder.send(event)
168+
169+
143170
def _forward_request(forwarder: "Forwarder", request: TracedRequest) -> None:
144171
raw = dataclasses.asdict(request)
145172
data = {_to_camel(k): v for k, v in raw.items()}

sdks/python/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "brakit"
7-
version = "0.1.6"
7+
version = "0.1.7"
88
description = "Zero-config observability for Python web frameworks"
99
readme = "README.md"
1010
license = "MIT"

src/cli/commands/uninstall.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
import { brakitDebug } from "../../utils/log.js";
2525
import { getErrorMessage } from "../../utils/type-guards.js";
2626
import { trackEvent } from "../../telemetry/index.js";
27-
import { TELEMETRY_EVENT_CLI_UNINSTALL } from "../../constants/config.js";
27+
import { TELEMETRY_EVENT_CLI_UNINSTALL } from "../../constants/telemetry.js";
2828

2929
/**
3030
* Entry point files where brakit may have prepended an import line.

src/constants/config.ts

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -135,32 +135,6 @@ export const VALID_ISSUE_CATEGORIES = new Set<IssueCategory>(["security", "perfo
135135
export const VALID_AI_FIX_STATUSES = new Set<AiFixStatus>(["fixed", "wont_fix"]);
136136
export const VALID_SECURITY_SEVERITIES = new Set<SecuritySeverity>(["critical", "warning"]);
137137

138-
// ── Telemetry ──
139-
140-
export const TELEMETRY_EVENT_CLI_INVOKED = "cli_invoked" as const;
141-
export const TELEMETRY_EVENT_CLI_UNINSTALL = "cli_uninstall" as const;
142-
export const TELEMETRY_EVENT_SETUP_COMPLETED = "setup_completed" as const;
143-
export const TELEMETRY_EVENT_FIRST_REQUEST = "first_request" as const;
144-
export const TELEMETRY_EVENT_DASHBOARD_VIEWED = "dashboard_viewed" as const;
145-
export const TELEMETRY_EVENT_SESSION = "session" as const;
146-
export const TELEMETRY_EVENT_GRAPH_FEATURE = "graph_feature" as const;
147-
148-
export const EXIT_REASON_CLEAN = "clean" as const;
149-
export const EXIT_REASON_SIGINT = "sigint" as const;
150-
export const EXIT_REASON_SIGTERM = "sigterm" as const;
151-
export const EXIT_REASON_UNKNOWN = "unknown" as const;
152138

153139
/** Max characters for SQL/query detail preview in insight cards. */
154140
export const DETAIL_PREVIEW_LENGTH = 120;
155-
156-
/**
157-
* Known dependency names to check for framework/ORM detection.
158-
* Used for telemetry diagnostics — captures which deps exist in
159-
* the user's package.json (names only, no versions or paths).
160-
*/
161-
export const KNOWN_DEPENDENCY_NAMES = [
162-
"next", "@remix-run/dev", "nuxt", "vite", "astro",
163-
"@nestjs/core", "@adonisjs/core", "sails",
164-
"express", "fastify", "hono", "koa", "@hapi/hapi",
165-
"prisma", "drizzle-orm", "typeorm", "sequelize",
166-
] as const;

src/constants/detection.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import type { HookName } from "../types/detection.js";
2+
3+
/** Segment used to identify node_modules paths in require.cache keys. */
4+
export const NODE_MODULES_SEGMENT = "/node_modules/";
5+
6+
/** Hooks installed during Brakit setup. */
7+
export const INSTALLED_HOOKS: readonly HookName[] = ["fetch", "console", "error"];
8+
9+
/**
10+
* Known public package names for stack detection.
11+
* Matched against package.json dependencies and require.cache at runtime.
12+
* Only public npm package names — no versions, paths, or private scopes.
13+
*/
14+
export const KNOWN_DEPENDENCY_NAMES = [
15+
// -- Frameworks (meta) --
16+
"next", "@remix-run/dev", "nuxt", "astro",
17+
// -- Frameworks (backend) --
18+
"@nestjs/core", "@adonisjs/core", "sails",
19+
"express", "fastify", "hono", "koa", "@hapi/hapi",
20+
"elysia", "h3", "nitro", "@trpc/server",
21+
// -- Bundlers --
22+
"vite",
23+
// -- ORM / query builders --
24+
"prisma", "@prisma/client", "drizzle-orm", "typeorm", "sequelize",
25+
"mongoose", "kysely", "knex", "@mikro-orm/core", "objection",
26+
// -- DB drivers --
27+
"pg", "mysql2", "mongodb", "better-sqlite3",
28+
"@libsql/client", "@planetscale/database",
29+
"ioredis", "redis",
30+
// -- Auth --
31+
"lucia", "next-auth", "@auth/core", "passport",
32+
// -- Queues / messaging --
33+
"bullmq", "amqplib", "kafkajs",
34+
// -- Validation --
35+
"zod", "joi", "yup", "arktype", "valibot",
36+
// -- HTTP clients --
37+
"axios", "got", "ky", "undici",
38+
// -- Realtime --
39+
"socket.io", "ws",
40+
// -- CSS / styling --
41+
"tailwindcss",
42+
// -- Testing --
43+
"vitest", "jest", "mocha",
44+
// -- Runtime indicators --
45+
"bun-types", "@types/bun",
46+
] as const;
47+
48+
/**
49+
* Config file paths → tool labels for stack detection.
50+
* Detection checks file existence only (existsSync), never reads contents.
51+
*/
52+
export const KNOWN_CONFIG_FILES = {
53+
"next.config.js": "nextjs",
54+
"next.config.mjs": "nextjs",
55+
"next.config.ts": "nextjs",
56+
"nuxt.config.ts": "nuxt",
57+
"nuxt.config.js": "nuxt",
58+
"astro.config.mjs": "astro",
59+
"astro.config.ts": "astro",
60+
"vite.config.ts": "vite",
61+
"vite.config.js": "vite",
62+
"drizzle.config.ts": "drizzle-orm",
63+
"drizzle.config.js": "drizzle-orm",
64+
"prisma/schema.prisma": "prisma",
65+
"knexfile.js": "knex",
66+
"knexfile.ts": "knex",
67+
"mikro-orm.config.ts": "@mikro-orm/core",
68+
"nest-cli.json": "@nestjs/core",
69+
"tailwind.config.js": "tailwindcss",
70+
"tailwind.config.ts": "tailwindcss",
71+
} as const;
72+
73+
/** Pre-built Set for O(1) lookups during require.cache scanning. */
74+
export const KNOWN_DEPENDENCY_SET: ReadonlySet<string> = new Set(KNOWN_DEPENDENCY_NAMES);

src/constants/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export * from "./config.js";
2+
export * from "./detection.js";
23
export * from "./labels.js";
34
export * from "./features.js";
5+
export * from "./telemetry.js";

src/constants/labels.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -100,21 +100,8 @@ export const SDK_EVENT_FETCH = "fetch" as const;
100100
export const SDK_EVENT_LOG = "log" as const;
101101
export const SDK_EVENT_ERROR = "error" as const;
102102
export const SDK_EVENT_AUTH_CHECK = "auth.check" as const;
103+
export const SDK_EVENT_HELLO = "sdk.hello" as const;
103104

104-
// ── Telemetry ──
105-
106-
export const POSTHOG_HOST = "https://us.i.posthog.com";
107-
export const POSTHOG_CAPTURE_PATH = "/i/v0/e/";
108-
export const POSTHOG_REQUEST_TIMEOUT_MS = 3_000;
109-
export const POSTHOG_SPAWN_TIMEOUT_MS = 5_000;
110-
export const SIGNAL_EXIT_SIGINT = 130;
111-
export const SIGNAL_EXIT_SIGTERM = 143;
112-
113-
/**
114-
* Thresholds (in ms) for categorizing endpoint response times
115-
* into human-readable buckets for telemetry reporting.
116-
*/
117-
export const SPEED_BUCKET_THRESHOLDS = [200, 500, 1_000, 2_000, 5_000] as const;
118105

119106
// ── Timeline ──
120107

@@ -124,3 +111,9 @@ export const TIMELINE_FETCH = "fetch" as const;
124111
export const TIMELINE_LOG = "log" as const;
125112
export const TIMELINE_ERROR = "error" as const;
126113
export const TIMELINE_QUERY = "query" as const;
114+
115+
// ── Unicode symbols ──
116+
117+
export const UNICODE_ARROW = "\u2192";
118+
export const UNICODE_EM_DASH = "\u2014";
119+
export const UNICODE_CHECK_MARK = "\u2713";

src/constants/telemetry.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// ── PostHog transport ──
2+
3+
export const POSTHOG_HOST = "https://us.i.posthog.com";
4+
export const POSTHOG_CAPTURE_PATH = "/i/v0/e/";
5+
export const POSTHOG_REQUEST_TIMEOUT_MS = 3_000;
6+
7+
// ── Telemetry event names ──
8+
9+
export const TELEMETRY_EVENT_CLI_INVOKED = "cli_invoked" as const;
10+
export const TELEMETRY_EVENT_CLI_UNINSTALL = "cli_uninstall" as const;
11+
export const TELEMETRY_EVENT_SETUP_COMPLETED = "setup_completed" as const;
12+
export const TELEMETRY_EVENT_FIRST_REQUEST = "first_request" as const;
13+
export const TELEMETRY_EVENT_DASHBOARD_VIEWED = "dashboard_viewed" as const;
14+
export const TELEMETRY_EVENT_SESSION = "session" as const;
15+
export const TELEMETRY_EVENT_GRAPH_FEATURE = "graph_feature" as const;
16+
17+
export const TELEMETRY_SDK_NAME = "node" as const;
18+
19+
// ── Exit reasons ──
20+
21+
export const EXIT_REASON_CLEAN = "clean" as const;
22+
export const EXIT_REASON_SIGINT = "sigint" as const;
23+
export const EXIT_REASON_SIGTERM = "sigterm" as const;
24+
export const EXIT_REASON_UNKNOWN = "unknown" as const;
25+
26+
// ── Signal codes ──
27+
28+
export const SIGNAL_EXIT_SIGINT = 130;
29+
export const SIGNAL_EXIT_SIGTERM = 143;
30+
31+
// ── Speed buckets ──
32+
33+
/**
34+
* Thresholds (in ms) for categorizing endpoint response times
35+
* into human-readable buckets for telemetry reporting.
36+
*/
37+
export const SPEED_BUCKET_THRESHOLDS = [200, 500, 1_000, 2_000, 5_000] as const;

src/dashboard/api/ingest.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { MAX_INGEST_BYTES } from "../../constants/config.js";
66
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";
77
import { sendJson } from "./shared.js";
88
import { routeSDKEvent } from "./sdk-event-parser.js";
9+
import { SDK_EVENT_HELLO } from "../../constants/labels.js";
10+
import { session } from "../../telemetry/index.js";
911

1012
function isBrakitBatch(msg: unknown): msg is TelemetryBatch {
1113
return (
@@ -81,6 +83,15 @@ export function createIngestHandler(
8183

8284
if (isSDKPayload(body)) {
8385
for (const event of body.events) {
86+
if (event.type === SDK_EVENT_HELLO) {
87+
const d = event.data as Record<string, unknown>;
88+
session.recordPythonStack({
89+
framework: String(d.framework ?? "unknown"),
90+
adapters: Array.isArray(d.adapters) ? d.adapters.map(String) : [],
91+
pythonVersion: String(d.pythonVersion ?? "unknown"),
92+
});
93+
continue;
94+
}
8495
routeSDKEvent(event, stores);
8596
}
8697
res.writeHead(HTTP_NO_CONTENT);

0 commit comments

Comments
 (0)