@timo972/cc-router 0.12.1 → 0.12.2-rc.0
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/CHANGELOG.md +18 -0
- package/Dockerfile +1 -0
- package/README.md +1 -1
- package/dist/cli/cmd-accounts.js +116 -65
- package/dist/cli/cmd-setup.js +100 -30
- package/dist/cli/cmd-status.js +14 -2
- package/dist/cli/cmd-telemetry.js +43 -32
- package/dist/cli/index.js +15 -1
- package/dist/config/directory.js +14 -0
- package/dist/config/telemetry.js +192 -41
- package/dist/providers/anthropic/usage-refresher.js +35 -1
- package/dist/providers/model-discovery.js +17 -11
- package/dist/providers/openai/device-oauth.js +88 -31
- package/dist/providers/openai/token-refresher.js +12 -2
- package/dist/providers/openai/usage-fetch.js +19 -2
- package/dist/proxy/anthropic-messages-route.js +144 -5
- package/dist/proxy/anthropic-proxy.js +10 -0
- package/dist/proxy/anthropic-response-capture.js +6 -19
- package/dist/proxy/openai-ingress.js +98 -1
- package/dist/proxy/server.js +35 -22
- package/dist/proxy/token-refresher.js +12 -2
- package/dist/proxy/usage-capture.js +41 -4
- package/dist/telemetry/contracts.js +129 -0
- package/dist/telemetry/facade.js +654 -0
- package/dist/telemetry/otel-exporters.js +289 -0
- package/dist/telemetry/posthog-client.js +398 -0
- package/dist/telemetry/privacy.js +567 -0
- package/dist/telemetry/runtime.js +306 -0
- package/dist/telemetry/setup-diagnostics.js +239 -0
- package/dist/utils/token-extractor.js +79 -11
- package/dist/utils/token-validator.js +25 -9
- package/docs/README.md +34 -0
- package/docs/telemetry.md +167 -0
- package/package.json +12 -2
- package/dist/utils/telemetry.js +0 -88
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate an OAuth access token against the Anthropic API.
|
|
3
|
+
*
|
|
4
|
+
* Uses GET /v1/models — lightweight call that doesn't create any resources.
|
|
5
|
+
* Required header: anthropic-version (per API spec).
|
|
6
|
+
* Auth: Authorization: Bearer <token> (OAuth tokens use Bearer, not x-api-key).
|
|
7
|
+
*/
|
|
8
|
+
import { classifyHttpSetupFailure, classifyNetworkSetupFailure, } from "../telemetry/setup-diagnostics.js";
|
|
1
9
|
export async function validateToken(accessToken) {
|
|
2
10
|
try {
|
|
3
11
|
const res = await fetch("https://api.anthropic.com/v1/models", {
|
|
@@ -10,17 +18,25 @@ export async function validateToken(accessToken) {
|
|
|
10
18
|
});
|
|
11
19
|
if (res.ok)
|
|
12
20
|
return { valid: true };
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
const reason = res.status === 401
|
|
22
|
+
? "Token invalid or expired (401)"
|
|
23
|
+
: res.status === 403
|
|
24
|
+
? "Token lacks required scopes (403) — needs user:inference"
|
|
25
|
+
// Any other non-ok status is unexpected but the token may still work
|
|
26
|
+
: `Unexpected HTTP ${res.status}`;
|
|
27
|
+
return {
|
|
28
|
+
valid: false,
|
|
29
|
+
reason,
|
|
30
|
+
diagnostic: classifyHttpSetupFailure("token_validation", res.status, reason),
|
|
31
|
+
};
|
|
21
32
|
}
|
|
22
33
|
catch (err) {
|
|
23
34
|
// Network error — can't validate, let user decide
|
|
24
|
-
|
|
35
|
+
const reason = `Network error: ${err.message}`;
|
|
36
|
+
return {
|
|
37
|
+
valid: false,
|
|
38
|
+
reason,
|
|
39
|
+
diagnostic: classifyNetworkSetupFailure("token_validation", err, reason),
|
|
40
|
+
};
|
|
25
41
|
}
|
|
26
42
|
}
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# CC-Router documentation
|
|
2
|
+
|
|
3
|
+
Start with the [README](../README.md) for what CC-Router is and a quickstart.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
| Guide | What's in it |
|
|
8
|
+
|---|---|
|
|
9
|
+
| [Installation & deployment](installation.md) | Requirements, per-platform token extraction, run modes, Docker |
|
|
10
|
+
| [CLI reference](cli-reference.md) | Every command and flag |
|
|
11
|
+
| [Codex CLI & OpenAI](codex.md) | Responses endpoint, model prefixes, OpenAI subscription accounts |
|
|
12
|
+
| [Grok / xAI](grok.md) | Adding Grok accounts, and why they are overview-only |
|
|
13
|
+
| [Claude Desktop](claude-desktop.md) | Routing Claude Desktop / Cowork through mitmproxy |
|
|
14
|
+
| [Client mode](client-mode.md) | Connecting another device you own to your router |
|
|
15
|
+
| [LiteLLM](litellm-setup.md) | Optional logging, rate limiting and web dashboard layer |
|
|
16
|
+
|
|
17
|
+
## Operating it
|
|
18
|
+
|
|
19
|
+
| Guide | What's in it |
|
|
20
|
+
|---|---|
|
|
21
|
+
| [Architecture](architecture.md) | The request path and what each component does |
|
|
22
|
+
| [Session routing](session-routing.md) | How an account is picked, failover behaviour, running it for a team |
|
|
23
|
+
| [Dashboard](dashboard.md) | The live TUI, keybindings, model management, ChatGPT usage resets |
|
|
24
|
+
| [Troubleshooting](troubleshooting.md) | When something doesn't connect |
|
|
25
|
+
|
|
26
|
+
## Reference
|
|
27
|
+
|
|
28
|
+
| Guide | What's in it |
|
|
29
|
+
|---|---|
|
|
30
|
+
| [OAuth tokens](oauth-tokens.md) | How subscription tokens work and why refresh rotation matters |
|
|
31
|
+
| [Security](security.md) | Token storage, proxy authentication, threat model |
|
|
32
|
+
| [Telemetry](telemetry.md) | Opt-in analytics: what's sent if you enable it |
|
|
33
|
+
|
|
34
|
+
Design specs and implementation plans live under [`superpowers/`](superpowers).
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Telemetry
|
|
2
|
+
|
|
3
|
+
CC-Router sends privacy-bounded operational telemetry to the maintainer's
|
|
4
|
+
PostHog EU project: sampled proxy traces, closed-schema diagnostics, a few
|
|
5
|
+
lifecycle events, and sanitized exceptions. This page is the complete public
|
|
6
|
+
inventory of what can leave the machine. The contracts are implemented in
|
|
7
|
+
[`src/telemetry/`](../src/telemetry/) and every outbound record is rebuilt from
|
|
8
|
+
an allowlist immediately before it is exported.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
cc-router telemetry status # effective state and what is sent
|
|
12
|
+
cc-router telemetry on
|
|
13
|
+
cc-router telemetry off
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Enablement
|
|
17
|
+
|
|
18
|
+
Telemetry is **on by default for fresh installations**. An existing
|
|
19
|
+
`~/.cc-router/telemetry.json` with `enabled: false` stays off after an upgrade.
|
|
20
|
+
The effective state is:
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
persisted enabled AND DO_NOT_TRACK != "1" AND CC_ROUTER_TELEMETRY != "0"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`cc-router telemetry off` persists the choice, emits no opt-out beacon, and
|
|
27
|
+
records a new *consent generation*. A running daemon re-reads the state before
|
|
28
|
+
every capture and every export; once it observes a different generation it
|
|
29
|
+
stops exporting for the rest of its life and discards queued records. An HTTPS
|
|
30
|
+
request already in flight cannot be recalled. Environment variables can only
|
|
31
|
+
turn telemetry off, never on. After `cc-router telemetry on`, restart a daemon
|
|
32
|
+
that started while disabled.
|
|
33
|
+
|
|
34
|
+
## Destination, identity, sampling
|
|
35
|
+
|
|
36
|
+
The only destination host is `eu.i.posthog.com`:
|
|
37
|
+
|
|
38
|
+
| Signal | Endpoint |
|
|
39
|
+
|---|---|
|
|
40
|
+
| analytics events and sanitized exceptions | `https://eu.i.posthog.com/batch/` |
|
|
41
|
+
| OpenTelemetry traces | `https://eu.i.posthog.com/i/v1/traces` |
|
|
42
|
+
| OpenTelemetry logs | `https://eu.i.posthog.com/i/v1/logs` |
|
|
43
|
+
|
|
44
|
+
PostHog necessarily sees the connection's source IP at the transport layer;
|
|
45
|
+
CC-Router does not put it in the payload, disables GeoIP enrichment, and never
|
|
46
|
+
creates a PostHog Person profile (`$process_person_profile: false`,
|
|
47
|
+
`$geoip_disable: true` on every event).
|
|
48
|
+
|
|
49
|
+
A random installation UUID stored in `~/.cc-router/telemetry.json` is the
|
|
50
|
+
stable PostHog `distinctId` and OpenTelemetry `service.instance.id`. It is not
|
|
51
|
+
derived from any user, account, host, network, or machine identifier.
|
|
52
|
+
|
|
53
|
+
Root traces are head-sampled at 10%; child spans follow their root. Diagnostics,
|
|
54
|
+
warnings, errors, lifecycle events, and exceptions are not sampled. Inbound
|
|
55
|
+
`traceparent`/`tracestate`/`baggage` headers are stripped from proxied requests
|
|
56
|
+
and no trace headers are injected upstream.
|
|
57
|
+
|
|
58
|
+
## Closed inventory
|
|
59
|
+
|
|
60
|
+
Application code cannot choose an arbitrary event name, log body, operation, or
|
|
61
|
+
property. Values outside the closed enums are rejected before export.
|
|
62
|
+
|
|
63
|
+
### Analytics events
|
|
64
|
+
|
|
65
|
+
| Event | When | Properties |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `app.first_start` | first start of a fresh installation, once | runtime fields |
|
|
68
|
+
| `proxy.started` | each `cc-router start` | runtime fields |
|
|
69
|
+
| `proxy.heartbeat` | hourly while the proxy runs | runtime fields |
|
|
70
|
+
| `account_setup.started` / `stage_completed` / `succeeded` / `cancelled` / `failed` | account setup funnel | setup fields |
|
|
71
|
+
|
|
72
|
+
Runtime fields: application version, OS family, runtime mode
|
|
73
|
+
(`foreground`/`daemon`/`service`), bounded account-pool size. Setup fields add
|
|
74
|
+
provider, method, stage, optional safe reason, optional duration bucket, and the
|
|
75
|
+
attempt's random diagnostic ID.
|
|
76
|
+
|
|
77
|
+
### Log records
|
|
78
|
+
|
|
79
|
+
- `account.setup.diagnostic` — provider, method, stage, optional reason and
|
|
80
|
+
outcome, exact HTTP status, duration bucket, diagnostic ID.
|
|
81
|
+
- `runtime.failure` — operation, provider, reason, outcome, exact HTTP status,
|
|
82
|
+
bounded attempt / pool-size / concurrency / duration values, optional
|
|
83
|
+
diagnostic ID.
|
|
84
|
+
|
|
85
|
+
Both carry severity (`info`, `warn`, `error`, `fatal`), a timestamp, the
|
|
86
|
+
runtime fields above, and trace/span IDs only when emitted inside a sampled
|
|
87
|
+
trace. Console output and local log files are never forwarded.
|
|
88
|
+
|
|
89
|
+
### Trace spans
|
|
90
|
+
|
|
91
|
+
Operations: `proxy.request`, `provider.inference`, `oauth.refresh`,
|
|
92
|
+
`provider.usage_refresh`, `model.discovery`.
|
|
93
|
+
|
|
94
|
+
A span may carry only: operation, trace/span/parent IDs, kind, start time,
|
|
95
|
+
duration, status, and these attributes — HTTP method and status code, provider
|
|
96
|
+
(`anthropic`/`openai`/`other`), route (`messages`/`responses`/`other`), model
|
|
97
|
+
family (`fable`/`sonnet`/`opus`/`haiku`/`codex`/`other`), request source
|
|
98
|
+
(`cli`/`desktop`/`api`/`other`), runtime mode, streaming flag, stream outcome,
|
|
99
|
+
outcome (`complete`/`rate_limited`/`timeout`/`upstream_error`/`cancelled`/
|
|
100
|
+
`other`), and bounded attempt, pool-size, concurrency, input-token,
|
|
101
|
+
output-token, and duration values. Span events, links, URLs, and headers are
|
|
102
|
+
not exported.
|
|
103
|
+
|
|
104
|
+
### Setup funnel
|
|
105
|
+
|
|
106
|
+
Providers `anthropic` (`macos_keychain`, `claude_credentials_file`,
|
|
107
|
+
`manual_token`) and `openai` (`manual_token`, `device_oauth`). Stages:
|
|
108
|
+
`attempt_start`, `credential_source_selection`, `credential_read`,
|
|
109
|
+
`credential_parse`, `token_validation`, `device_code_request`,
|
|
110
|
+
`authorization_polling`, `token_exchange`, `access_token_parse`, `persistence`,
|
|
111
|
+
`success`, `cancellation`, `failure`. Safe reasons: `not_found`,
|
|
112
|
+
`permission_denied`, `malformed_credentials`, `invalid_token`, `unauthorized`,
|
|
113
|
+
`forbidden`, `rate_limited`, `upstream_4xx`, `upstream_5xx`, `timeout`,
|
|
114
|
+
`network_failure`, `unexpected_response_shape`, `persistence_failure`,
|
|
115
|
+
`user_cancelled`, `other`. Duration buckets: `under_1s`, `1s_to_5s`,
|
|
116
|
+
`5s_to_30s`, `30s_to_2m`, `over_2m`.
|
|
117
|
+
|
|
118
|
+
### Sanitized exceptions
|
|
119
|
+
|
|
120
|
+
An unexpected failure is rebuilt as a *new* `Error` containing only: category
|
|
121
|
+
(`setup`/`runtime`), one safe reason, error kind (`error`, `type_error`,
|
|
122
|
+
`range_error`, `reference_error`, `syntax_error`, `uri_error`, `eval_error`,
|
|
123
|
+
`aggregate_error`, `unexpected_error`), optional system code (`EAI_AGAIN`,
|
|
124
|
+
`ECONNREFUSED`, `ECONNRESET`, `ENETUNREACH`, `ENOTFOUND`, `EPIPE`,
|
|
125
|
+
`ETIMEDOUT`), optional HTTP status, operation, provider, setup stage, runtime
|
|
126
|
+
mode, stack frames normalized to `dist/...` or `node_modules/<package>/...`
|
|
127
|
+
(max 20 frames, 256 chars each), a fingerprint over those safe fields, and a
|
|
128
|
+
fresh random diagnostic ID. The original message, cause chain, custom
|
|
129
|
+
properties, and unrecognized frames are dropped. The diagnostic ID is printed
|
|
130
|
+
next to the detailed local error so an issue report can reference it.
|
|
131
|
+
|
|
132
|
+
A fatal (uncaught) exception cannot be sent by the crashing process. Its
|
|
133
|
+
sanitized record is written to `~/.cc-router/telemetry.json.pending.json`
|
|
134
|
+
(mode 0600, at most 20 records) and sent by the next start only if the
|
|
135
|
+
installation ID and consent generation still match; otherwise it is deleted
|
|
136
|
+
unsent.
|
|
137
|
+
|
|
138
|
+
### Resource and bounds
|
|
139
|
+
|
|
140
|
+
Resource: `service.name` (`cc-router`), `service.version`,
|
|
141
|
+
`service.instance.id` (installation UUID), `process.runtime.version`, `os.type`
|
|
142
|
+
(`macos`/`linux`/`windows`/`other`), `host.arch` (`arm64`/`x64`/`other`),
|
|
143
|
+
`cc_router.runtime_mode`. Automatic resource detection is off; host, process,
|
|
144
|
+
cloud, and environment metadata is not exported.
|
|
145
|
+
|
|
146
|
+
| Value | Accepted range |
|
|
147
|
+
|---|---:|
|
|
148
|
+
| version strings | 1–64 characters |
|
|
149
|
+
| attempt | 0–100 |
|
|
150
|
+
| account-pool size, concurrency | 0–10,000 |
|
|
151
|
+
| input/output tokens | 0–1,000,000,000 |
|
|
152
|
+
| durations | 0–86,400,000 ms |
|
|
153
|
+
| HTTP status | 100–599 |
|
|
154
|
+
|
|
155
|
+
## Never sent
|
|
156
|
+
|
|
157
|
+
Prompts, tool calls, message content, request or response bodies; OAuth or
|
|
158
|
+
refresh tokens, cookies, credentials, device or user codes; request or
|
|
159
|
+
response headers, URLs, query strings, peer addresses; account IDs or names,
|
|
160
|
+
Claude session IDs, user IDs, emails, usernames; hostnames, home or working
|
|
161
|
+
directories, absolute paths, command lines, PIDs, environment variables;
|
|
162
|
+
raw exception messages, cause chains, custom error properties, or arbitrary
|
|
163
|
+
attributes — raw, encoded, or hashed.
|
|
164
|
+
|
|
165
|
+
Telemetry failures are swallowed at the telemetry boundary and never change
|
|
166
|
+
proxy responses, streamed bytes, retry behavior, exit codes, or crash
|
|
167
|
+
semantics.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timo972/cc-router",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2-rc.0",
|
|
4
4
|
"description": "Cache-aware session router for Claude Max OAuth tokens — use multiple Claude Max accounts with Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -36,15 +36,25 @@
|
|
|
36
36
|
"accounts.example.json",
|
|
37
37
|
"README.md",
|
|
38
38
|
"CHANGELOG.md",
|
|
39
|
+
"docs/telemetry.md",
|
|
39
40
|
"LICENSE"
|
|
40
41
|
],
|
|
41
42
|
"dependencies": {
|
|
42
43
|
"@inquirer/prompts": "^7.0.0",
|
|
44
|
+
"@opentelemetry/api": "^1.9.1",
|
|
45
|
+
"@opentelemetry/api-logs": "^0.221.0",
|
|
46
|
+
"@opentelemetry/otlp-exporter-base": "^0.221.0",
|
|
47
|
+
"@opentelemetry/otlp-transformer": "^0.221.0",
|
|
48
|
+
"@opentelemetry/resources": "^2.10.0",
|
|
49
|
+
"@opentelemetry/sdk-logs": "^0.221.0",
|
|
50
|
+
"@opentelemetry/sdk-trace-base": "^2.10.0",
|
|
51
|
+
"@opentelemetry/sdk-trace-node": "^2.10.0",
|
|
43
52
|
"chalk": "^5.3.0",
|
|
44
53
|
"commander": "^12.0.0",
|
|
45
54
|
"express": "^4.21.0",
|
|
46
55
|
"http-proxy-middleware": "^3.0.5",
|
|
47
56
|
"ink": "^5.0.0",
|
|
57
|
+
"posthog-node": "5.47.3",
|
|
48
58
|
"react": "^18.3.0",
|
|
49
59
|
"smol-toml": "^1.8.0"
|
|
50
60
|
},
|
|
@@ -61,7 +71,7 @@
|
|
|
61
71
|
"node": ">=22.0.0"
|
|
62
72
|
},
|
|
63
73
|
"scripts": {
|
|
64
|
-
"build": "tsc",
|
|
74
|
+
"build": "node scripts/clean-dist.mjs && tsc",
|
|
65
75
|
"dev": "tsx src/cli/index.ts",
|
|
66
76
|
"start": "node dist/cli/index.js",
|
|
67
77
|
"test": "vitest run",
|
package/dist/utils/telemetry.js
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import os from "os";
|
|
2
|
-
import { isTelemetryEnabled, loadTelemetryState } from "../config/telemetry.js";
|
|
3
|
-
import { detectPlatform } from "./platform.js";
|
|
4
|
-
import { getCurrentVersion } from "./self-update.js";
|
|
5
|
-
// ─── Aptabase configuration ──────────────────────────────────────────────────
|
|
6
|
-
// Aptabase is a privacy-first, open source analytics service.
|
|
7
|
-
// The full payload we send is documented below — search for "trackEvent" calls
|
|
8
|
-
// in the codebase to audit every event. Nothing here contains PII.
|
|
9
|
-
const APTABASE_APP_KEY = "A-EU-1060569594";
|
|
10
|
-
const APTABASE_ENDPOINT = "https://eu.aptabase.com/api/v0/event";
|
|
11
|
-
const TIMEOUT_MS = 3_000;
|
|
12
|
-
function getOsName() {
|
|
13
|
-
switch (detectPlatform()) {
|
|
14
|
-
case "macos": return "macOS";
|
|
15
|
-
case "linux": return "Linux";
|
|
16
|
-
case "windows": return "Windows";
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
function getLocale() {
|
|
20
|
-
try {
|
|
21
|
-
// Aptabase limits locale to 10 characters — truncate extended subtags
|
|
22
|
-
const raw = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
23
|
-
return raw.length <= 10 ? raw : raw.slice(0, 10);
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
return process.env["LANG"]?.split(".")[0]?.slice(0, 10) ?? "unknown";
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
function getSystemProps() {
|
|
30
|
-
return {
|
|
31
|
-
isDebug: false,
|
|
32
|
-
locale: getLocale(),
|
|
33
|
-
osName: getOsName(),
|
|
34
|
-
osVersion: os.release(),
|
|
35
|
-
appVersion: getCurrentVersion(),
|
|
36
|
-
engineName: "node",
|
|
37
|
-
engineVersion: process.versions.node,
|
|
38
|
-
sdkVersion: `cc-router@${getCurrentVersion()}`,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
// Session ID — use the installId directly so Aptabase always identifies the
|
|
42
|
-
// same machine as the same user. Aptabase limits sessionId to 36 characters;
|
|
43
|
-
// a standard UUID with dashes is exactly 36, so we pass it through unchanged.
|
|
44
|
-
function getSessionId(installId) {
|
|
45
|
-
return installId.slice(0, 36);
|
|
46
|
-
}
|
|
47
|
-
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
48
|
-
// Fire-and-forget: never throws, never blocks the caller. If telemetry is
|
|
49
|
-
// disabled (env var or opt-out) this is a synchronous no-op.
|
|
50
|
-
export async function trackEvent(eventName, props) {
|
|
51
|
-
try {
|
|
52
|
-
if (!isTelemetryEnabled())
|
|
53
|
-
return;
|
|
54
|
-
const state = loadTelemetryState();
|
|
55
|
-
const body = {
|
|
56
|
-
timestamp: new Date().toISOString(),
|
|
57
|
-
sessionId: getSessionId(state.installId),
|
|
58
|
-
eventName,
|
|
59
|
-
systemProps: getSystemProps(),
|
|
60
|
-
props: props ?? {},
|
|
61
|
-
};
|
|
62
|
-
await fetch(APTABASE_ENDPOINT, {
|
|
63
|
-
method: "POST",
|
|
64
|
-
headers: {
|
|
65
|
-
"Content-Type": "application/json",
|
|
66
|
-
"App-Key": APTABASE_APP_KEY,
|
|
67
|
-
},
|
|
68
|
-
body: JSON.stringify(body),
|
|
69
|
-
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
// Silently swallow — telemetry must never disrupt the proxy
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
// Start a heartbeat that fires every hour while the proxy is running.
|
|
77
|
-
// Uses .unref() so the timer does not prevent Node from exiting.
|
|
78
|
-
export function startHeartbeat(accountCount) {
|
|
79
|
-
const startTime = Date.now();
|
|
80
|
-
const timer = setInterval(() => {
|
|
81
|
-
const uptimeMinutes = Math.floor((Date.now() - startTime) / 60_000);
|
|
82
|
-
trackEvent("proxy_heartbeat", {
|
|
83
|
-
uptime_minutes: uptimeMinutes,
|
|
84
|
-
account_count: accountCount,
|
|
85
|
-
});
|
|
86
|
-
}, 60 * 60 * 1000);
|
|
87
|
-
timer.unref();
|
|
88
|
-
}
|