# Telemetry

> Software telemetry is code that sends usage and performance data back to its developers. It uses the same mechanism as data exfiltration.

Also known as: Software telemetry, Usage analytics, Phone-home code, Product analytics

Source: https://socket.dev/glossary/telemetry

Last updated: 2026-11-09

Software telemetry, also called usage analytics or phone-home code, is code that collects data about how a program is used and sends it to a server its developers control. In open source packages it is often on by default and can run at install time. Telemetry and data exfiltration share one mechanism; consent and payload content separate them.

The mechanism is ordinary: read some local state, serialize it, make an HTTPS request. What makes telemetry a supply chain topic is that the code arrives inside a dependency, runs with your user's privileges, and decides for itself what "local state" means. Our position is simple. Telemetry in a dependency should be opt-in, should never run at install time, and should be judged by what its payload contains, not by what the code calls itself. Malware authors label their exfiltration code `trackingData` and `trackKeyDerivation`, and we have the payloads to prove it.

## What is software telemetry?

Software telemetry is data a program reports about its own use and behavior back to its maintainers. The usual contents are counters (which commands ran, how often), environment facts (OS, CPU, runtime version, whether it ran in CI), timings, and crash signatures. It exists because maintainers are otherwise blind. Bug reports only arrive when something visibly breaks, and most users never file one.

Russ Cox made the strongest case for open source telemetry in his 2023 [transparent telemetry series](https://research.swtch.com/telemetry-intro), arguing that "the choice between invasive tracking and doing nothing at all is a false dichotomy, and it's harming open source." His lead example was a macOS bug in the `go` command that "no one reported" over three years of releases, because users assumed the behavior was normal. The design he proposed used counters instead of traces, no stable identifiers, sampled uploads, and fully public raw data. After community feedback, Go switched the design to opt-in before it shipped.

As of Go 1.23, the [Go toolchain](https://go.dev/doc/telemetry) defaults to `local` mode, which writes counters under `os.UserConfigDir()/go/telemetry` and never uploads them. Uploading requires `go telemetry on`. The cost of opt-in is a small sample. In September 2024 the Go team reported that after prompting [5% of VS Code Go users](https://go.dev/blog/gotelemetry), uploads had grown to "around 1800 weekly participants." The Go team says that sample still surfaced real crashes. A smaller sample is the honest price of asking first.

Most JavaScript tooling made the other choice. Next.js, Astro, Gatsby, Storybook, Turborepo, and Prisma all collect by default. Several print a one-time notice instead of asking. A notice is not consent when the command runs in a CI job nobody reads.

## How does telemetry get into your dependency tree?

Telemetry enters a project at three layers: the package manager, the frameworks and CLIs you invoke directly, and libraries you never chose, pulled in as a [transitive dependency](https://socket.dev/glossary/transitive-dependency). Each layer collects different data and needs a different control.

### The package manager reports on every request

Package managers send environment details to the registry on every metadata request. We pointed npm 10.9.9, pnpm 10.34.9, and [pip](https://socket.dev/glossary/pip-python-package-index) 26.2.1 at a local HTTP server that logs request headers, then ran an install. This is what the registry sees:

```text
# npm install left-pad, with CI=true and GITHUB_ACTIONS=true set
user-agent: npm/10.9.9 node/v22.23.4 linux x64 workspaces/false ci/github-actions
npm-auth-type: web
npm-command: install

# pnpm add left-pad
user-agent: pnpm/10.34.9 npm/? node/v22.23.4 linux x64

# pip download requests, with CI=true set
User-Agent: pip/26.2.1 {"ci":true,"cpu":"x86_64","distro":{"id":"noble",
  "libc":{"lib":"glibc","version":"2.39"},"name":"Ubuntu","version":"24.04"},
  "implementation":{"name":"CPython","version":"3.13.18"},
  "installer":{"name":"pip","version":"26.2.1"},
  "openssl_version":"OpenSSL 3.5.9 29 Sep 2026","python":"3.13.18",
  "rustc_version":"1.97.0","setuptools_version":"84.0.0",
  "system":{"name":"Linux","release":"6.18.44-fc-v310"}}
```

pip's user agent is a JSON telemetry record. To fill in `rustc_version`, pip spawns `rustc --version` with a 0.5 second timeout whenever `rustc` is on the `PATH`. The `ci` field is set if any of four environment variables exists (`BUILD_BUILDID`, `BUILD_ID`, `CI`, `PIP_IS_CI`), and pip's source says the list exists "to provide some indication of and lower bound for CI traffic to PyPI." PyPI's Linehaul project streams these download logs into the public [`bigquery-public-data.pypi.file_downloads`](https://docs.pypi.org/api/bigquery/) dataset.

None of this is secret or malicious. It is coarse, documented, and goes only to the registry you already trust with your dependency list. npm lets you override the string with its `user-agent` config setting, but it still sends `npm-command`, and pip has no switch to drop its fields. That is a useful baseline: your installs are never fully silent, even before any package code runs.

### Install-time telemetry in a library you never chose

The more interesting layer is a library that starts collecting the moment it lands in `node_modules`. `@mui/x-telemetry` is a good case study because it is legitimate, documented, and pulled in transitively: `@mui/x-data-grid-pro` depends on `@mui/x-license`, which depends on `@mui/x-telemetry`. Version 9.17.0 declares `"postinstall": "node ./postinstall/index.mjs"`.

We installed it with npm 10 inside a scratch Git repository whose `origin` remote was `https://github.com/acme/secret-internal-app.git`. The [install script](https://socket.dev/glossary/postinstall-scripts) ran `git config --local --get remote.origin.url` and `git rev-parse --show-toplevel`, read the machine ID (via `node-machine-id`, then `/etc/machine-id`), hashed each value, and wrote the result into the package directory:

```javascript
export default {
  "config": { "isInitialized": true },
  "traits": {
    "isDocker": false,
    "isCI": false,
    "machineId": "87d696c166932a34f2557ca3ce5f97d7a75f0b1e1bf494033120a8e5349275aa",
    "repoHash": "4ed443f0550b8b6c41e10de633058d53c1d0f48a21a3033fb6fe9d900b1d2e00",
    "postinstallPackageNameHash": "19d0d46380da5c4253d05453878d9c4119d9c29b946e713f187404462a94437b",
    "rootPathHash": "0108d3c23da46724d516d1f0d97f0cb175e9c8074b142d137293140c902a07a3",
    "projectId": "4ed443f0550b8b6c41e10de633058d53c1d0f48a21a3033fb6fe9d900b1d2e00",
    "sessionId": "ce90f9c4b0695d6abf2515781949fd16cbd95471560a6ba4890670fe89e5f9c2",
    "anonymousId": "66ad812682c5315ef7cd2c0d4c60e8ff640160ed8493d603bf12b6c57c3ac6f0"
  }
};
```

The `repoHash` is a plain, unsalted SHA-256 of the remote URL. Anyone can recompute it:

```bash
$ printf %s https://github.com/acme/secret-internal-app.git | sha256sum
4ed443f0550b8b6c41e10de633058d53c1d0f48a21a3033fb6fe9d900b1d2e00  -
```

Nothing leaves the machine at install time. Transmission happens later, in the browser, in development builds only. When a Pro or Premium component verifies its license, the runtime merges these traits with a FingerprintJS browser fingerprint (fonts, canvas, audio, video card, and more) and the license key, then POSTs the event to `https://x-telemetry.mui.com/v2/telemetry/record`. MUI's [telemetry guide](https://mui.com/x/guides/telemetry/) documents these fields, calls the data "pseudonymous," and says the purpose includes license compliance. The runtime skips sending in CI, in production builds, and when `MUI_X_TELEMETRY_DISABLED=true` is set.

Two details matter for anyone writing a policy. First, the opt-out variable gates transmission only. We set `MUI_X_TELEMETRY_DISABLED=true` and reinstalled; the postinstall still ran Git, read the machine ID, and wrote `context.mjs` and `~/.config/mui-x/config.json`. Second, an unsalted hash of a guessable string is a pseudonym, not anonymization. Repository URLs are guessable, and public ones are enumerable.

> Figure 1. In @mui/x-telemetry 9.17.0, the opt-out stops sending, not collecting.
>
> The two phases of @mui/x-telemetry: hashes computed at install time, sent later from the browser in development

Figure 1, in text. At install time, the `@mui/x-telemetry` postinstall runs `git config --local --get remote.origin.url` and `git rev-parse --show-toplevel`, reads the machine ID, computes SHA-256 hashes, and writes `context.js`, `context.mjs`, and `~/.config/mui-x/config.json`. It sends nothing, and `MUI_X_TELEMETRY_DISABLED` does not stop it. In a development build running in the browser, a license check triggers the runtime, which adds a FingerprintJS fingerprint, a session ID, and the license key, then POSTs to `x-telemetry.mui.com`. That second step is skipped in CI, in production builds, and when the opt-out is set.

Not every project hashes the same way, and the difference is the whole point of hashing. We read the project-ID code in three packages:

| Package (version checked) | Raw value hashed | Salt | Can the vendor reverse it? |
| --- | --- | --- | --- |
| next 16.4.5 | git config --local --get remote.origin.url | 16 random bytes per user, stored locally and never sent | No. The source says this makes it "impossible for us to reverse or try to guess the remote." |
| @astrojs/telemetry 3.4.2 | First commit SHA from git rev-list --max-parents=0 HEAD | None | For public repos, yes. Root commit SHAs are public. |
| @mui/x-telemetry 9.17.0 | Git remote URL, package name, repo root path | None | For any URL it can guess, yes. |

Verdict: a salted hash with a locally stored, never-transmitted salt is the only one of these that behaves like an anonymous project ID. The other two are stable, linkable identifiers that happen to be written in hexadecimal.

## Where does telemetry end and data exfiltration begin?

Telemetry becomes data exfiltration when code collects data the user did not agree to share and that the stated purpose does not need. The network call is identical in both cases. The difference lives in three places: what the code reads, when it runs, and whether the user had a real chance to say no.

Our research team has documented the far end of this spectrum many times. In April 2024 we analyzed [React ReExt](https://socket.dev/blog/npm-package-for-reext-react-components-library-exfiltrates-git-credentials), a component library that peaked near 11,000 downloads in one October 2023 week. Its `preinstall.js` read `~/.gitconfig` and sent the OS username, Git name, and Git email as query parameters to `2tak.l.serverhost.name:1962`. Asked about it, the author told us "we do gather developer information, but only on developer consent." The versions we examined showed no consent prompt.

In May 2025 we found 60 [malicious packages](https://socket.dev/glossary/malicious-package) on npm in a [host-fingerprinting campaign](https://socket.dev/blog/60-malicious-npm-packages-leak-network-and-host-data) across three accounts with more than 3,000 combined downloads. Every one ran the same install-time script, and the author called the payload what it pretended to be:

```javascript
const trackingData = JSON.stringify({            // Builds large JSON blob:
     package, directory: __dirname, home_directory: os.homedir(),
     username: os.userInfo().username, dns: dns.getServers(),
     internal_hostname: os.hostname(), internal_ip: getIPAddress(),
     external_ip: ext.ip, external_hostname: ext.hostname, organization: ext.org,
     resolved_url: packageJSON.___resolved, package_version: packageJSON.version,
     package_json: packageJSON, package_type: 'npm'
});
```

The script skipped AWS, GCP, and known research sandboxes, then posted to a Discord webhook. Real telemetry wants the most representative sample it can get. Code that hides from analysis environments is not measuring usage.

Legitimate tools cross the line too. In February 2025, [devenv 1.4](https://socket.dev/blog/devenv-faces-backlash-over-ai-driven-telemetry-in-version-1-4-1) shipped an AI `generate` command that, in NixOS committer Martin Weinelt's words, "tars up all files it can find through git ls-files -z" and uploads them. The maintainer removed the feature after community backlash. Nothing about the author's intent changed the fact that a repository left the machine without consent.

The hardest case is the [Injective SDK compromise](https://socket.dev/blog/compromised-injective-sdk-npm-package) in June 2026. `@injectivelabs/sdk-ts@1.20.21`, a package with about 50,000 weekly downloads, shipped what looked like telemetry: `fromMnemonic` and `fromHex` called a new `trackKeyDerivation` function. That function recorded the full mnemonic or private key material, base64-encoded it, and POSTed it to an Injective-owned testnet endpoint. It ran at call time, not install time, and the destination was the vendor's own infrastructure. The malicious commits came through a hijacked GitHub account with a real contribution history, a variant of [maintainer account takeover](https://socket.dev/glossary/maintainer-account-takeover), and the attacker pinned 17 more `@injectivelabs` packages to the bad version. Functionally it was an [infostealer](https://socket.dev/glossary/infostealer) wearing a telemetry label.

> Figure 2. The network call is the same in every row. What the code reads and when it runs is what changes.
>
> Five real cases on a spectrum from consented telemetry to data exfiltration

Figure 2, in text. Five cases ordered from consented telemetry to exfiltration. Go toolchain: counters only, upload opt-in, runs when you use the toolchain. Next.js: command, version, and machine facts with a salted project hash, on by default with an env var opt-out, runs on CLI use. `@mui/x-telemetry`: unsalted repo and machine hashes plus a browser fingerprint and license key, on by default with an opt-out that gates sending only, hashes computed at install. React ReExt: OS username and Git name and email, no disclosure or consent, runs at preinstall. `@injectivelabs/sdk-ts@1.20.21`: wallet mnemonics and private keys, no disclosure or consent, runs when keys are derived, sent to the vendor's own domain.

The pattern across these cases gives a test we apply in analysis:

| Signal | Telemetry | Data exfiltration | Can it be faked? |
| --- | --- | --- | --- |
| Function or variable name | trackEvent, telemetry | trackingData, trackKeyDerivation | Yes, trivially |
| Destination | First-party, documented endpoint | Webhook, paste site, or the vendor's own domain | Yes. Injective used vendor infrastructure. |
| Disclosure | README, docs page, first-run notice | None, or a vague line in the README | Partly |
| When it runs | First use of a CLI or dev server | preinstall/postinstall, or a hot code path | No, observable |
| What it reads | Counters, versions, OS family, hashed IDs | ~/.gitconfig, env vars, keys, DNS servers, home paths | No, observable |
| Sandbox evasion | None. Wants a representative sample. | Skips cloud and analysis hosts | No, observable |

Verdict: ignore names and destinations. Classify by what the code reads and when it executes, because those are the only two properties an author cannot disguise without changing what the code does. This is why we analyze what a package actually does instead of what its metadata or comments claim.

Socket's [Telemetry alert](https://socket.dev/alerts/telemetry) (key `telemetry`, High severity) fires when package code tracks how the package is used. It sits beside separate capability alerts for network access, environment variable access, and install scripts, and the combination is the signal: a telemetry module that also reads environment variables in an install script deserves a human. In our [Balanced default policy](https://docs.socket.dev/docs/security-policy-default-enabled-alerts), Telemetry is set to Monitor. It moved from Block to Monitor on August 28, 2024, as part of a [rework of the default policies](https://socket.dev/blog/announcing-new-default-security-policies) aimed at reducing alert noise. Known malware stays on Block.

## How do you disable telemetry in npm packages?

You disable telemetry in npm packages one tool at a time, using each tool's own environment variable or CLI command. No universal switch exists. The `DO_NOT_TRACK=1` [environment variable](https://socket.dev/glossary/environment-variables) is the closest thing to a standard, and most of the JavaScript packages we checked ignore it.

We unpacked current releases and grepped their source for the variables they actually read, as of November 2026:

| Package (version checked) | Opt-out | Reads DO\_NOT\_TRACK? | Default |
| --- | --- | --- | --- |
| next 16.4.5 | NEXT\_TELEMETRY\_DISABLED=1 or next telemetry disable | No | On |
| astro 7.4.7 (@astrojs/telemetry 3.4.2) | ASTRO\_TELEMETRY\_DISABLED=1 or astro telemetry disable | No | On |
| @nuxt/telemetry 2.9.1 | NUXT\_TELEMETRY\_DISABLED=1 | Yes | Off until you accept a prompt |
| gatsby-telemetry 4.14.0 | GATSBY\_TELEMETRY\_DISABLED=1 | No | On |
| storybook 10.6.6 | STORYBOOK\_DISABLE\_TELEMETRY=1 | No | On |
| turbo 2.11.10 | TURBO\_TELEMETRY\_DISABLED=1 or turbo telemetry disable | Yes, per its bundled docs | On |
| Prisma (checkpoint-client 1.1.33) | CHECKPOINT\_DISABLE=1 | No | On |
| @mui/x-telemetry 9.17.0 | MUI\_X\_TELEMETRY\_DISABLED=true (framework prefixes also work) | No | On in development; hashes computed at install regardless |
| huggingface\_hub 2.4.0 (PyPI) | HF\_HUB\_DISABLE\_TELEMETRY=1 | Yes | On |

Verdict: set `DO_NOT_TRACK=1` everywhere, because it is free, and then set each tool's own variable, because `DO_NOT_TRACK` alone silences only three of the nine packages above. Env var opt-outs also stop at the boundary of what the author chose to gate. The MUI postinstall shows that "disabled" can mean "not sent" rather than "not collected."

Install-time collection needs a different control: do not run dependency install scripts. As of [npm v12](https://socket.dev/blog/npm-12), that is the default. We installed `@mui/x-telemetry` with npm 12.2.2 and got:

```text
npm warn install-scripts 1 package had install scripts blocked because they are not covered by allowScripts:
npm warn install-scripts   @mui/x-telemetry@9.17.0 (postinstall: node ./postinstall/index.mjs)
npm warn install-scripts Run `npm install-scripts ls` to review, or `npm install-scripts approve <pkg>` to allow.
```

The generated `context.mjs` stayed at `isInitialized: false`, with no hashes. npm v12 made dependency lifecycle scripts opt-in in July 2026, and [pnpm 10 blocked lifecycle scripts by default](https://socket.dev/blog/pnpm-10-0-0-blocks-lifecycle-scripts-by-default) in January 2025. Default-on install scripts were a design mistake, and telemetry is one of the quieter things that mistake allowed. On older npm versions, `--ignore-scripts` or `ignore-scripts=true` in `.npmrc` gets the same effect, at the cost of breaking packages that genuinely need a build step.

Blocking scripts does not touch runtime telemetry. Next.js, Astro, and the MUI browser runtime all collect on use, and the Injective stealer ran on a function call. For CI, the reliable backstop is egress control: build jobs that can only reach your registry or [package firewall](https://socket.dev/glossary/package-firewall) cannot phone home to anyone, whatever the code decides. That breaks anything that downloads binaries during install, so it needs an allowlist, and it does nothing for developer laptops.

## Telemetry needs consent that installs cannot ask for

The deepest problem with dependency telemetry is that `npm install` is non-interactive by design. It runs in CI, in Docker builds, in editor background tasks, and increasingly inside coding agents. There is nobody to prompt, so "on by default with a notice" quietly becomes "on." Nuxt's consent prompt works because a human is at the dev server. Go's opt-in works because a human runs `go telemetry on` once, or answers an editor prompt, and the choice persists.

The package manager is the right place to fix this, and npm v12 already did half of it. Install scripts now need approval. The approval list says which packages have scripts and what command each runs, but not what the script does once it runs. A community RFC proposes a review mode that would surface whether a script makes network calls or reads environment variables. That is the missing half. A dependency that wants to measure you should have to say so in a place the package manager can enforce, and the honest default for code you never chose is that it measures nothing.
