ai-cc-router 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -1
- package/dist/cli/cmd-service.js +4 -0
- package/dist/cli/cmd-setup.js +3 -0
- package/dist/cli/cmd-start.js +4 -0
- package/dist/cli/cmd-telemetry.js +60 -0
- package/dist/cli/index.js +2 -0
- package/dist/config/paths.js +3 -0
- package/dist/config/telemetry.js +63 -0
- package/dist/proxy/server.js +22 -0
- package/dist/utils/telemetry.js +117 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -513,12 +513,47 @@ Press `q` to quit. Run with `--json` for non-interactive output.
|
|
|
513
513
|
- The file is excluded by `.gitignore`
|
|
514
514
|
- Writes are atomic (write to `.tmp`, then rename) — no corruption on crash
|
|
515
515
|
- Keychain reads use `execFile` with a fixed argument array — no shell injection
|
|
516
|
-
-
|
|
516
|
+
- Anonymous opt-out telemetry via [Aptabase](https://aptabase.com) (see [Telemetry](#telemetry) below)
|
|
517
517
|
|
|
518
518
|
See [docs/security.md](docs/security.md) for details.
|
|
519
519
|
|
|
520
520
|
---
|
|
521
521
|
|
|
522
|
+
## Telemetry
|
|
523
|
+
|
|
524
|
+
CC-Router sends a handful of anonymous lifecycle events to [Aptabase](https://aptabase.com) (privacy-first, open source, EU-hosted). The goal is simple: know how many people use the project, which versions are live, and roughly how many instances are running — so we can prioritize fixes and features.
|
|
525
|
+
|
|
526
|
+
**What we send** — the entire payload lives in [`src/utils/telemetry.ts`](src/utils/telemetry.ts), audit it yourself:
|
|
527
|
+
|
|
528
|
+
| Event | When | Custom props |
|
|
529
|
+
| -------------------- | ------------------------------------------------ | ---------------------------------------- |
|
|
530
|
+
| `app_started` | First proxy start after install | `first_run: true` |
|
|
531
|
+
| `setup_completed` | Setup wizard finishes successfully | `account_count` |
|
|
532
|
+
| `proxy_started` | Each `cc-router start` | `account_count`, `mode` |
|
|
533
|
+
| `proxy_heartbeat` | Every 6h while the proxy is running | `uptime_hours`, `account_count` |
|
|
534
|
+
| `telemetry_disabled` | When you run `cc-router telemetry off` | — |
|
|
535
|
+
|
|
536
|
+
Plus anonymous system props with every event: `appVersion`, `osName` (macOS/Linux/Windows), `osVersion`, `locale`, `engineVersion` (Node), and an anonymous `installId` (random UUID generated on first run, stored in `~/.cc-router/telemetry.json`).
|
|
537
|
+
|
|
538
|
+
**What we never send**: IPs, OAuth tokens, account names, request content, prompts, responses, URLs, hostnames, usernames, file paths — nothing that could identify you or your usage patterns.
|
|
539
|
+
|
|
540
|
+
**Disable it** — three ways, any one works:
|
|
541
|
+
|
|
542
|
+
```bash
|
|
543
|
+
# 1. Persistent opt-out (recommended)
|
|
544
|
+
cc-router telemetry off
|
|
545
|
+
|
|
546
|
+
# 2. Respect the de-facto standard (honored by many OSS tools)
|
|
547
|
+
export DO_NOT_TRACK=1
|
|
548
|
+
|
|
549
|
+
# 3. Project-specific override
|
|
550
|
+
export CC_ROUTER_TELEMETRY=0
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
Check status anytime: `cc-router telemetry status`.
|
|
554
|
+
|
|
555
|
+
---
|
|
556
|
+
|
|
522
557
|
## Disclaimer
|
|
523
558
|
|
|
524
559
|
> CC-Router uses the OAuth tokens of your own Claude Max subscriptions.
|
package/dist/cli/cmd-service.js
CHANGED
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from "url";
|
|
|
4
4
|
import { join, dirname } from "path";
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import { detectPlatform } from "../utils/platform.js";
|
|
7
|
+
import { showTelemetryDisclosureIfNeeded } from "../utils/telemetry.js";
|
|
7
8
|
const execFileAsync = promisify(execFile);
|
|
8
9
|
// Resolve the path to the compiled CLI entry point
|
|
9
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -18,6 +19,9 @@ export function registerService(program) {
|
|
|
18
19
|
.description("Register cc-router to start automatically on system boot")
|
|
19
20
|
.action(async () => {
|
|
20
21
|
console.log(chalk.cyan("\nInstalling cc-router as a system service...\n"));
|
|
22
|
+
// Show telemetry disclosure once before the service takes over — after
|
|
23
|
+
// this point output goes to PM2 logs, not the user's terminal.
|
|
24
|
+
showTelemetryDisclosureIfNeeded();
|
|
21
25
|
// 1. Verify PM2 is installed
|
|
22
26
|
const pm2Version = await getPm2Version();
|
|
23
27
|
if (!pm2Version) {
|
package/dist/cli/cmd-setup.js
CHANGED
|
@@ -13,6 +13,7 @@ import { DEFAULT_RATE_LIMITS } from "../proxy/types.js";
|
|
|
13
13
|
import { existsSync } from "fs";
|
|
14
14
|
import { checkMitmproxyInstalled, isCaCertInstalled, generateCaCert, installCaCert, writeAddonScript, getNetworkExtensionStatus, openNetworkExtensionSettings, } from "../interceptor/mitmproxy-manager.js";
|
|
15
15
|
import { printDesktopSupportExplainer, printNetworkExtensionInstructions } from "./cmd-client.js";
|
|
16
|
+
import { trackEvent, showTelemetryDisclosureIfNeeded } from "../utils/telemetry.js";
|
|
16
17
|
const execFileAsync = promisify(execFile);
|
|
17
18
|
// ─── Public registration ──────────────────────────────────────────────────────
|
|
18
19
|
export function registerSetup(program) {
|
|
@@ -212,6 +213,8 @@ async function runSetupWizard({ addMode }) {
|
|
|
212
213
|
console.log(chalk.bold(`\n${"━".repeat(40)}\n Saving\n${"━".repeat(40)}\n`));
|
|
213
214
|
saveAccounts(merged);
|
|
214
215
|
console.log(chalk.green(` ✓ ${merged.length} account(s) saved to ~/.cc-router/accounts.json`));
|
|
216
|
+
showTelemetryDisclosureIfNeeded();
|
|
217
|
+
void trackEvent("setup_completed", { account_count: merged.length });
|
|
215
218
|
// ─── Post-setup interactive flow ─────────────────────────────────────────
|
|
216
219
|
await runPostSetupFlow(merged.length);
|
|
217
220
|
}
|
package/dist/cli/cmd-start.js
CHANGED
|
@@ -2,6 +2,7 @@ import { execFile } from "child_process";
|
|
|
2
2
|
import { promisify } from "util";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { PROXY_PORT, LITELLM_PORT, ACCOUNTS_PATH } from "../config/paths.js";
|
|
5
|
+
import { showTelemetryDisclosureIfNeeded } from "../utils/telemetry.js";
|
|
5
6
|
const execFileAsync = promisify(execFile);
|
|
6
7
|
export function registerStart(program) {
|
|
7
8
|
program
|
|
@@ -13,6 +14,9 @@ export function registerStart(program) {
|
|
|
13
14
|
.option("--accounts <path>", "Path to accounts.json", ACCOUNTS_PATH)
|
|
14
15
|
.action(async (opts) => {
|
|
15
16
|
if (opts.daemon) {
|
|
17
|
+
// Show telemetry disclosure in the user's terminal before handing off
|
|
18
|
+
// to PM2 — once the daemon starts, its stdout goes to PM2 logs.
|
|
19
|
+
showTelemetryDisclosureIfNeeded();
|
|
16
20
|
await startDaemon();
|
|
17
21
|
return;
|
|
18
22
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { loadTelemetryState, writeTelemetryState, isTelemetryEnabled } from "../config/telemetry.js";
|
|
3
|
+
import { trackEvent } from "../utils/telemetry.js";
|
|
4
|
+
export function registerTelemetry(program) {
|
|
5
|
+
program
|
|
6
|
+
.command("telemetry [action]")
|
|
7
|
+
.description("Manage anonymous usage analytics: on, off, status (default: status)")
|
|
8
|
+
.action(async (action) => {
|
|
9
|
+
const resolved = action ?? "status";
|
|
10
|
+
if (resolved === "status") {
|
|
11
|
+
showStatus();
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (resolved === "on") {
|
|
15
|
+
const state = loadTelemetryState();
|
|
16
|
+
state.enabled = true;
|
|
17
|
+
writeTelemetryState(state);
|
|
18
|
+
console.log(chalk.green("Telemetry enabled."));
|
|
19
|
+
console.log(chalk.dim(`Install ID: ${state.installId}`));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (resolved === "off") {
|
|
23
|
+
// Send one last event so we know about opt-out rates
|
|
24
|
+
await trackEvent("telemetry_disabled");
|
|
25
|
+
const state = loadTelemetryState();
|
|
26
|
+
state.enabled = false;
|
|
27
|
+
writeTelemetryState(state);
|
|
28
|
+
console.log(chalk.yellow("Telemetry disabled. No data will be sent."));
|
|
29
|
+
console.log(chalk.dim("Re-enable anytime with: cc-router telemetry on"));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
console.error(chalk.red(`Unknown action "${resolved}". Use: on, off, status`));
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function showStatus() {
|
|
37
|
+
const state = loadTelemetryState();
|
|
38
|
+
const envDisabled = process.env["DO_NOT_TRACK"] === "1" || process.env["CC_ROUTER_TELEMETRY"] === "0";
|
|
39
|
+
console.log(chalk.bold("Telemetry"));
|
|
40
|
+
console.log();
|
|
41
|
+
if (envDisabled) {
|
|
42
|
+
console.log(` Status: ${chalk.yellow("disabled")} (by environment variable)`);
|
|
43
|
+
}
|
|
44
|
+
else if (state.enabled) {
|
|
45
|
+
console.log(` Status: ${chalk.green("enabled")}`);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
console.log(` Status: ${chalk.yellow("disabled")}`);
|
|
49
|
+
}
|
|
50
|
+
console.log(` Active: ${isTelemetryEnabled() ? chalk.green("yes") : chalk.yellow("no")}`);
|
|
51
|
+
console.log(` Install ID: ${chalk.dim(state.installId)}`);
|
|
52
|
+
console.log(` Since: ${chalk.dim(state.firstRunAt)}`);
|
|
53
|
+
console.log();
|
|
54
|
+
console.log(chalk.dim(" What we send: version, OS, locale, lifecycle events (start, heartbeat)"));
|
|
55
|
+
console.log(chalk.dim(" What we DON'T: IPs, tokens, prompts, request content, account names"));
|
|
56
|
+
console.log(chalk.dim(" Source code: src/utils/telemetry.ts"));
|
|
57
|
+
console.log();
|
|
58
|
+
console.log(chalk.dim(" Disable: cc-router telemetry off"));
|
|
59
|
+
console.log(chalk.dim(" Or set: DO_NOT_TRACK=1 | CC_ROUTER_TELEMETRY=0"));
|
|
60
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { registerConfigure } from "./cmd-configure.js";
|
|
|
10
10
|
import { registerDocker } from "./cmd-docker.js";
|
|
11
11
|
import { registerUpdate } from "./cmd-update.js";
|
|
12
12
|
import { registerClient } from "./cmd-client.js";
|
|
13
|
+
import { registerTelemetry } from "./cmd-telemetry.js";
|
|
13
14
|
import { getCurrentVersion, checkForUpdate, printUpdateBanner } from "../utils/self-update.js";
|
|
14
15
|
const program = new Command();
|
|
15
16
|
program
|
|
@@ -42,6 +43,7 @@ registerConfigure(program);
|
|
|
42
43
|
registerDocker(program);
|
|
43
44
|
registerUpdate(program);
|
|
44
45
|
registerClient(program);
|
|
46
|
+
registerTelemetry(program);
|
|
45
47
|
// Background update check — fires on every CLI invocation, uses 6h disk cache
|
|
46
48
|
// so it's essentially free after the first check. Notify on process exit.
|
|
47
49
|
if (!process.env["NO_UPDATE_NOTIFIER"] && !process.env["CI"]) {
|
package/dist/config/paths.js
CHANGED
|
@@ -12,3 +12,6 @@ export const LITELLM_URL = process.env["LITELLM_URL"];
|
|
|
12
12
|
// Proxy-level config (password, future settings) — separate from accounts.json
|
|
13
13
|
export const CONFIG_PATH = process.env["CONFIG_PATH"] ??
|
|
14
14
|
path.join(CONFIG_DIR, "config.json");
|
|
15
|
+
// Anonymous telemetry state — install id + opt-in flag
|
|
16
|
+
export const TELEMETRY_PATH = process.env["TELEMETRY_PATH"] ??
|
|
17
|
+
path.join(CONFIG_DIR, "telemetry.json");
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, renameSync } from "fs";
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import { TELEMETRY_PATH } from "./paths.js";
|
|
4
|
+
import { ensureConfigDir } from "./manager.js";
|
|
5
|
+
function defaultState() {
|
|
6
|
+
return {
|
|
7
|
+
enabled: true,
|
|
8
|
+
installId: randomUUID(),
|
|
9
|
+
firstRunAt: new Date().toISOString(),
|
|
10
|
+
disclosureShown: false,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
// Read the telemetry state, creating and persisting a fresh one on first run.
|
|
14
|
+
// Malformed files are treated as missing so a corrupted file can't crash the CLI.
|
|
15
|
+
export function loadTelemetryState() {
|
|
16
|
+
if (!existsSync(TELEMETRY_PATH)) {
|
|
17
|
+
const state = defaultState();
|
|
18
|
+
writeTelemetryState(state);
|
|
19
|
+
return state;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const raw = JSON.parse(readFileSync(TELEMETRY_PATH, "utf-8"));
|
|
23
|
+
// Fill any missing fields to keep the file forward-compatible
|
|
24
|
+
const state = {
|
|
25
|
+
enabled: raw.enabled ?? true,
|
|
26
|
+
installId: raw.installId ?? randomUUID(),
|
|
27
|
+
firstRunAt: raw.firstRunAt ?? new Date().toISOString(),
|
|
28
|
+
disclosureShown: raw.disclosureShown ?? false,
|
|
29
|
+
};
|
|
30
|
+
if (!raw.installId || raw.disclosureShown === undefined) {
|
|
31
|
+
writeTelemetryState(state);
|
|
32
|
+
}
|
|
33
|
+
return state;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
const state = defaultState();
|
|
37
|
+
writeTelemetryState(state);
|
|
38
|
+
return state;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Atomic write: .tmp + rename, same pattern as writeAccountsAtomic
|
|
42
|
+
export function writeTelemetryState(state) {
|
|
43
|
+
ensureConfigDir();
|
|
44
|
+
const tmp = TELEMETRY_PATH + ".tmp";
|
|
45
|
+
writeFileSync(tmp, JSON.stringify(state, null, 2), "utf-8");
|
|
46
|
+
renameSync(tmp, TELEMETRY_PATH);
|
|
47
|
+
}
|
|
48
|
+
// Returns true only if the user has not opted out through any mechanism:
|
|
49
|
+
// - DO_NOT_TRACK=1 (de-facto standard)
|
|
50
|
+
// - CC_ROUTER_TELEMETRY=0 (project-specific override)
|
|
51
|
+
// - `cc-router telemetry off` (persisted enabled: false)
|
|
52
|
+
export function isTelemetryEnabled() {
|
|
53
|
+
if (process.env["DO_NOT_TRACK"] === "1")
|
|
54
|
+
return false;
|
|
55
|
+
if (process.env["CC_ROUTER_TELEMETRY"] === "0")
|
|
56
|
+
return false;
|
|
57
|
+
try {
|
|
58
|
+
return loadTelemetryState().enabled;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
package/dist/proxy/server.js
CHANGED
|
@@ -6,6 +6,8 @@ import { TokenPool } from "./token-pool.js";
|
|
|
6
6
|
import { needsRefresh, refreshAccountToken, saveAccounts, startRefreshLoop } from "./token-refresher.js";
|
|
7
7
|
import { loadAccounts, accountsFileExists, readAccountsFromPath, readConfig } from "../config/manager.js";
|
|
8
8
|
import { checkForUpdate, performUpdate, restartSelf } from "../utils/self-update.js";
|
|
9
|
+
import { trackEvent, startHeartbeat, showTelemetryDisclosureIfNeeded } from "../utils/telemetry.js";
|
|
10
|
+
import { loadTelemetryState } from "../config/telemetry.js";
|
|
9
11
|
import { logRoute, logError, logStartup } from "./logger.js";
|
|
10
12
|
import { stats } from "./stats.js";
|
|
11
13
|
import { PROXY_PORT, LITELLM_URL } from "../config/paths.js";
|
|
@@ -383,5 +385,25 @@ export async function startServer(opts = {}) {
|
|
|
383
385
|
logStartup(port, host, mode, target, accounts.length);
|
|
384
386
|
if (autoUpdate)
|
|
385
387
|
console.log(chalk.gray(" Auto-update: enabled (patch/minor)"));
|
|
388
|
+
// Anonymous telemetry — fire-and-forget, never blocks proxy startup.
|
|
389
|
+
// Show the disclosure on the very first start (covers existing users
|
|
390
|
+
// upgrading from versions before telemetry existed) before sending anything.
|
|
391
|
+
try {
|
|
392
|
+
showTelemetryDisclosureIfNeeded();
|
|
393
|
+
const telemetryState = loadTelemetryState();
|
|
394
|
+
// First-run detection: if the install is brand new, emit app_started too
|
|
395
|
+
const firstRunAge = Date.now() - new Date(telemetryState.firstRunAt).getTime();
|
|
396
|
+
if (firstRunAge < 5 * 60 * 1000) {
|
|
397
|
+
void trackEvent("app_started", { first_run: true });
|
|
398
|
+
}
|
|
399
|
+
void trackEvent("proxy_started", {
|
|
400
|
+
account_count: accounts.length,
|
|
401
|
+
mode,
|
|
402
|
+
});
|
|
403
|
+
startHeartbeat(accounts.length);
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
// never let telemetry break the proxy
|
|
407
|
+
}
|
|
386
408
|
});
|
|
387
409
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { isTelemetryEnabled, loadTelemetryState, writeTelemetryState } from "../config/telemetry.js";
|
|
4
|
+
import { detectPlatform } from "./platform.js";
|
|
5
|
+
import { getCurrentVersion } from "./self-update.js";
|
|
6
|
+
// ─── Aptabase configuration ──────────────────────────────────────────────────
|
|
7
|
+
// Aptabase is a privacy-first, open source analytics service.
|
|
8
|
+
// The full payload we send is documented below — search for "trackEvent" calls
|
|
9
|
+
// in the codebase to audit every event. Nothing here contains PII.
|
|
10
|
+
const APTABASE_APP_KEY = "A-EU-1060569594";
|
|
11
|
+
const APTABASE_ENDPOINT = "https://eu.aptabase.com/api/v0/event";
|
|
12
|
+
const TIMEOUT_MS = 3_000;
|
|
13
|
+
function getOsName() {
|
|
14
|
+
switch (detectPlatform()) {
|
|
15
|
+
case "macos": return "macOS";
|
|
16
|
+
case "linux": return "Linux";
|
|
17
|
+
case "windows": return "Windows";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function getLocale() {
|
|
21
|
+
try {
|
|
22
|
+
return Intl.DateTimeFormat().resolvedOptions().locale;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return process.env["LANG"]?.split(".")[0] ?? "unknown";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function getSystemProps() {
|
|
29
|
+
return {
|
|
30
|
+
isDebug: false,
|
|
31
|
+
locale: getLocale(),
|
|
32
|
+
osName: getOsName(),
|
|
33
|
+
osVersion: os.release(),
|
|
34
|
+
appVersion: getCurrentVersion(),
|
|
35
|
+
engineName: "node",
|
|
36
|
+
engineVersion: process.versions.node,
|
|
37
|
+
sdkVersion: `cc-router@${getCurrentVersion()}`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// Session ID: <installId>-<epoch-hours>. This groups events that belong to the
|
|
41
|
+
// same "session" (proxy run) without leaking any timing precision finer than 1h.
|
|
42
|
+
function getSessionId(installId) {
|
|
43
|
+
const epochHours = Math.floor(Date.now() / 3_600_000);
|
|
44
|
+
return `${installId}-${epochHours}`;
|
|
45
|
+
}
|
|
46
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
47
|
+
// Fire-and-forget: never throws, never blocks the caller. If telemetry is
|
|
48
|
+
// disabled (env var or opt-out) this is a synchronous no-op.
|
|
49
|
+
export async function trackEvent(eventName, props) {
|
|
50
|
+
try {
|
|
51
|
+
if (!isTelemetryEnabled())
|
|
52
|
+
return;
|
|
53
|
+
const state = loadTelemetryState();
|
|
54
|
+
const body = {
|
|
55
|
+
timestamp: new Date().toISOString(),
|
|
56
|
+
sessionId: getSessionId(state.installId),
|
|
57
|
+
eventName,
|
|
58
|
+
systemProps: getSystemProps(),
|
|
59
|
+
props: props ?? {},
|
|
60
|
+
};
|
|
61
|
+
await fetch(APTABASE_ENDPOINT, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: {
|
|
64
|
+
"Content-Type": "application/json",
|
|
65
|
+
"App-Key": APTABASE_APP_KEY,
|
|
66
|
+
},
|
|
67
|
+
body: JSON.stringify(body),
|
|
68
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Silently swallow — telemetry must never disrupt the proxy
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Start a heartbeat that fires every 6 hours while the proxy is running.
|
|
76
|
+
// Uses .unref() so the timer does not prevent Node from exiting.
|
|
77
|
+
export function startHeartbeat(accountCount) {
|
|
78
|
+
const startTime = Date.now();
|
|
79
|
+
const timer = setInterval(() => {
|
|
80
|
+
const uptimeHours = Math.floor((Date.now() - startTime) / 3_600_000);
|
|
81
|
+
trackEvent("proxy_heartbeat", {
|
|
82
|
+
uptime_hours: uptimeHours,
|
|
83
|
+
account_count: accountCount,
|
|
84
|
+
});
|
|
85
|
+
}, 6 * 60 * 60 * 1000);
|
|
86
|
+
timer.unref();
|
|
87
|
+
}
|
|
88
|
+
// One-time disclosure shown the very first time CC-Router runs after install
|
|
89
|
+
// or upgrade. Idempotent — gated by telemetry.disclosureShown so it's safe to
|
|
90
|
+
// call from multiple entry points (setup wizard, foreground start, daemon
|
|
91
|
+
// start, service install). Returns true if the disclosure was just shown.
|
|
92
|
+
export function showTelemetryDisclosureIfNeeded() {
|
|
93
|
+
try {
|
|
94
|
+
const state = loadTelemetryState();
|
|
95
|
+
if (state.disclosureShown)
|
|
96
|
+
return false;
|
|
97
|
+
console.log();
|
|
98
|
+
console.log(chalk.dim("─".repeat(60)));
|
|
99
|
+
console.log(chalk.bold(" Anonymous usage analytics"));
|
|
100
|
+
console.log();
|
|
101
|
+
console.log(" CC-Router sends anonymous lifecycle events (version, OS,");
|
|
102
|
+
console.log(" startup, heartbeat) to help us understand usage and prioritize");
|
|
103
|
+
console.log(" improvements. No IPs, no tokens, no prompts, no request content.");
|
|
104
|
+
console.log();
|
|
105
|
+
console.log(` Disable: ${chalk.cyan("cc-router telemetry off")}`);
|
|
106
|
+
console.log(` Or set: ${chalk.cyan("DO_NOT_TRACK=1")} | ${chalk.cyan("CC_ROUTER_TELEMETRY=0")}`);
|
|
107
|
+
console.log(` Source: ${chalk.dim("src/utils/telemetry.ts")}`);
|
|
108
|
+
console.log(chalk.dim("─".repeat(60)));
|
|
109
|
+
console.log();
|
|
110
|
+
state.disclosureShown = true;
|
|
111
|
+
writeTelemetryState(state);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|