Commit 113a12c
feat(telemetry): one-time opt-out beacon + banner Settings deep-link (MCP-2482) (#684)
* feat(telemetry): one-time opt-out beacon + banner Settings deep-link (MCP-2482)
Part B — server-side opt-out beacon:
- Add internal/telemetry/optout.go: TelemetryDisableTransition (resolved-state
compare, nil == enabled per MCP-2477), SendOptOutBeacon (reuses the existing
/heartbeat ingest with event:"telemetry_disabled" + only the anonymous_id,
no usage payload), and Service.NotifyConfigChanged.
- Detect the enabled->disabled flip on config write and fire EXACTLY ONE
best-effort, fire-and-forget beacon; latch optedOut so no further heartbeats
are emitted. Re-enable clears the latch (exactly once per flip).
- Wire NotifyConfigChanged into the daemon's REST apply path (runtime.ApplyConfig)
and disk-reload path (ReloadConfiguration) so web UI + macOS app are covered,
and into `mcpproxy telemetry disable` so the CLI path is covered (no fsnotify
watcher means the daemon won't auto-reload the file). Send failure still
disables; disabled->disabled / env-disabled emit nothing.
Part A — banner deep-link + disclosure:
- TelemetryBanner: add "Manage in Settings" link to /settings?focus=telemetry.enabled
and a transparency note about the one-time opt-out signal.
- Settings.vue: focusField() resolves a field key to its tab, opens the
enclosing accordion, scrolls to and highlights the row.
- Disclosure line added near the telemetry toggle (field help + confirm copy).
Docs: document the one-time opt-out signal in docs/features/telemetry.md.
Related #MCP-2482
* fix(telemetry): route opt-out beacon through service endpoint (fix CodeQL SSRF)
CodeQL go/request-forgery (CWE-918) flagged optout.go because the destination
URL flowed as a function parameter directly from the config source to the
http.Do sink. Make SendOptOutBeacon a *Service method that reads the resolved
s.endpoint/s.config — the same struct-field indirection the existing heartbeat
and feedback senders use (which CodeQL does not flag) — so the beacon can never
target an arbitrary caller-supplied URL. The CLI path now constructs a
telemetry.Service and calls the method instead of passing a raw URL.
No behavior change: still exactly one fire-and-forget beacon to /heartbeat on
the enabled->disabled flip.
Related #MCP-2482
* fix(telemetry): validate opt-out beacon URL (scheme/host) to clear CWE-918
Apply the repo's established request-forgery barrier (mirror validateRegistryURL):
parse the configured telemetry endpoint, constrain the scheme to http/https,
require a host, and issue the beacon against the re-serialized URL. Rejects
file://, gopher://, and schemeless/malformed endpoints before any request.
Related #MCP-2482
* fix(telemetry): pin opt-out beacon host to clear CWE-918 (host allowlist guard)
Scheme/host-presence validation alone did not satisfy CodeQL go/request-forgery;
the established repo barrier (validateRegistryURL) is a host equality guard. Pin
the beacon destination to the built-in telemetry host or a loopback address
(tests/local dev) and reject anything else before issuing the request. This both
clears the alert and genuinely hardens the path — the anonymous ID can only ever
reach the official endpoint or localhost, never an arbitrary host from a
malformed/hostile telemetry.endpoint value (documented as a testing override).
Related #MCP-2482
* fix(telemetry): inline opt-out beacon host guard (CWE-918 barrier shape)
Move the host allowlist check inline into validateTelemetryURL as an
'if !match { return err }' equality guard on the same control-flow path as the
request sink, mirroring validateRegistryURL (the repo's CodeQL-accepted
request-forgery barrier). The prior bool-returning helper put the guard across a
function boundary that CodeQL's barrier-guard analysis did not propagate.
Related #MCP-2482
* fix(telemetry): explicit-equality host guard for opt-out beacon (CWE-918)
Earlier barrier shapes (helper returning bool; EqualFold + net.ParseIP/IsLoopback)
did not satisfy CodeQL go/request-forgery. Switch to an inline exact-equality
allowlist (switch on the lower-cased host against the built-in telemetry host and
explicit loopback literals) — the canonical barrier shape the scanner recognizes,
on the same control-flow path as the request. Behaviour unchanged: prod host and
loopback (tests/local) allowed, everything else rejected.
Related #MCP-2482
* fix(telemetry): address Codex review — env-resolved gate, opt-out race, CLI guards (MCP-2482)
Codex REQUEST_CHANGES on PR #684:
1. [P1] Respect env-resolved disabled state. The transition gated only on
Config.IsTelemetryEnabled() (MCPPROXY_TELEMETRY), so DO_NOT_TRACK=1 / CI=true
could emit a beacon when telemetry was never enabled. Add
EffectiveTelemetryEnabled() = IsTelemetryEnabled() && !IsDisabledByEnv(), and
use it for the transition, the New() resolvedEnabled seed, and the CLI.
2. [P1] Heartbeat-after-opt-out race. sendHeartbeat checked optedOut only at
entry; a heartbeat in flight when the latch flipped still shipped a full usage
payload. Re-check the latch immediately before transmit so no usage data
leaves after opt-out.
3. [P2] CLI disable path. Routed through a single guarded entry point
(Service.EmitOptOutBeacon — applies semver/dev, env, and anon-id guards and
owns the send) instead of calling SendOptOutBeacon directly (which bypassed
the guards). The CLI now confirms "Telemetry disabled." before the best-effort
beacon (non-blocking) with a short 3s timeout.
Tests: env-disabled emits nothing; mid-flight opt-out suppresses transmit;
EmitOptOutBeacon guard matrix (dev/env/no-anon-id skip, eligible sends). CLI
smoke: enabled=1 beacon, DO_NOT_TRACK/CI=0.
Related #MCP-2482
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>1 parent b8f0d86 commit 113a12c
10 files changed
Lines changed: 795 additions & 12 deletions
File tree
- cmd/mcpproxy
- docs/features
- frontend/src
- components
- views
- settings
- internal
- runtime
- telemetry
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
14 | 15 | | |
15 | 16 | | |
16 | 17 | | |
| |||
267 | 268 | | |
268 | 269 | | |
269 | 270 | | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
270 | 278 | | |
271 | 279 | | |
272 | 280 | | |
| |||
278 | 286 | | |
279 | 287 | | |
280 | 288 | | |
| 289 | + | |
| 290 | + | |
281 | 291 | | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
282 | 306 | | |
283 | 307 | | |
284 | 308 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
44 | 44 | | |
45 | 45 | | |
46 | 46 | | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
47 | 60 | | |
48 | 61 | | |
49 | 62 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | | - | |
| 2 | + | |
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
14 | 33 | | |
15 | | - | |
16 | | - | |
17 | | - | |
18 | | - | |
19 | | - | |
20 | 34 | | |
21 | 35 | | |
22 | 36 | | |
23 | 37 | | |
24 | 38 | | |
| 39 | + | |
25 | 40 | | |
26 | 41 | | |
27 | 42 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
174 | 174 | | |
175 | 175 | | |
176 | 176 | | |
177 | | - | |
178 | | - | |
| 177 | + | |
| 178 | + | |
179 | 179 | | |
180 | 180 | | |
181 | 181 | | |
| |||
200 | 200 | | |
201 | 201 | | |
202 | 202 | | |
| 203 | + | |
203 | 204 | | |
204 | 205 | | |
205 | 206 | | |
| |||
434 | 435 | | |
435 | 436 | | |
436 | 437 | | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
437 | 473 | | |
438 | 474 | | |
439 | 475 | | |
| |||
456 | 492 | | |
457 | 493 | | |
458 | 494 | | |
459 | | - | |
460 | | - | |
| 495 | + | |
461 | 496 | | |
462 | 497 | | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
463 | 504 | | |
464 | 505 | | |
465 | 506 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
251 | 251 | | |
252 | 252 | | |
253 | 253 | | |
254 | | - | |
| 254 | + | |
255 | 255 | | |
256 | 256 | | |
257 | 257 | | |
258 | 258 | | |
259 | 259 | | |
260 | | - | |
| 260 | + | |
261 | 261 | | |
262 | 262 | | |
263 | 263 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1029 | 1029 | | |
1030 | 1030 | | |
1031 | 1031 | | |
| 1032 | + | |
| 1033 | + | |
| 1034 | + | |
| 1035 | + | |
| 1036 | + | |
| 1037 | + | |
| 1038 | + | |
| 1039 | + | |
1032 | 1040 | | |
1033 | 1041 | | |
1034 | 1042 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1304 | 1304 | | |
1305 | 1305 | | |
1306 | 1306 | | |
| 1307 | + | |
| 1308 | + | |
| 1309 | + | |
| 1310 | + | |
| 1311 | + | |
| 1312 | + | |
| 1313 | + | |
| 1314 | + | |
1307 | 1315 | | |
1308 | 1316 | | |
1309 | 1317 | | |
| |||
0 commit comments