Skip to main content

Anonymous telemetry

A self-hosted installation sends one anonymous ping per day. It is on by default, and one environment variable turns it off:

TELEMETRY_DISABLED: "true"

That is the whole feature. The rest of this page exists so that nothing about it is a surprise: what leaves your server, what never leaves it, when, where, and how to check all of it yourself without taking this page's word for it.

Why it exists

Usage of the hosted console can be counted, because it runs on our servers. Self-hosted installations cannot: a Docker Hub pull count says how many times an image was downloaded, not how many organizations are running the orchestrator in production. Without that number, decisions about which version to keep supporting, when to drop a Node.js version, or whether a feature is used at all are guesswork.

The ping answers three questions and no others: does this installation exist, which version is it on, and is it actually being used?

What is sent

This is a complete ping, read out of a running installation. There is no other field, no nested object and no free text:

{
"installationId": "cd937a0c-e51a-4b72-85c6-b6aec3dca2cd",
"version": "1.0.0",
"nodeVersion": "24.18",
"projects": 1,
"microfrontends": 0,
"environments": 0,
"users": 3,
"deploymentsLastWeek": 0
}
FieldMeaning
installationIdRandom UUID generated by your installation on its first ping and stored in your own database. See The installation id.
versionVersion of the orchestrator, so we know which releases are still alive.
nodeVersionMajor and minor of the Node.js runtime, so we know when a Node version is safe to drop.
projectsHow many projects exist. A count, never a name.
microfrontendsHow many microfrontends exist. A count, never a name, a URL or a version.
environmentsHow many environments exist. A count, never a name or a domain.
usersHow many accounts exist. A count, never an email or a name.
deploymentsLastWeekDeployments performed in the last 7 days. This is what distinguishes a live installation from one that was merely installed.

The request also carries a User-Agent of mfe-orchestrator/<version>, like any HTTP client.

:::note version reads the backend package version, which lags the image tag In the 4.0.0 image that field still reports 1.0.0, as the payload above shows: it is read from the backend's own package.json, which is not bumped with the release tag. It is the one field in the payload that currently says less than it looks like it says. :::

What is never sent

Not "is not currently sent", but "there is no code that could send it":

  • No names — no project, microfrontend, environment, user, company or host name
  • No emails, no user ids, no database ids
  • No URLs, domains, git remotes or repository names
  • No hostname, no IP address in the payload, no environment variables, no secrets, no configuration values
  • No microfrontend content, no bundles, no deployment contents, no global variables
  • No logs, no stack traces, no error messages
  • No page views, no clicks, no session tracking — there is no client-side tracker, no cookie and no third-party analytics anywhere in the product

The service receiving the ping is an ordinary HTTP service, so it sees the source IP of the request, exactly as any outbound call your server makes does. That IP is not part of the payload, is not stored alongside the ping, and is not used to identify or locate an installation.

When and where

  • 60 seconds after startup, then once every 24 hours while the process runs. The initial delay is what keeps builds, CI runs and smoke tests from being counted as installations.
  • The request has a 5 second timeout and is never retried. Any failure is swallowed: the first failure of each process is logged with its reason and with how to stop trying, later ones only at debug level. Telemetry cannot slow down, block or break the orchestrator.
  • Pings are skipped entirely while the database is unreachable.
  • The destination is https://telemetry.mfe-orchestrator.dev/api/telemetry/self-hosted.

Telemetry only ever travels outwards. The orchestrator exposes no endpoint that accepts pings, from your own instances or from anybody else, and it never fetches anything back — no configuration, no feature flags, no licence check. GET /api/telemetry/status is read-only and describes only what this installation would send.

Pointing it at your own collector

TELEMETRY_ENDPOINT replaces the destination. It is the honest way to satisfy a policy that forbids unreviewed outbound traffic without turning the feature off: point it at something of yours, read what arrives, and decide afterwards.

TELEMETRY_ENDPOINT: "http://collector.internal:9000/telemetry"

Anything that accepts a JSON POST works — the payload above is the entire body. A netcat listener is enough to see it once.

How to turn it off

Any one of these is enough:

WhereHow
docker-compose.yamladd TELEMETRY_DISABLED: "true" to the mfe-orchestrator environment:
docker run-e TELEMETRY_DISABLED=true
.env, or any hostTELEMETRY_DISABLED=true
Machine-wide, across toolsDO_NOT_TRACK=1, the consoledonottrack.com convention

Turning it off restarts nothing but the orchestrator itself, and needs no account, licence or online check: the switch is local and unconditional. A disabled installation is not treated differently in any way — no nagging to re-enable it, no reduced functionality, no check of whether you disabled it.

Full precedence

Every switch understands true/false, 1/0, yes/no and on/off. A value that cannot be read — TELEMETRY_DISABLED=disabled, say — is ignored and logged as a warning at startup, so a typo never silently leaves telemetry on.

The first rule that matches wins:

#ConditionResultWhy
1TELEMETRY_ENABLED is set to a value that can be readits valueAn explicit choice wins, in both directions
2TELEMETRY_DISABLED is trueoffThe documented opt-out
3DO_NOT_TRACK is trueoffCross-tool convention
4NODE_ENV is set to anything other than prodoffDeveloper machines, CI and test runs are not installations
5none of the aboveonOpt-out default

:::caution Rule 4 does not save the shipped container The published image sets no NODE_ENV at all, and an unset NODE_ENV counts as prod. So a plain docker compose up of the published compose file lands on rule 5 and telemetry is on. Rule 4 protects a local pnpm dev and a CI job, not a deployment. :::

The variables

VariableDefaultDescription
TELEMETRY_DISABLED(unset)true turns telemetry off.
TELEMETRY_ENABLED(unset)Explicit override in both directions, wins over everything else.
DO_NOT_TRACK(unset)1 turns telemetry off.
TELEMETRY_ENDPOINThttps://telemetry.mfe-orchestrator.dev/api/telemetry/self-hostedWhere the ping goes.
TELEMETRY_INTERVAL_HOURS24Hours between pings. Values below 1 are raised to 1.

How to check all of this yourself

1. Read the startup log. Every start prints the whole disclosure, no docs required:

Anonymous telemetry is ENABLED (every 24h to https://telemetry.mfe-orchestrator.dev/api/telemetry/self-hosted)
What we send, and nothing else: installationId, version, nodeVersion, projects, microfrontends, environments, users, deploymentsLastWeek
No names, no emails, no URLs, no hostnames, no IPs, no project or microfrontend content
Inspect the exact payload of this installation: GET /api/telemetry/status
Turn it off: TELEMETRY_DISABLED=true

When it is off, one line says so and names the reason:

Anonymous telemetry is disabled (TELEMETRY_DISABLED is set)

The first ping of each process is also logged in full, so the bytes that left your server are in your own logs.

2. Ask the running installation. GET /api/telemetry/status, with the token of any signed-in user, returns the configuration and the payload as it would be sent right now:

{
"enabled": false,
"reason": "TELEMETRY_DISABLED is set",
"endpoint": "https://telemetry.mfe-orchestrator.dev/api/telemetry/self-hosted",
"intervalHours": 24,
"payload": { "installationId": "cd937a0c-...", "version": "1.0.0", "...": "..." },
"disableWith": "TELEMETRY_DISABLED=true",
"documentationUrl": "https://github.com/mfe-orchestrator/mfe-orchestrator/blob/main/docs/TELEMETRY.md"
}

reason is the rule from the precedence table that actually decided, which makes it the quickest way to confirm that the variable you set is the one taking effect. payload is filled in even when telemetry is off — it answers what would be sent, not what was sent — and is null only when the database is not connected.

3. Watch the wire. Point TELEMETRY_ENDPOINT at a local listener and read what arrives. It will be the JSON above, byte for byte.

The installation id

installationId is a random UUID. It is not derived from your hostname, MAC address, licence, domain, database name or anything else about you — it carries no information beyond "these pings come from the same place".

It is generated on first use and stored in your own database, in the configurations collection under the name TELEMETRY_INSTALLATION_ID. You can look at it, and you can delete it: the installation then simply looks like a new one on the next ping. Wiping the database resets it too.

Its only purpose is to avoid counting one installation as thousands — without a stable id, "how many installations are alive" is unanswerable, which is the entire reason this feature exists.

The commitments behind it

  1. The payload only changes in public. Any new field lands in the payload type, in this page and in the startup log in the same release, and is listed in the changelog.
  2. No personal data, ever. Not "anonymized", not "pseudonymized": not collected.
  3. No dark patterns. No nagging, no reduced functionality when it is off, no online activation, no check of whether you disabled it.
  4. Aggregate use only. The data drives release support, Node.js version support and product decisions.

If something on this page turns out to be wrong, or a field says more about you than is claimed here, please open an issue.