diff --git a/crates/pixi_cli/src/self_update.rs b/crates/pixi_cli/src/self_update.rs index 741043d86e..0a9c1d0eb9 100644 --- a/crates/pixi_cli/src/self_update.rs +++ b/crates/pixi_cli/src/self_update.rs @@ -58,6 +58,45 @@ fn user_agent() -> String { format!("pixi {}", consts::PIXI_VERSION) } +/// Send a best-effort anonymous ping after a successful self-update, tagging +/// the OS, architecture and target version. It never blocks or fails the +/// update (short timeout, all errors ignored) and is skipped when +/// `PIXI_NO_TELEMETRY` or `DO_NOT_TRACK` is set. +async fn send_update_ping( + client: &reqwest_middleware::ClientWithMiddleware, + target_version: Option<&Version>, +) { + if std::env::var_os("PIXI_NO_TELEMETRY").is_some() || std::env::var_os("DO_NOT_TRACK").is_some() + { + return; + } + + let version = target_version + .map(|v| v.to_string()) + .unwrap_or_else(|| "latest".to_string()); + + // Encode the metadata as a synthetic page URL. Scarf reports on the `Page` + // dimension (normally inferred from the referrer), so each event/version/ + // platform combination shows up as its own page in the dashboard. + let page = format!( + "https://pixi.sh/ping/self-update/{}/{}-{}", + version, + std::env::consts::OS, + std::env::consts::ARCH, + ); + + let _ = client + .get(consts::INSTALL_PING_URL) + .query(&[ + ("x-pxid", consts::INSTALL_PING_PXID), + ("Page", page.as_str()), + ]) + .header("User-Agent", user_agent()) + .timeout(std::time::Duration::from_secs(3)) + .send() + .await; +} + fn default_archive_name() -> Option { if cfg!(target_os = "macos") { if cfg!(target_arch = "x86_64") { @@ -410,6 +449,9 @@ pub async fn execute(args: Args, global_options: &GlobalOptions) -> miette::Resu tracing::warn!(fetch_release_warning); } + // Best-effort anonymous ping; must not affect the update result. + send_update_ping(&client, target_version.as_ref()).await; + Ok(()) } diff --git a/crates/pixi_consts/src/consts.rs b/crates/pixi_consts/src/consts.rs index 7b36abf699..76a6cf7da4 100644 --- a/crates/pixi_consts/src/consts.rs +++ b/crates/pixi_consts/src/consts.rs @@ -124,6 +124,11 @@ pub const RELEASES_API_BY_TAG: &str = "https://api.github.com/repos/prefix-dev/p pub const RELEASES_API_LATEST: &str = "https://api.github.com/repos/prefix-dev/pixi/releases/latest"; +/// Endpoint for the anonymous installation/update ping. The same pixel id is +/// used by the install scripts (`install/install.sh`, `install/install.ps1`). +pub const INSTALL_PING_URL: &str = "https://installation-ping.prefix.dev/a.png"; +pub const INSTALL_PING_PXID: &str = "21354c5b-2936-42bc-9d4b-9d6253815afd"; + pub const CLAP_CONFIG_OPTIONS: &str = "Config Options"; pub const CLAP_GIT_OPTIONS: &str = "Git Options"; pub const CLAP_GLOBAL_OPTIONS: &str = "Global Options"; diff --git a/docs/installation.md b/docs/installation.md index 5088f66281..c5df2bfc57 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -131,6 +131,8 @@ its [compile steps](https://github.com/conda/rattler/tree/main#give-it-a-try). | `PIXI_DOWNLOAD_URL` | Overrides the download URL for the Pixi binary (useful for mirrors or custom builds). | GitHub releases, e.g. [linux-64](https://github.com/prefix-dev/pixi/releases/latest/download/pixi-x86_64-unknown-linux-musl.tar.gz) | | `NETRC` | Path to a custom `.netrc` file for authentication with private repositories. | | | `TMP_DIR` | The temporary directory the script uses to download to and unpack the binary from. | `/tmp` | + | `PIXI_NO_TELEMETRY` | If set, the script will not send the anonymous installation ping (see note below). | | + | `DO_NOT_TRACK` | Same as `PIXI_NO_TELEMETRY`; honors the common `DO_NOT_TRACK` convention. | | For example, on Apple Silicon, you can force the installation of the x86 version: ```shell @@ -185,6 +187,8 @@ its [compile steps](https://github.com/conda/rattler/tree/main#give-it-a-try). | `PIXI_HOME` | The location of the installation. | `$Env:USERPROFILE\.pixi` | | `PIXI_NO_PATH_UPDATE`| If set, the `$PATH` will not be updated to add `pixi` to it. | `false` | | `PIXI_DOWNLOAD_URL` | Overrides the download URL for the Pixi binary (useful for mirrors or custom builds). | GitHub releases, e.g. [win-64](https://github.com/prefix-dev/pixi/releases/latest/download/pixi-x86_64-pc-windows-msvc.zip) | + | `PIXI_NO_TELEMETRY` | If set, the script will not send the anonymous installation ping (see note below). | | + | `DO_NOT_TRACK` | Same as `PIXI_NO_TELEMETRY`; honors the common `DO_NOT_TRACK` convention. | | For example, set the version: ```powershell @@ -202,6 +206,12 @@ its [compile steps](https://github.com/conda/rattler/tree/main#give-it-a-try). !!! tip "Security Note" The PowerShell install script automatically masks any credentials embedded in the download URL when displaying messages, replacing them with `***:***@` to prevent credentials from appearing in logs or console output. +!!! note "Anonymous installation ping" + After a successful install, the script sends a single anonymous request that helps us estimate how many installations happen. It contains no personal data and is best-effort: it has a short timeout and never blocks or fails the installation. To disable it, set `PIXI_NO_TELEMETRY=1` or `DO_NOT_TRACK=1` before running the script: + ```shell + curl -fsSL https://pixi.sh/install.sh | PIXI_NO_TELEMETRY=1 bash + ``` + ## Autocompletion To get autocompletion follow the instructions for your shell. diff --git a/install/install.ps1 b/install/install.ps1 index 9fd7fadde5..81f64ff585 100644 --- a/install/install.ps1 +++ b/install/install.ps1 @@ -211,6 +211,22 @@ try { Remove-Item -Path $ZIP_FILE } +# Send an anonymous installation ping (best-effort, never fails the install). +# Opt out by setting PIXI_NO_TELEMETRY or DO_NOT_TRACK. +if (-not $Env:PIXI_NO_TELEMETRY -and -not $Env:DO_NOT_TRACK) { + try { + $PingArch = if ($ARCH -like 'aarch64*') { 'aarch64' } elseif ($ARCH -like 'i686*') { 'i686' } else { 'x86_64' } + # Metadata is encoded into the `Page` query parameter so it shows up as a + # distinct page in Scarf. Invoke-WebRequest URL-encodes the parameter value. + $Page = "https://pixi.sh/ping/install/$PixiVersion/windows-$PingArch" + $PingBase = 'https://installation-ping.prefix.dev/a.png' + $PingUrl = "$PingBase`?x-pxid=21354c5b-2936-42bc-9d4b-9d6253815afd&Page=$([uri]::EscapeDataString($Page))" + Invoke-WebRequest -Uri $PingUrl -UseBasicParsing -TimeoutSec 3 | Out-Null + } catch { + # Ignore telemetry errors + } +} + # Add pixi to PATH if the folder is not already in the PATH variable if (!$NoPathUpdate) { $PATH = Get-Env 'PATH' diff --git a/install/install.sh b/install/install.sh index 8ed02c90f6..a8e4bc9a51 100644 --- a/install/install.sh +++ b/install/install.sh @@ -196,6 +196,26 @@ __wrap__() { echo "The 'pixi' binary is installed into '${PIXI_BIN_DIR}'" + # Send an anonymous installation ping (best-effort, never fails the install). + # Opt out by setting PIXI_NO_TELEMETRY or DO_NOT_TRACK. + if [ -z "${PIXI_NO_TELEMETRY:-}" ] && [ -z "${DO_NOT_TRACK:-}" ]; then + case "$PLATFORM" in + apple-darwin) OS_TAG="macos" ;; + *linux*) OS_TAG="linux" ;; + *windows*) OS_TAG="windows" ;; + *) OS_TAG="unknown" ;; + esac + # Metadata is encoded into the (pre-encoded) `Page` query parameter so it + # shows up as a distinct page in Scarf; '%2F' are the path separators. + PAGE="https%3A%2F%2Fpixi.sh%2Fping%2Finstall%2F${VERSION}%2F${OS_TAG}-${ARCH}" + PING_URL="https://installation-ping.prefix.dev/a.png?x-pxid=21354c5b-2936-42bc-9d4b-9d6253815afd&Page=${PAGE}" + if hash curl 2>/dev/null; then + curl -sSL --max-time 3 "$PING_URL" >/dev/null 2>&1 || true + elif hash wget 2>/dev/null; then + wget -q -T 3 -O /dev/null "$PING_URL" >/dev/null 2>&1 || true + fi + fi + # shell update can be suppressed by `PIXI_NO_PATH_UPDATE` env var if [ -n "${PIXI_NO_PATH_UPDATE:-}" ]; then echo "No path update because PIXI_NO_PATH_UPDATE is set"