-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtelemetry.ts
More file actions
112 lines (99 loc) · 3.74 KB
/
telemetry.ts
File metadata and controls
112 lines (99 loc) · 3.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import os from "os";
import { isTelemetryEnabled, loadTelemetryState } from "../config/telemetry.js";
import { detectPlatform } from "./platform.js";
import { getCurrentVersion } from "./self-update.js";
// ─── Aptabase configuration ──────────────────────────────────────────────────
// Aptabase is a privacy-first, open source analytics service.
// The full payload we send is documented below — search for "trackEvent" calls
// in the codebase to audit every event. Nothing here contains PII.
const APTABASE_APP_KEY = "A-EU-1060569594";
const APTABASE_ENDPOINT = "https://eu.aptabase.com/api/v0/event";
const TIMEOUT_MS = 3_000;
// ─── System properties (sent with every event) ──────────────────────────────
interface SystemProps {
isDebug: boolean;
locale: string;
osName: string;
osVersion: string;
appVersion: string;
engineName: string;
engineVersion: string;
sdkVersion: string;
}
function getOsName(): string {
switch (detectPlatform()) {
case "macos": return "macOS";
case "linux": return "Linux";
case "windows": return "Windows";
}
}
function getLocale(): string {
try {
// Aptabase limits locale to 10 characters — truncate extended subtags
const raw = Intl.DateTimeFormat().resolvedOptions().locale;
return raw.length <= 10 ? raw : raw.slice(0, 10);
} catch {
return process.env["LANG"]?.split(".")[0]?.slice(0, 10) ?? "unknown";
}
}
function getSystemProps(): SystemProps {
return {
isDebug: false,
locale: getLocale(),
osName: getOsName(),
osVersion: os.release(),
appVersion: getCurrentVersion(),
engineName: "node",
engineVersion: process.versions.node,
sdkVersion: `cc-router@${getCurrentVersion()}`,
};
}
// Session ID — use the installId directly so Aptabase always identifies the
// same machine as the same user. Aptabase limits sessionId to 36 characters;
// a standard UUID with dashes is exactly 36, so we pass it through unchanged.
function getSessionId(installId: string): string {
return installId.slice(0, 36);
}
// ─── Public API ──────────────────────────────────────────────────────────────
// Fire-and-forget: never throws, never blocks the caller. If telemetry is
// disabled (env var or opt-out) this is a synchronous no-op.
export async function trackEvent(
eventName: string,
props?: Record<string, string | number | boolean>,
): Promise<void> {
try {
if (!isTelemetryEnabled()) return;
const state = loadTelemetryState();
const body = {
timestamp: new Date().toISOString(),
sessionId: getSessionId(state.installId),
eventName,
systemProps: getSystemProps(),
props: props ?? {},
};
await fetch(APTABASE_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
"App-Key": APTABASE_APP_KEY,
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(TIMEOUT_MS),
});
} catch {
// Silently swallow — telemetry must never disrupt the proxy
}
}
// Start a heartbeat that fires every hour while the proxy is running.
// Uses .unref() so the timer does not prevent Node from exiting.
export function startHeartbeat(accountCount: number): void {
const startTime = Date.now();
const timer = setInterval(() => {
const uptimeMinutes = Math.floor((Date.now() - startTime) / 60_000);
trackEvent("proxy_heartbeat", {
uptime_minutes: uptimeMinutes,
account_count: accountCount,
});
}, 60 * 60 * 1000);
timer.unref();
}