telemetry · API reference

Telemetry API

Anonymous usage telemetry for CLIs, desktop apps, plugins, web apps and servers. Free, self-hostable, runs on Cloudflare Workers. Raw events live 90 days in Workers Analytics Engine. Only daily aggregates are kept long term.

Base URL: https://telemetry.anyr.workers.dev

Quickstart

  1. Pick a slug and send the first event with curl. A new slug is created by its first valid event. There is no signup step.
curl -X POST https://telemetry.anyr.workers.dev/my-tool \
  -H 'content-type: application/json' \
  -d '{"event":"ping","id":"3f2c9a7e-1d4b-4c8e-9a52-7b1e0d6f4a21","version":"1.0.0","os":"linux","arch":"x64"}'
# {"accepted":1,"dropped":0,"project":{"slug":"my-tool","created":true,"admin_token":"tk_...","page_url":"https://telemetry.anyr.workers.dev/my-tool"}}
  1. Save admin_token from that response. It is shown once. You need it to rename the project, rotate the token, import or delete.

  2. Send more events the same way. Later responses are {"accepted":1,"dropped":0}, with no token.

  3. See it. Open https://telemetry.anyr.workers.dev/my-tool, or read the JSON:

curl https://telemetry.anyr.workers.dev/api/my-tool/stats

Today's numbers come from a live query cached for about 5 minutes. Earlier days come from the daily rollup.

Sending events

POST https://telemetry.anyr.workers.dev/<project>

The body is one event object, or an array of at most 25 events. Content type application/json. Max body size is 16 KB.

Field Type Required Rules
event string yes Matches ^[A-Za-z0-9_.:/-]{1,64}$.
id string no A random install id that the client creates and keeps, at most 128 chars. The server hashes it with a secret salt and never stores it raw.
version string no The app version. Letters, digits, ., +, -, _, at most 32 chars.
os string no Free-form lowercase, at most 16 chars of [a-z0-9_-]. For example linux, darwin, windows. Send what the runtime reports.
arch string no Same rules as os. x64, x86_64, arm64 and aarch64 are all fine. Do not remap them.
ci boolean no True when the event came from a CI run.
props object no At most 8 entries. Keys match [a-z0-9_.-]{1,32}. Values are string, number or boolean, and are cut to 64 chars.
n integer no Occurrence count, 1 to 1000. Default 1.

Rules the server applies:

  • The country comes from Cloudflare. The client cannot set it.
  • The IP address and user agent are never stored.
  • Any event with an id counts that id as an active user for that UTC day. install and ping are not special. Days are UTC.
  • Without id, the server uses a hash that rotates every day. That counts daily users (DAU) only. Weekly and monthly users need a stable id.
  • A bad optional field (for example an os with spaces) is ignored, and the event is still counted. Only a missing or invalid event drops the event.
  • Invalid events in a batch are dropped. The valid ones are kept.
  • Identical events in one request are merged into one data point with a summed n. accepted counts data points after merging, dropped counts invalid events.

Responses:

Status Meaning
202 Accepted. Body is {"accepted": <number>, "dropped": <number>}. When this request created the project, the body also has project and the token is in the x-telemetry-admin-token header. See Projects below.
400 Body is not valid JSON, is not an object or array, has more than 25 events, or has no valid events.
404 Invalid or reserved slug.
413 Body is larger than 16 KB.
429 Per-IP rate limit (with a Retry-After header in seconds), the project's daily cap, or the new project limit. Do not retry in a loop.

CORS is open (access-control-allow-origin: *), so browsers can send events directly.

Batch example:

curl -X POST https://telemetry.anyr.workers.dev/my-tool \
  -H 'content-type: application/json' \
  -d '[{"event":"run","id":"3f2c9a7e","props":{"command":"build"}},{"event":"run","id":"3f2c9a7e","props":{"command":"test"},"n":3}]'

OTLP

Any OpenTelemetry SDK or collector can send to the same project. Only OTLP/HTTP with JSON is supported.

export OTEL_EXPORTER_OTLP_ENDPOINT=https://telemetry.anyr.workers.dev/my-tool
export OTEL_EXPORTER_OTLP_PROTOCOL=http/json

SDKs append /v1/logs, /v1/traces and /v1/metrics to the endpoint. You can also call them directly:

POST https://telemetry.anyr.workers.dev/<project>/v1/{logs,traces,metrics}

  • Protobuf bodies (any content type other than application/json) get 415. Set OTEL_EXPORTER_OTLP_PROTOCOL=http/json.
  • gzip bodies (content-encoding: gzip) are accepted. Other encodings get 415. A body over 256 KB, or over 1 MB after unzip, gets 413.
  • At most 250 points per request. The success response is {}, or {"partialSuccess": {...}} when records were rejected (invalid event name, empty metric, or over the 250 limit).
  • The first export with at least one valid record creates a new project. The admin token is only in the x-telemetry-admin-token response header, and exporters drop it. Create the project with one curl first (see Quickstart), then point the exporter at it. An export with no valid records to an unknown slug gets 400 and creates nothing.

How OTLP data maps to an event:

Event field Source
event Logs: the event.name attribute, else the log body when it is a valid event name, else log. Traces: the span name. Metrics: the metric name. Names must match the native event rule.
n Metrics only: the sum of a delta sum, the count of a histogram, or the number of data points for a gauge or cumulative sum. Logs and spans count 1.
id Attribute service.instance.id, else host.id, else user.id.
version service.version
os os.type
arch host.arch
props Other record attributes, with the same limits as native props. Attributes that can identify a person or host are dropped (user.*, host.*, client.*, url.*, exception.*, and keys containing email, token, path, session, and similar).

Set the install id as a resource attribute:

export OTEL_RESOURCE_ATTRIBUTES=service.instance.id=<random-install-id>,service.version=1.0.0

Client guidelines

Follow these in every client. They are what makes telemetry safe to ship.

  • A variable counts as set when it has any value except empty, 0 or false. This applies to DO_NOT_TRACK, CI and the <APP>_TELEMETRY_* flags.
  • Honor DO_NOT_TRACK and <APP>_TELEMETRY_DISABLED. Send nothing when either is set. If a config file holds an opt-out and it cannot be read, send nothing.
  • Skip sending in CI unless the user opts in with <APP>_TELEMETRY_CI=1. When opted in, send ci: true.
  • <APP>_TELEMETRY_DEBUG=1 prints the payload and does not send it. It has no side effects: no id file, no daily stamp, no notice.
  • Create a random id once and keep it at ~/.config/<app>/telemetry-id, or the platform equivalent. Never derive it from the machine, user, hostname or path. Prefer the tool's own env prefix and config directory when it has one.
  • Fail closed. If the id or the daily stamp cannot be saved, send nothing. Otherwise a daemon would send on every tick.
  • Send fire-and-forget with a timeout of 1 second or less. Never block the app, never crash it, never retry in a loop.
  • Send install once on first run. Then send at most one ping per UTC day, plus meaningful events such as a command run. Use the UTC date for the daily gate.
  • Show a one-line notice on first run, in the app's user-visible channel: stderr for a CLI, the log or notify channel for a daemon or plugin, the UI for a desktop app. It says what is sent and how to turn it off.
  • Send no personal data. No file paths, hostnames, emails, usernames, arguments values or error messages.

Ready-made snippets for curl, shell, TypeScript, Rust, Go, Python and the browser are on each project page, and in https://telemetry.anyr.workers.dev/<project>/install.md.

Projects

A project is created by its first valid event, native or OTLP. There is no signup step.

  • The slug matches ^[a-z0-9][a-z0-9-]{1,38}$ and is not a reserved word (api, docs, fonts, health, llms.txt, and similar). Other slugs get 404.
  • A request with no valid events never creates a project.
  • At most 3 new projects per IP and 100 in total per UTC day. Over either cap, the request gets 429 {"error": "new project limit reached for today"} and nothing is stored. Existing projects are not affected.
  • The response that creates the project carries the admin token, once: in the native body as project.admin_token, and in the x-telemetry-admin-token header for native and OTLP. Only its hash is stored. It cannot be shown again.
  • Save the token from your first curl. If the first event comes from a client that ignores responses, such as a shipped app or an OTLP exporter, the token is lost.
  • When two requests create the same slug at the same moment, one gets the token and the other is accepted without it.

List the public projects:

curl https://telemetry.anyr.workers.dev/api/projects

The response is {"projects": [...]}. It lists only projects with daily rollups in the last 30 days. Each entry has slug, name, description, homepage, created_at, series (daily users for the last 30 closed UTC days, oldest first) and dau (yesterday's daily users). A new project shows up after its first nightly rollup at 00:10 UTC. Its page at https://telemetry.anyr.workers.dev/<project> works right away.

Manage a project

Every change needs the project's admin token in Authorization: Bearer <admin_token>. A missing or wrong token gets 401. An unknown project gets 404.

Set the display name, description and homepage: PATCH https://telemetry.anyr.workers.dev/api/<project>

curl -X PATCH https://telemetry.anyr.workers.dev/api/my-tool \
  -H "Authorization: Bearer $ADMIN_TOKEN" -H 'content-type: application/json' \
  -d '{"name":"My Tool","description":"A CLI that does one thing","homepage":"https://example.com"}'
# {"project":{"slug":"my-tool","name":"My Tool","description":"A CLI that does one thing","homepage":"https://example.com","created_at":"..."}}
Field Rules
name At most 60 chars.
description At most 200 chars.
homepage An http(s) URL, at most 200 chars.

A field you leave out stays as it is. null or "" clears it. A bad field gets 400 and changes nothing.

The slug cannot change. Raw events in Analytics Engine are stored under it. Only the display name can change. To move to a new slug, send events to the new slug.

Rotate the admin token, for example after a leak. The old token stops working at once:

curl -X POST https://telemetry.anyr.workers.dev/api/my-tool/token -H "Authorization: Bearer $ADMIN_TOKEN"
# {"admin_token":"tk_..."}

Delete a project and its rollups:

curl -X DELETE https://telemetry.anyr.workers.dev/api/my-tool -H "Authorization: Bearer $ADMIN_TOKEN"

Raw events in Analytics Engine cannot be deleted. They expire after 90 days.

Lost token: there is no self-service reset, because the token is the only proof of ownership. The operator of the deployment can set a new one: bun scripts/reset-token.ts <project> prints a new token and the D1 command that stores its hash.

Stats API

GET https://telemetry.anyr.workers.dev/api/<project>/stats?days=7|30|90|365

Public, no auth. Any other days value falls back to 30. Response:

{
  "project": "my-tool",
  "days": 30,
  "generated_at": "2026-10-13T12:00:00.000Z",
  "summary": {
    "dau": 42, "wau": 180, "mau": 510, "events": 9120,
    "today": { "users": 17, "events": 301 }
  },
  "series": [{ "day": "2026-10-12", "dau": 42, "events": 310 }],
  "dims": {
    "event": { "ping": [510, 4100], "run": [220, 5020] },
    "version": { "1.2.0": [400, 7000] },
    "prop:command": { "build": [90, 2000] }
  }
}
  • summary.dau, wau, mau are for the latest closed day. summary.events is summed over the range.
  • summary.today is live from Analytics Engine and cached for about 5 minutes.
  • series has one entry per day, with zeros for days without data.
  • dims maps a dimension name to values, each as [users, events], top 20 per dimension by events. Dimension names are event, version, os, arch, country, ci, source and prop:<key>. Users are summed per day, so they are user-days, not unique users.

Rollups run daily at 00:10 UTC. To rebuild one day from live data (admin only, the last 89 days):

curl -X POST "https://telemetry.anyr.workers.dev/api/my-tool/rollup?day=2026-10-12" -H "Authorization: Bearer $ADMIN_TOKEN"

Import and backfill

Use this to bring history from another system. Compute the daily aggregates locally, then upload them. See scripts/import.ts in the repo, which turns raw NDJSON rows into exact rollups.

POST https://telemetry.anyr.workers.dev/api/<project>/import with Authorization: Bearer <admin_token>

{
  "rows": [
    {
      "day": "2026-07-01", "dau": 12, "wau": 40, "mau": 90, "events": 150,
      "dims": {
        "event": { "run": [10, 120], "ping": [12, 30] },
        "version": { "1.0.0": [12, 150] },
        "prop:command": { "build": [6, 50] }
      }
    }
  ]
}
  • At most 360 rows per request, one row per day, no future days.
  • dims values are [users, events]. Dims are cut to the top 20 per dimension. The dims JSON is at most 16 KB.
  • Import never overwrites a day that was rolled up from live data. Importing the same day again replaces the earlier import.
  • Response: {"imported": <rows written>, "skipped": <rows kept as they were>}.

Privacy

  • The id is hashed with a secret salt and the project name (sha256, first 16 hex chars). The raw id is never written anywhere.
  • IP address and user agent are never stored. Without id, they are mixed into a hash that rotates every day and only counts daily users.
  • The country comes from Cloudflare's edge, as a 2-letter code.
  • Hashed ids exist only in Analytics Engine and are deleted after 90 days. The database holds only daily counts.
  • Stats pages and the stats API are public. Do not send anything you would not publish as an aggregate.

Limits and free tier

Limit Value
Body size 16 KB
Events per request 25 (native), 250 points (OTLP)
Rate limit 60 requests per minute per IP (best effort, per Worker instance)
Daily cap 20,000 data points per project per UTC day, then 429
New projects 3 per IP and 100 in total per UTC day, then 429
Raw event retention 90 days
Rollups One row per active project per day, kept long term

The service is meant to run inside the Cloudflare free plan. Workers allows 100,000 requests per day and Analytics Engine allows 100,000 data points per day, both per account. Sending events to an existing project never writes to D1. The first event of a new project writes one row.

Self-hosting

git clone https://github.com/duyet/telemetry && cd telemetry && bun install && bunx cf deploy
  • Enable Analytics Engine once per account: Cloudflare dashboard, Workers & Pages, Analytics Engine.
  • There are no secrets and no environment variables. The Worker creates its own D1 schema on first request, and generates its own hashing salt.
  • cf deploy creates the D1 database, the Analytics Engine dataset and the daily cron.
  • Source: https://github.com/duyet/telemetry