Usage telemetry

Gaffer collects anonymous usage statistics and sends them to Kurrent, Inc. when the tool is run. Telemetry data helps us refine and improve gaffer based on real usage patterns.

What is usage telemetry

Gaffer telemetry only tracks non-Personally-Identifiable Information. Collected data does not allow Kurrent to fingerprint users by any of the collected data points.

Examples of the telemetry data collected:

What gaffer does not track:

There are three event types. Each is wrapped in an envelope alongside shared install metadata (gaffer version, host OS, architecture, the runtime environment - local or ci) before being sent. When gaffer runs inside a project, the envelope also carries a salted hash of the project's root path (project_id). The path itself never leaves your machine, only the hash. When the gaffer CLI is launched by another gaffer process (typically the VS Code extension), the spawned CLI additionally carries the parent's anonymous id.

The receiving worker stamps each event with its own deploy timestamp.

The precise wire format lives in telemetry/schemas/events.cue (event shapes) and telemetry/schemas/wire.cue (envelope).

command_invoked

Records which gaffer command ran, what its outcome was, and bucketed counts of work done.

Example envelope
{
	"schema_version": "1",
	"emitter_id": "8f2b1a4c-9e7d-4a3e-b5f2-7c8a9d4e1f02",
	"run_id": "01938e7a-3c8d-7e2f-bac3-8d4e2f1c9a07",
	"context": {
		"emitter": "cli",
		"lib_version": "0.4.2",
		"os": "linux",
		"arch": "x64",
		"runtime_environment": "local"
	},
	"events": [
		{
			"name": "command_invoked",
			"timestamp": "2026-05-08T12:34:56.000Z",
			"properties": {
				"command": "dev",
				"duration_ms": 100,
				"outcome": "user_interrupt",
				"invoked_by": "direct",
				"invoked_via": "terminal",
				"manifest_features_used": ["projections", "fixtures"],
				"projection_count": 10,
				"fixture_count": 2,
				"connected_to_db": true,
				"db_version": "26.1"
			}
		}
	]
}

projection_shape

Records the shape of a projection file: which projection builtins are called (fromAll, when, partitionBy, etc.) with bucketed call counts, which handlers are registered, and a bucketed file size. The projection's identifier is a salted hash that's stable across runs but does not reveal the projection's path or contents.

Example envelope
{
	"schema_version": "1",
	"emitter_id": "8f2b1a4c-9e7d-4a3e-b5f2-7c8a9d4e1f02",
	"run_id": "01938e7a-3c8d-7e2f-bac3-8d4e2f1c9a07",
	"context": {
		"emitter": "cli",
		"lib_version": "0.4.2",
		"os": "linux",
		"arch": "x64",
		"runtime_environment": "local"
	},
	"events": [
		{
			"name": "projection_shape",
			"timestamp": "2026-05-08T12:34:56.000Z",
			"properties": {
				"projection_id": "a1b2c3d4e5f6789a",
				"parsable": true,
				"file_size": 5120,
				"handlers": {
					"any": false,
					"init": true,
					"deleted": false,
					"distinct_event_names": 10
				},
				"builtin_counts": {
					"fromAll": 1,
					"when": 10,
					"partitionBy": 1,
					"emit": 100
				}
			}
		}
	]
}

exception

Records crashes in gaffer's own code (Go panics in the CLI, runtime exceptions in the projection engine). Exception messages are always written by gaffer and never propagated from your projection code. Stack frames are scrubbed: file basenames only, user-JS frames dropped entirely.

Example envelope
{
	"schema_version": "1",
	"emitter_id": "8f2b1a4c-9e7d-4a3e-b5f2-7c8a9d4e1f02",
	"run_id": "01938e7a-3c8d-7e2f-bac3-8d4e2f1c9a07",
	"context": {
		"emitter": "cli",
		"lib_version": "0.4.2",
		"os": "linux",
		"arch": "x64",
		"runtime_environment": "local"
	},
	"events": [
		{
			"name": "exception",
			"timestamp": "2026-05-08T12:34:56.000Z",
			"properties": {
				"exceptions": [
					{
						"type": "RuntimeError",
						"value": "failed to load runtime library",
						"in_app": true,
						"stacktrace": {
							"type": "raw",
							"frames": [
								{
									"filename": "engine.go",
									"function": "Run",
									"lineno": 123,
									"in_app": true
								}
							]
						}
					}
				],
				"command": "dev",
				"phase": "startup"
			}
		}
	]
}

Disclosure

On the first invocation, gaffer prints a one-time message to the terminal, similar to:

Telemetry
---------
Gaffer collects usage data in order to improve your experience. The data is anonymous and collected by Kurrent, Inc.

You can opt out by any of:
  - Running `gaffer config telemetry off` (this machine)
  - Adding `telemetry = false` to your project's gaffer.toml
  - Setting GAFFER_TELEMETRY_OPTOUT, KURRENTDB_TELEMETRY_OPTOUT, or DO_NOT_TRACK to a truthy value

For more information visit https://telemetry.gaffer.kurrent.io.

If telemetry collection has already been disabled (for example via KURRENTDB_TELEMETRY_OPTOUT carried over from KurrentDB, or DO_NOT_TRACK), no disclosure is shown.

Reporting frequency

Gaffer emits events at the boundary of work, not on a periodic schedule:

A gaffer process that does no work emits nothing. There is no periodic heartbeat.

How to opt out

Telemetry transmission can be disabled by any one of the following:

When opted out, gaffer does not collect telemetry. No envelope is constructed and no event is recorded locally.

How to see what's being sent

Set GAFFER_TELEMETRY_DEBUG=1 (truthy values: 1, true, yes, on) and gaffer prints every event as JSON to stderr before sending it. When opted out, no envelopes are constructed and nothing is printed.

Where data is stored

Telemetry data is stored in PostHog's EU instance. Envelopes transit Cloudflare's edge network on the way there. Cloudflare's standard request logs include IP and are retained for around 30 days; gaffer does not forward IPs to PostHog. The worker that handles ingest is open source and lives in the gaffer repository, alongside the machine-readable schema for the events described above.

How to delete your data

Email privacy@kurrent.io with the identifier gaffer prints below. All events associated with that id are deleted from PostHog within 30 days. Session-stitching and identity-merge rows the worker holds for that id expire automatically within 25 hours and 30 days respectively.

To find your id:

The same identifier is what every gaffer telemetry envelope carries on the wire and what PostHog stores as the person id.