Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

observability.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

observability.tsBlame134 lines · 1 contributor
80bed05Claude1/**
2 * Minimal, dependency-free observability layer.
3 *
4 * - `reportError` NEVER throws. Production paths must not break when a webhook
5 * is misconfigured or unreachable.
6 * - Always logs to stderr with a `[error]` prefix.
7 * - Optionally fans out to:
8 * - `ERROR_WEBHOOK_URL` — generic JSON POST (fire-and-forget)
9 * - `SENTRY_DSN` — Sentry envelope endpoint (fire-and-forget)
10 *
11 * No SDKs. Only `fetch` (built into Bun).
12 */
13
14interface SentryFrame {
15 function?: string;
16 filename?: string;
17 lineno?: number;
18 colno?: number;
19}
20
21function toErr(err: unknown): { message: string; stack?: string; type: string } {
22 if (err instanceof Error) {
23 return { message: err.message, stack: err.stack, type: err.name || "Error" };
24 }
25 try {
26 return { message: typeof err === "string" ? err : JSON.stringify(err), type: "NonError" };
27 } catch {
28 return { message: String(err), type: "NonError" };
29 }
30}
31
32function parseStack(stack?: string): SentryFrame[] {
33 if (!stack) return [];
34 const frames: SentryFrame[] = [];
35 for (const raw of stack.split("\n").slice(1)) {
36 const m = raw.trim().match(/^at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?$/);
37 if (!m) continue;
38 frames.push({
39 function: m[1] || undefined,
40 filename: m[2],
41 lineno: Number(m[3]),
42 colno: Number(m[4]),
43 });
44 }
45 return frames.reverse();
46}
47
48function randomEventId(): string {
49 const b = new Uint8Array(16);
50 crypto.getRandomValues(b);
51 return Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
52}
53
54function parseDsn(dsn: string): { url: string; headers: Record<string, string> } | null {
55 try {
56 const u = new URL(dsn);
57 const key = u.username;
58 const projectId = u.pathname.replace(/^\/+/, "");
59 if (!key || !projectId) return null;
60 return {
61 url: `${u.protocol}//${u.host}/api/${projectId}/envelope/`,
62 headers: {
63 "Content-Type": "application/x-sentry-envelope",
64 "X-Sentry-Auth": `Sentry sentry_version=7, sentry_key=${key}, sentry_client=gluecron/1.0`,
65 },
66 };
67 } catch {
68 return null;
69 }
70}
71
72function safeLog(msg: string, e: unknown): void {
73 try {
74 console.error(msg, e);
75 } catch {
76 /* ignore */
77 }
78}
79
80function fireAndForget(
81 url: string,
82 init: RequestInit,
83 label: string
84): void {
85 try {
86 void fetch(url, init).catch((e) => safeLog(`[error] ${label} failed:`, e));
87 } catch (e) {
88 safeLog(`[error] ${label} threw:`, e);
89 }
90}
91
92export function reportError(err: unknown, context?: Record<string, unknown>): void {
93 const { message, stack, type } = toErr(err);
94 const timestamp = new Date().toISOString();
95
96 safeLog("[error]", err);
97
98 const webhookUrl = process.env.ERROR_WEBHOOK_URL;
99 if (webhookUrl) {
100 fireAndForget(
101 webhookUrl,
102 {
103 method: "POST",
104 headers: { "Content-Type": "application/json" },
105 body: JSON.stringify({ timestamp, message, stack, context, env: process.env.NODE_ENV }),
106 },
107 "observability webhook"
108 );
109 }
110
111 const dsn = process.env.SENTRY_DSN;
112 if (dsn) {
113 const parsed = parseDsn(dsn);
114 if (!parsed) return;
115 const eventId = randomEventId();
116 const event = {
117 event_id: eventId,
118 timestamp,
119 platform: "node",
120 level: "error",
121 message,
122 exception: {
123 values: [{ type, value: message, stacktrace: { frames: parseStack(stack) } }],
124 },
125 extra: context,
126 environment: process.env.NODE_ENV,
127 };
128 const body =
129 `${JSON.stringify({ event_id: eventId, sent_at: timestamp })}\n` +
130 `${JSON.stringify({ type: "event" })}\n` +
131 `${JSON.stringify(event)}\n`;
132 fireAndForget(parsed.url, { method: "POST", headers: parsed.headers, body }, "sentry report");
133 }
134}