Skip to content

Commit ebf2948

Browse files
committed
Record and send one cli.command telemetry event per run
Each run builds one cli.command event from the run record, under the privacy rules reviewed in mapbox/event-schema#300: a value only for flags, fixed choices, numbers and language, country or feature-type codes; coordinates and tile addresses by name only; free strings as lengths; --data by the keys its spec declares; a userId replaced daily; no request ids. A detached child sends it to production Mapbox Events, so the command never waits. cli_token::send tries tokens in the order its caller declares: the user's --token or MAPBOX_ACCESS_TOKEN, then the login (refreshed only when about to expire), then the CLI's own token (MAPBOX_CLI_TOKEN, then a pk. token compiled in from MAPBOX_CLI_BUNDLED_TOKEN; build.rs refuses anything else). With no token the event is dropped. MAPBOX_CLI_NO_TELEMETRY=1 turns it off, and runs under sudo record nothing. MAPBOX_INTERNAL_TELEMETRY_URL redirects the send in builds that are not a production release, and MAPBOX_INTERNAL_TELEMETRY_LOG logs each delivery. On Windows, detached children no longer hold the caller's stdout pipe open.
1 parent fec6ab6 commit ebf2948

33 files changed

Lines changed: 3208 additions & 27 deletions

‎CHANGELOG.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,15 @@ that may never merge. They are not releases and are not listed here.
260260
code; it does find a `~/.mapbox/history` directory it didn't before, and
261261
`mapbox config list` now reports a second key, `history`.
262262

263+
- Each run sends one `cli.command` telemetry event to Mapbox, from a
264+
background process the command doesn't wait for, with your own token
265+
(`--token`, `MAPBOX_ACCESS_TOKEN` or your login) or, when you have none,
266+
one built into the CLI. A build from source has no built-in token, so with
267+
no token of your own the event is dropped. It never touches stdout, the
268+
exit code or how long a command takes, so a script sees no difference; a
269+
machine that already has `~/.mapbox` finds a `.telemetry` directory in it.
270+
`MAPBOX_CLI_NO_TELEMETRY=1` turns it off.
271+
263272
- `MAPBOX_CLI_EXTRA_QUERY` appends raw query parameters to every request, in
264273
the same `k1=v1&k2=v2` shape as a URL's own query string — for an API
265274
parameter this CLI's specs don't declare a flag for.

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ path = "src/main.rs"
3737
# until somebody writes the obvious thing at two in the morning.
3838
[lints.rust]
3939
# There is no `unsafe` in this crate outside a couple of `env::set_var` calls
40-
# in tests, and there is no reason for there to be: nothing here does FFI,
41-
# manual memory management or lock-free anything. Denying it means adding some
40+
# in tests and two Win32 calls in `src/detach.rs` (the reason is there), and
41+
# there is no reason for more: nothing else here does FFI, manual memory
42+
# management or lock-free anything. Denying it means adding some
4243
# is a deliberate `#[allow]` with a reason next to it rather than a diff nobody
4344
# looks twice at.
4445
unsafe_code = "deny"
@@ -111,6 +112,11 @@ jsonc-parser = { version = "0.34.0", features = ["serde", "serde_json"] }
111112
# that shows help; see src/output/theme.rs.
112113
terminal-colorsaurus = "1"
113114

115+
[target.'cfg(windows)'.dependencies]
116+
# For `src/detach.rs`: keeping the parent's standard handles out of a detached
117+
# child. Already in the graph through tokio and others; the same version.
118+
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_System_Console"] }
119+
114120
[dev-dependencies]
115121
# Integration tests build fake Mapbox tokens; same crate the CLI already uses.
116122
base64 = "0.22"

‎README.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,18 @@ Every request sends `User-Agent: mapbox-cli/<version>` and nothing else
285285
about you or your machine. `MAPBOX_CLI_NO_TELEMETRY=1` keeps even future
286286
markers out of that header.
287287

288+
Each run also sends one event to Mapbox: the command's name, its options
289+
(a value only when it comes from a fixed list, otherwise just its length or
290+
size, and for a JSON body only the field names the API defines), how it
291+
ended, and how long it took — never a token, a file path or free text you
292+
typed. It carries a random ID that is replaced on the first run a day after
293+
it was created. It is sent in the background, so a command never waits for
294+
it, and dropped if it can't be delivered. It is sent with your own token
295+
(`--token`, `MAPBOX_ACCESS_TOKEN` or your login) when you have one, and with
296+
a token built into the CLI otherwise. A build from source has no built-in
297+
token, so with no token of your own the event is dropped.
298+
`MAPBOX_CLI_NO_TELEMETRY=1` turns it off.
299+
288300
### Diagnostics and settings
289301

290302
```sh
@@ -451,6 +463,14 @@ process sends, in the same `k1=v1&k2=v2` shape as a URL's own query string —
451463
for an API parameter this CLI's specs don't declare a flag for. `--debug`
452464
and `--dry-run` show it alongside everything else on the request.
453465

466+
### The CLI's own requests
467+
468+
Requests the CLI makes for itself rather than for your commands use your own
469+
token whenever you have one: `--token` or `MAPBOX_ACCESS_TOKEN`, then your
470+
login. Only when you have none does it use the CLI's token:
471+
`MAPBOX_CLI_TOKEN`, or one built into the binary. The CLI's token is only
472+
used for what `src/cli_token.rs` lists for it, today just telemetry.
473+
454474
### Proxies
455475

456476
`HTTPS_PROXY`, `HTTP_PROXY`, `ALL_PROXY` and `NO_PROXY` are all honored, so

‎build.rs‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
use std::path::Path;
4242

4343
fn main() {
44+
check_bundled_token();
45+
4446
let manifest = Path::new("../internal/openapi-command-config/PINNED_SOURCE");
4547

4648
// The vendored specs and their provenance are ordinary build inputs now,
@@ -91,6 +93,20 @@ fn main() {
9193
warn_if_stale(committed_at);
9294
}
9395

96+
/// `src/cli_token.rs` compiles `MAPBOX_CLI_BUNDLED_TOKEN` into the binary,
97+
/// where `strings` can read it. Only a public `pk.` token may go there, so
98+
/// anything else fails the build rather than shipping a secret.
99+
fn check_bundled_token() {
100+
println!("cargo:rerun-if-env-changed=MAPBOX_CLI_BUNDLED_TOKEN");
101+
let Ok(token) = std::env::var("MAPBOX_CLI_BUNDLED_TOKEN") else {
102+
return;
103+
};
104+
let token = token.trim();
105+
if !token.is_empty() && !token.starts_with("pk.") {
106+
panic!("MAPBOX_CLI_BUNDLED_TOKEN must be a public pk. token");
107+
}
108+
}
109+
94110
/// New API surface lands in `openapi-specs` at something closer to a monthly
95111
/// rate, and the maintainer-only sync that regenerates `openapi/` from it
96112
/// runs weekly, so two weeks without one is already long enough to be worth a

‎src/auth.rs‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -479,8 +479,9 @@ fn credentials_path_readonly(profile: Option<&str>) -> Option<PathBuf> {
479479
/// directory as a side effect of reading it — see
480480
/// [`credentials_path_readonly`]. What [`profiles`] reads each stored
481481
/// profile through, since listing what exists must not be the reason a
482-
/// directory starts to exist or its permissions change.
483-
fn load_credentials_readonly(profile: Option<&str>) -> Option<Credentials> {
482+
/// directory starts to exist or its permissions change, and what
483+
/// [`crate::cli_token`] reads the login through for the same reason.
484+
pub(crate) fn load_credentials_readonly(profile: Option<&str>) -> Option<Credentials> {
484485
let data = std::fs::read_to_string(credentials_path_readonly(profile)?).ok()?;
485486
serde_json::from_str(&data).ok()
486487
}
@@ -717,7 +718,7 @@ pub fn warn_if_environment_token_shadows_login(profile: Option<&str>, remedy: &s
717718
}
718719
}
719720

720-
fn token_needs_refresh(token: &str) -> bool {
721+
pub(crate) fn token_needs_refresh(token: &str) -> bool {
721722
match token_expires_at(token) {
722723
Some(exp) => {
723724
let now = std::time::SystemTime::now()

0 commit comments

Comments
 (0)