Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,10 @@ platforms and verification details.

</details>

Before the CLI sends its first event, it discloses anonymous usage and
sanitized error telemetry. Telemetry is enabled by default. Run
`okn telemetry show-payload` to inspect a sample. Put `--no-telemetry` before a
command to disable telemetry and save the opt-out. See
Anonymous usage and sanitized error telemetry is disabled by default. Run
`okn telemetry show-payload` to inspect a sample. Run `okn telemetry enable`
to opt in. Run `okn telemetry disable`, or put `--no-telemetry` before a
command, to disable it. See
[Product Telemetry and Privacy](Wiki/features/telemetry.md).

## Workflows
Expand Down
19 changes: 19 additions & 0 deletions Wiki/changelog/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,25 @@ page records release-level changes.
- Source: `package.json`, `packages/web/package.json`, `pnpm-lock.yaml`.
- Docs: `Wiki/changelog/cli.md`.

### Telemetry

- CLI product telemetry is now opt-in. The CLI sends no event and creates no
installation ID before you run `okn telemetry enable`.
- Unconfigured `okn telemetry status` reports `disabled` and `default`.
- The schema version 2 migration disables all schema version 1 configurations.
It deletes each random installation ID and all activity state.
- v0.11.0 did not distinguish automatic enablement from explicit opt-in. If you
enabled telemetry in v0.11.0, run `okn telemetry enable` again.
- `okn telemetry disable`, `--no-telemetry`, `DO_NOT_TRACK`, and
`OPENKNOWLEDGE_TELEMETRY=off` remain unchanged.
- Installer preflight and continuous integration suppression remain unchanged.
- Website consent and aggregate `/install` redirect telemetry remain unchanged.
- Source: `packages/cli/internal/telemetry/`,
`packages/cli/cmd/openknowledge/telemetry_command.go`.
- Docs: `README.md`, `packages/npm/README.md`,
`Wiki/features/commands/telemetry.md`, `Wiki/features/telemetry.md`,
`Wiki/features/installation.md`, and `Wiki/features/index.md`.

## v0.11.0 — 2026-08-08

Version 0.11 adds privacy-safe product telemetry, standalone skill setup, a
Expand Down
11 changes: 6 additions & 5 deletions Wiki/features/commands/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ okn telemetry enable
okn --no-telemetry <command>
```

Telemetry is enabled by default. The CLI prints a disclosure before it sends
the first event. Installer preflight and continuous integration do not send
telemetry.
Telemetry is disabled by default. The CLI sends no event and creates no
installation ID before you run `okn telemetry enable`. Installer preflight and
continuous integration do not send telemetry.

`status` reports the saved or default preference. Process-level environment
overrides do not change this report. `show-payload` prints a representative
JSON envelope without sending it.
overrides do not change this report. On an unconfigured installation, `status`
reports `disabled` and `default`. `show-payload` prints a representative JSON
envelope without sending it.

`disable` deletes the random installation ID and clears activity markers.
`enable` creates a new random installation ID when necessary.
Expand Down
2 changes: 1 addition & 1 deletion Wiki/features/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
- [Installation](installation.md) — Gives CLI installation and verification
steps.
- [Product telemetry and privacy](telemetry.md) — Describes collected data and
opt-out controls.
opt-in controls.
- [Configuration](configuration.md) — Describes the
`.openknowledge.toml` configuration.
- [Commands](commands/) — Lists the current CLI commands.
Expand Down
5 changes: 2 additions & 3 deletions Wiki/features/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,8 @@ assets exist. The package versions must also match.
Offline transactional tests cover shell and npm installation.
The root `pnpm test` gate runs these tests.

Before the CLI sends its first event, it prints the product telemetry
disclosure. Telemetry is enabled by default. Use
`okn --no-telemetry <command>` to disable it before the first event. See
CLI telemetry is disabled by default. The CLI sends no event and creates no
installation ID before you run `okn telemetry enable`. See
[Product Telemetry and Privacy](telemetry.md).

---
Expand Down
21 changes: 13 additions & 8 deletions Wiki/features/telemetry.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
---
type: Feature Documentation
title: Product Telemetry and Privacy
description: Describes anonymous CLI and website telemetry, data limits, and opt-out controls.
description: Describes anonymous CLI and website telemetry, data limits, and consent controls.
tags: [openknowledge, telemetry, analytics, privacy]
timestamp: 2026-08-08T00:00:00Z
---

# Product Telemetry and Privacy

Open Knowledge collects bounded product telemetry to measure installation,
Open Knowledge can collect bounded product telemetry to measure installation,
activation, useful command activity, daily activity, and sanitized errors.

CLI telemetry is enabled by default. The CLI prints a disclosure before it
sends the first event. Telemetry commands do not send events. JSON error mode
waits for a prior telemetry disclosure. Installer preflight and continuous
integration do not send telemetry.
CLI telemetry is disabled by default. The CLI sends no event and creates no
installation ID before you run `okn telemetry enable`. Telemetry commands do
not send events. Installer preflight and continuous integration do not send
telemetry.

Website analytics require explicit consent. The website records no analytics
event before consent.
Expand All @@ -32,6 +32,10 @@ okn telemetry enable
okn --no-telemetry <command>
```

On an unconfigured installation, `status` reports `disabled` and `default`.
Run `okn telemetry enable` to create a random installation ID and enable CLI
telemetry.

Put `--no-telemetry` before the command. This option disables telemetry for the
current command and future commands. It also deletes the random installation ID.

Expand Down Expand Up @@ -78,8 +82,9 @@ not add request IP addresses or raw user agents to events or upstream requests.

## Delivery

The CLI sends one bounded JSON envelope after a command finishes. Delivery uses
a short timeout. A delivery failure does not change output or exit status.
When enabled, the CLI sends one bounded JSON envelope after a command finishes.
Delivery uses a short timeout. A delivery failure does not change output or
exit status.

The first-party relay accepts only documented fields and values. It rejects
extra content. The relay converts accepted envelopes to PostHog's batch capture
Expand Down
11 changes: 6 additions & 5 deletions packages/cli/cmd/openknowledge/telemetry_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,11 @@ Usage:
openknowledge telemetry disable
openknowledge telemetry show-payload

Telemetry is enabled by default after a first-run disclosure. It sends only
allowlisted command, outcome, duration, version, platform, and random
installation identifiers. It does not send command arguments, paths, content,
repository or user identity, output, hostnames, IP addresses, or raw user
agents. Use --no-telemetry before a command to disable telemetry persistently.
Telemetry is disabled by default. Enable it with "openknowledge telemetry
enable". When enabled, it sends only allowlisted command, outcome, duration,
version, platform, and random installation identifiers. It does not send
command arguments, paths, content, repository or user identity, output,
hostnames, IP addresses, or raw user agents. Use --no-telemetry before a
command to disable telemetry persistently.
`
}
20 changes: 19 additions & 1 deletion packages/cli/cmd/openknowledge/telemetry_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,17 @@ func TestTelemetryCommandsExposeStateAndPayload(t *testing.T) {
stdout, stderr, code := captureMainOutput(t, func() int {
return runTelemetry([]string{"status"})
})
if code != 0 || stderr != "" || !strings.Contains(stdout, "Telemetry: enabled") || !strings.Contains(stdout, "Configuration: default") {
if code != 0 || stderr != "" || !strings.Contains(stdout, "Telemetry: disabled") || !strings.Contains(stdout, "Configuration: default") {
t.Fatalf("unexpected default status: code=%d stdout=%q stderr=%q", code, stdout, stderr)
}
stdout, stderr, code = captureMainOutput(t, func() int { return runTelemetry([]string{"enable"}) })
if code != 0 || stderr != "" || !strings.Contains(stdout, "telemetry is enabled") {
t.Fatalf("unexpected enable result: code=%d stdout=%q stderr=%q", code, stdout, stderr)
}
stdout, _, _ = captureMainOutput(t, func() int { return runTelemetry([]string{"status"}) })
if !strings.Contains(stdout, "Telemetry: enabled") || !strings.Contains(stdout, "Configuration: saved") {
t.Fatalf("unexpected enabled status: %q", stdout)
}
_, _, code = captureMainOutput(t, func() int { return runTelemetry([]string{"disable"}) })
if code != 0 {
t.Fatalf("disable returned %d", code)
Expand All @@ -57,3 +65,13 @@ func TestTelemetryCommandsExposeStateAndPayload(t *testing.T) {
t.Fatalf("invalid sample payload: %v %q", err, stdout)
}
}

func TestTelemetryHelpDescribesExplicitOptIn(t *testing.T) {
help := telemetryHelpText()
if !strings.Contains(help, "disabled by default") || !strings.Contains(help, "telemetry enable") {
t.Fatalf("telemetry help does not describe explicit opt-in: %q", help)
}
if strings.Contains(help, "enabled by default") {
t.Fatalf("telemetry help still describes an enabled default: %q", help)
}
}
31 changes: 24 additions & 7 deletions packages/cli/internal/telemetry/telemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ const (
ControlEnv = "OPENKNOWLEDGE_TELEMETRY"
SuppressEnv = "OPENKNOWLEDGE_TELEMETRY_SUPPRESS"

defaultEndpoint = "https://openknowledge.sh/api/telemetry"
maxConfigBytes = 64 << 10
currentConfigSchemaVersion = "2"
defaultEndpoint = "https://openknowledge.sh/api/telemetry"
maxConfigBytes = 64 << 10
)

var now = time.Now
Expand Down Expand Up @@ -248,6 +249,25 @@ func Load() (Config, bool, error) {
if err != nil {
return Config{}, false, err
}
config, exists, err := readConfig(path)
if err != nil || !exists {
return config, exists, err
}
switch config.SchemaVersion {
case "1":
// Version 1 cannot distinguish default enablement from explicit consent.
config = defaultConfig()
if err := save(path, config); err != nil {
return Config{}, false, err
}
case currentConfigSchemaVersion:
default:
return Config{}, false, fmt.Errorf("unsupported telemetry config version")
}
return config, true, nil
}

func readConfig(path string) (Config, bool, error) {
file, err := os.Open(path)
if os.IsNotExist(err) {
return Config{}, false, nil
Expand All @@ -269,9 +289,6 @@ func Load() (Config, bool, error) {
if err := decoder.Decode(&config); err != nil {
return Config{}, false, err
}
if config.SchemaVersion != "1" {
return Config{}, false, fmt.Errorf("unsupported telemetry config version")
}
return config, true, nil
}

Expand Down Expand Up @@ -326,7 +343,7 @@ func SamplePayload() Envelope {
}

func disclosureText() string {
return `Open Knowledge sends anonymous usage and sanitized error telemetry by default.
return `Open Knowledge sends anonymous usage and sanitized error telemetry because it is enabled.
It sends command names, outcomes, timing buckets, version, OS, architecture,
and a random installation ID. It never sends arguments, paths, content,
repository or user identity, command output, hostnames, IP addresses, or raw
Expand All @@ -335,7 +352,7 @@ user agents. Disable it with "okn --no-telemetry <command>" or
}

func defaultConfig() Config {
return Config{SchemaVersion: "1", Enabled: true}
return Config{SchemaVersion: currentConfigSchemaVersion, Enabled: false}
}

func save(path string, config Config) (resultErr error) {
Expand Down
83 changes: 76 additions & 7 deletions packages/cli/internal/telemetry/telemetry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,75 @@ func noContentResponse() *http.Response {
return &http.Response{StatusCode: http.StatusNoContent, Body: io.NopCloser(strings.NewReader(""))}
}

func TestFirstRunDisclosesAndSendsAllowlistedEvents(t *testing.T) {
func TestFirstRunDefaultsToDisabledAndSendsNothing(t *testing.T) {
configPath := filepath.Join(t.TempDir(), "telemetry.json")
t.Setenv(ConfigFileEnv, configPath)
t.Setenv(SuppressEnv, "")
t.Setenv("CI", "")
t.Setenv(ControlEnv, "")
t.Setenv("DO_NOT_TRACK", "")

requests := 0
client := testClient(func(request *http.Request) (*http.Response, error) {
requests++
_, _ = io.Copy(io.Discard, request.Body)
return noContentResponse(), nil
})
t.Setenv(EndpointEnv, "https://telemetry.example.test/v1/events")

session := Start(StartOptions{Version: "1.2.3", Command: "validate", HTTPClient: client})
session.Finish(0)

if requests != 0 {
t.Fatalf("default telemetry sent %d requests", requests)
}
config, exists, err := Status()
if err != nil || exists || config.Enabled || config.InstallationID != "" {
t.Fatalf("unexpected default config: %#v exists=%v err=%v", config, exists, err)
}
}

func TestLegacyDefaultOnConfigMigratesToDisabled(t *testing.T) {
configPath := filepath.Join(t.TempDir(), "telemetry.json")
t.Setenv(ConfigFileEnv, configPath)
t.Setenv(SuppressEnv, "")
t.Setenv("CI", "")
t.Setenv(ControlEnv, "")
t.Setenv("DO_NOT_TRACK", "")
legacy := Config{
SchemaVersion: "1",
Enabled: true,
InstallationID: "legacy-installation-id",
DisclosedAt: "2026-08-08T00:00:00Z",
FirstCommandRecorded: true,
FirstMeaningfulUseSeen: true,
LastActiveDate: "2026-08-08",
}
if err := save(configPath, legacy); err != nil {
t.Fatalf("save legacy config: %v", err)
}

requests := 0
client := testClient(func(request *http.Request) (*http.Response, error) {
requests++
_, _ = io.Copy(io.Discard, request.Body)
return noContentResponse(), nil
})
t.Setenv(EndpointEnv, "https://telemetry.example.test/v1/events")

Start(StartOptions{Version: "1.2.3", Command: "search", HTTPClient: client}).Finish(0)

if requests != 0 {
t.Fatalf("legacy telemetry sent %d requests", requests)
}
config, exists, err := Load()
if err != nil || !exists || config.SchemaVersion != "2" || config.Enabled || config.InstallationID != "" ||
config.DisclosedAt != "" || config.FirstCommandRecorded || config.FirstMeaningfulUseSeen || config.LastActiveDate != "" {
t.Fatalf("unexpected migrated config: %#v exists=%v err=%v", config, exists, err)
}
}

func TestExplicitOptInSendsAllowlistedEvents(t *testing.T) {
configPath := filepath.Join(t.TempDir(), "telemetry.json")
t.Setenv(ConfigFileEnv, configPath)
t.Setenv(SuppressEnv, "")
Expand All @@ -42,15 +110,13 @@ func TestFirstRunDisclosesAndSendsAllowlistedEvents(t *testing.T) {
return noContentResponse(), nil
})
t.Setenv(EndpointEnv, "https://telemetry.example.test/v1/events")
if _, err := SetEnabled(true); err != nil {
t.Fatalf("enable telemetry: %v", err)
}

var disclosure strings.Builder
session := Start(StartOptions{Version: "1.2.3", Command: "validate", Stderr: &disclosure, HTTPClient: client})
session := Start(StartOptions{Version: "1.2.3", Command: "validate", HTTPClient: client})
session.Finish(1)

if !strings.Contains(disclosure.String(), "anonymous usage and sanitized error telemetry") ||
!strings.Contains(disclosure.String(), "--no-telemetry") {
t.Fatalf("missing first-run disclosure: %q", disclosure.String())
}
if len(received.Events) != 3 {
t.Fatalf("expected command, first-command, and error events, got %#v", received.Events)
}
Expand Down Expand Up @@ -111,6 +177,9 @@ func TestMeaningfulUseAndDailyActivityAreBounded(t *testing.T) {
return noContentResponse(), nil
})
t.Setenv(EndpointEnv, "https://telemetry.example.test/v1/events")
if _, err := SetEnabled(true); err != nil {
t.Fatalf("enable telemetry: %v", err)
}

Start(StartOptions{Version: "1.2.3", Command: "export html", Stderr: io.Discard, HTTPClient: client}).Finish(0)
Start(StartOptions{Version: "1.2.3", Command: "export html", Stderr: io.Discard, HTTPClient: client}).Finish(0)
Expand Down
8 changes: 4 additions & 4 deletions packages/npm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ okn version

The `openknowledge` and `okn` commands run the same installed CLI.

Before the CLI sends its first event, it discloses default-on anonymous usage
and sanitized error telemetry. Run `okn telemetry show-payload` to inspect a
sample. Use `okn --no-telemetry <command>` or `okn telemetry disable` to save an
opt-out. Telemetry does not include command arguments, paths, content,
Anonymous usage and sanitized error telemetry is disabled by default. Run
`okn telemetry show-payload` to inspect a sample. Run `okn telemetry enable`
to opt in. Use `okn --no-telemetry <command>` or `okn telemetry disable` to
disable it. Telemetry does not include command arguments, paths, content,
identities, output, hostnames, IP addresses, or raw user agents.

The package downloads the matching binary from GitHub Releases during
Expand Down
Loading