Telemetry

Keep Shipping ships no telemetry. There is no command that turns it on, no config key that enables it, and no outbound call that is not already in ADR 0005's exhaustive list of what leaves the runner. This document exists anyway, because the useful moment to decide what a payload may contain is before anyone writes a sender. It is a contract written against a day that has not arrived: if a telemetry module is ever built, the payload below is what it sends, the consent model below is what turns it on, and a sender that does not match this document field for field is a bug. The decisions behind it are in ADR 0015.

The decision

What is never collected

Not "not by default". Never. Each of these is excluded for a reason, and the reason is why the payload above is short.

The payload, if it is ever sent

One request, one interval, one command. Every value is either a fixed literal, a value drawn from a closed vocabulary the harness already owns, a count, or a bucket. type below is drawn from exactly this set and nothing else: const, semver, enum(cli), enum(action), enum(code), count, bucket. A field whose type is not in that set does not belong in the payload, and the leak test fails on it.

{
  "schema": "keepshipping.telemetry.v1",
  "cli_version": "0.4.1",
  "command": "run",
  "count": 3,
  "failures": 1,
  "duration": "1024ms",
  "actions": 2,
  "action_counts": {
    "plan": 1,
    "apply": 1
  }
}
fieldtypeexamplenote
schemaconstkeepshipping.telemetry.v1bumped if the fields change; a receiver refuses a version it does not know
cli_versionsemver0.4.1the binary that sent it, never the version of a plan or image
commandenum(cli)runthe top-level word, never its arguments
countcount3invocations of command in the interval
failurescount1invocations that exited non-zero
durationbucket1024mspower-of-two bucket, so a duration is a magnitude and not a measurement
actionscount2steps that finished in the interval, whatever their kind
action_countscount{"plan": 1, "apply": 1}one entry per kind that occurred, keyed by the kind; every key is an enum(action) member and every value is a count

The closed vocabularies are named here rather than left to the receiver, so that a new kind cannot become a free-form field:

action_counts is the only nesting in the payload, and its shape is fixed: an object whose keys are members of enum(action) and whose values are counts. A kind that happens not to occur is simply absent; nothing else appears at any level, and there is no free-form string anywhere in the request.

Note that step kinds are not in the payload. Step kinds — the dotted names like oci.image or tofu.plan, registered in Catalog::builtin() and open-ended, since a custom kind can be added at runtime — are not the same thing as action kinds. A step kind is counted by the closed enum(action) its name maps to, and a kind that maps to nothing is counted under other, a fixed bucket that is already in the vocabulary. An unrecognised kind is therefore counted, never named, and never sent as a string that could carry whatever a runtime-registered kind happened to be called.

What the numbers mean

Consent

None of the commands below exist today. They are the proposed interface, recorded so that whoever builds this cannot invent a worse one.

keepshipping telemetry on
keepshipping telemetry off
keepshipping telemetry status

How this is tested

crates/testing/tests/telemetry_policy.rs is the leak test. It reads this document and checks it, so the contract is enforced rather than described:

The test does not, and cannot, prove that a future sender is honest; it proves the shape of what this project has agreed is allowed to leave a machine. That is the part worth testing, because it is the part that is easy to erode one plausible-looking field at a time.

See also