Skip to content
Merged
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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,15 @@ You can always go back:

</details>

<details>
<summary><strong>Telemetry</strong> – OpenSpec collects anonymous usage stats (opt-out: <code>OPENSPEC_TELEMETRY=0</code>)</summary>

We collect only command names and version to understand usage patterns. No arguments, paths, content, or PII. Automatically disabled in CI.

**Opt-out:** `export OPENSPEC_TELEMETRY=0` or `export DO_NOT_TRACK=1`

</details>

## Contributing

- Install dependencies: `pnpm install`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-01-10
175 changes: 175 additions & 0 deletions openspec/changes/archive/2026-01-09-add-posthog-analytics/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
## Context

OpenSpec needs usage analytics to understand adoption and inform product decisions. PostHog provides a privacy-conscious analytics platform suitable for open source projects.

## Goals / Non-Goals

**Goals:**
- Track daily/weekly/monthly active usage
- Understand command usage patterns
- Keep implementation minimal and privacy-respecting
- Enable opt-out with minimal friction

**Non-Goals:**
- Detailed error tracking or diagnostics
- User identification or profiling
- Complex event hierarchies
- Full CLI command for telemetry management (env var sufficient for now)

## Decisions

### Opt-Out Model

**Decision:** Telemetry enabled by default, opt-out via environment variable.

```bash
OPENSPEC_TELEMETRY=0 # Disable telemetry
DO_NOT_TRACK=1 # Industry standard, also respected
```

Auto-disabled when `CI=true` is detected.

**Rationale:**
- Opt-in typically yields ~3% participation—not enough for meaningful data
- Understanding usage patterns requires statistically significant sample sizes
- Environment variable opt-out is simple and immediate
- Respecting `DO_NOT_TRACK` follows industry convention

**Alternatives considered:**
- Opt-in only - Insufficient data for product decisions
- Config file setting - More complex, env var sufficient for MVP
- Full `openspec telemetry` command - Can add later if users request

### Event Design

**Decision:** Single event type with minimal properties.

```typescript
{
event: 'command_executed',
properties: {
command: 'init', // Command name only
version: '1.2.3' // OpenSpec version
}
}
```

**Rationale:**
- Answers the core questions: how much usage, which commands are popular
- PostHog derives DAU/WAU/MAU from anonymous user counts over time
- No arguments, paths, or content—clean privacy story
- Easy to explain in disclosure notice

**Not tracked:**
- Command arguments
- File paths or contents
- Error messages or stack traces
- Project names or spec content
- IP addresses (`$ip: null` explicitly set)

### Anonymous ID

**Decision:** Random UUID, lazily generated on first telemetry send, stored in global config.

```typescript
// ~/.config/openspec/config.json
{
"telemetry": {
"anonymousId": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}
}
```

**Rationale:**
- Random UUID has no relation to the person—can't be reversed
- Stored in config so same user = same ID across sessions (needed for DAU/WAU/MAU)
- Lazy generation means no ID created if user opts out before first command
- User can delete config to reset identity

**Alternatives considered:**
- Machine-derived hash (hostname, MAC) - Feels invasive, fingerprint-like
- Per-session UUID - Breaks user counting metrics entirely

### SDK Configuration

**Decision:** PostHog Node SDK with immediate flush, shutdown on exit.

```typescript
const posthog = new PostHog(API_KEY, {
flushAt: 1, // Send immediately, don't batch
flushInterval: 0 // No timer-based flushing
});

// Before CLI exits
await posthog.shutdown();
```

**Rationale:**
- CLI processes are short-lived; batching would lose events
- `flushAt: 1` ensures each event sends immediately
- `shutdown()` guarantees flush before process exit
- Adds ~100-300ms to exit—negligible for typical CLI workflows

**Error handling:**
- Network failures silently ignored (telemetry shouldn't break CLI)
- `shutdown()` wrapped in try/catch

### Hook Location

**Decision:** Commander.js `preAction` and `postAction` hooks.

```typescript
program
.hook('preAction', (thisCommand) => {
maybeShowTelemetryNotice();
trackCommand(thisCommand.name(), VERSION);
})
.hook('postAction', async () => {
await shutdown();
});
```

**Rationale:**
- Centralized—one place for all telemetry logic
- Automatic—new commands get tracked without code changes
- Clean separation—command handlers don't know about telemetry

**Subcommand handling:**
- Track full command path for nested commands (e.g., `change:apply`)

### First-Run Notice

**Decision:** One-liner on first command ever, stored "seen" flag in config.

```
Note: OpenSpec collects anonymous usage stats. Opt out: OPENSPEC_TELEMETRY=0
```

**Rationale:**
- First command (not just `init`) ensures notice is always seen
- Non-blocking—no prompt, just informational
- One-liner is visible but not intrusive
- Storing "seen" in config prevents repeated display

**Config after first run:**
```json
{
"telemetry": {
"anonymousId": "...",
"noticeSeen": true
}
}
```

## Risks / Trade-offs

| Risk | Mitigation |
|------|------------|
| Users prefer opt-in | Clear disclosure, trivial opt-out, transparent about what's collected |
| GDPR concerns | No personal data, no IP, user can delete config |
| Slows CLI exit by ~200ms | Negligible for most workflows; can optimize if needed |
| PostHog outage affects CLI | Fire-and-forget with timeout; failures are silent |

## Open Questions

None—design is intentionally minimal. Future enhancements (dedicated command, workflow tracking) can be added based on user feedback.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## Why

OpenSpec currently has no visibility into how the tool is being used. Without analytics, we cannot:
- Understand which commands and features are most valuable to users
- Measure adoption and usage patterns
- Make data-driven decisions about product development

Adding PostHog analytics enables product insights while respecting user privacy through transparent, opt-out telemetry.

## What Changes

- Add PostHog Node.js SDK as a dependency
- Implement telemetry system with environment variable opt-out
- Track command usage (command name and version only)
- Show first-run notice informing users about telemetry
- Store anonymous ID in global config (`~/.config/openspec/config.json`)
- Respect `DO_NOT_TRACK` and `OPENSPEC_TELEMETRY=0` environment variables
- Auto-disable in CI environments

## Capabilities

### New Capabilities

- `telemetry`: Anonymous usage analytics using PostHog. Covers command tracking, opt-out controls, and first-run disclosure notice.

### Modified Capabilities

- `global-config`: Add telemetry state storage (anonymous ID, notice seen flag)

## Impact

- **Dependencies**: Add `posthog-node` package
- **Privacy**: Opt-out via env var, no personal data collected, clear disclosure
- **Configuration**: New global config fields for telemetry state
- **Network**: Async event sending with flush on exit (~100-300ms added)
- **CI/CD**: Telemetry auto-disabled when `CI=true`
- **Documentation**: Update README with telemetry disclosure
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## MODIFIED Requirements

### Requirement: Global configuration storage
The system SHALL store global configuration in `~/.config/openspec/config.json`, including telemetry state with `anonymousId` and `noticeSeen` fields.

#### Scenario: Initial config creation
- **WHEN** no global config file exists
- **AND** the first telemetry event is about to be sent
- **THEN** the system creates `~/.config/openspec/config.json` with telemetry configuration

#### Scenario: Telemetry config structure
- **WHEN** reading or writing telemetry configuration
- **THEN** the config contains a `telemetry` object with `anonymousId` (string UUID) and `noticeSeen` (boolean) fields

#### Scenario: Config file format
- **WHEN** storing configuration
- **THEN** the system writes valid JSON that can be read and modified by users

#### Scenario: Existing config preservation
- **WHEN** adding telemetry fields to an existing config file
- **THEN** the system preserves all existing configuration fields
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
## ADDED Requirements

### Requirement: Command execution tracking
The system SHALL send a `command_executed` event to PostHog when any CLI command executes, including only the command name and OpenSpec version as properties.

#### Scenario: Standard command execution
- **WHEN** a user runs any openspec command
- **THEN** the system sends a `command_executed` event with `command` and `version` properties

#### Scenario: Subcommand execution
- **WHEN** a user runs a nested command like `openspec change apply`
- **THEN** the system sends a `command_executed` event with the full command path (e.g., `change:apply`)

### Requirement: Privacy-preserving event design
The system SHALL NOT include command arguments, file paths, project names, spec content, error messages, or IP addresses in telemetry events.

#### Scenario: Command with arguments
- **WHEN** a user runs `openspec init my-project --force`
- **THEN** the telemetry event contains only `command: "init"` and `version: "<version>"` without arguments

#### Scenario: IP address exclusion
- **WHEN** the system sends a telemetry event
- **THEN** the event explicitly sets `$ip: null` to prevent IP tracking

### Requirement: Environment variable opt-out
The system SHALL disable telemetry when `OPENSPEC_TELEMETRY=0` or `DO_NOT_TRACK=1` environment variables are set.

#### Scenario: OPENSPEC_TELEMETRY opt-out
- **WHEN** `OPENSPEC_TELEMETRY=0` is set in the environment
- **THEN** the system sends no telemetry events

#### Scenario: DO_NOT_TRACK opt-out
- **WHEN** `DO_NOT_TRACK=1` is set in the environment
- **THEN** the system sends no telemetry events

#### Scenario: Environment variable takes precedence
- **WHEN** the user has previously used the CLI (config exists)
- **AND** the user sets `OPENSPEC_TELEMETRY=0`
- **THEN** telemetry is disabled regardless of config state

### Requirement: CI environment auto-disable
The system SHALL automatically disable telemetry when `CI=true` environment variable is detected.

#### Scenario: CI environment detection
- **WHEN** `CI=true` is set in the environment
- **THEN** the system sends no telemetry events

#### Scenario: CI with explicit enable
- **WHEN** `CI=true` is set
- **AND** `OPENSPEC_TELEMETRY=1` is explicitly set
- **THEN** telemetry remains disabled (CI takes precedence for privacy)

### Requirement: First-run telemetry notice
The system SHALL display a one-line telemetry disclosure notice on the first command execution, before any telemetry is sent.

#### Scenario: First command execution
- **WHEN** a user runs their first openspec command
- **AND** telemetry is enabled
- **THEN** the system displays: "Note: OpenSpec collects anonymous usage stats. Opt out: OPENSPEC_TELEMETRY=0"

#### Scenario: Subsequent command execution
- **WHEN** a user has already seen the notice (noticeSeen: true in config)
- **THEN** the system does not display the notice

#### Scenario: Notice before telemetry
- **WHEN** displaying the first-run notice
- **THEN** the notice appears before any telemetry event is sent

### Requirement: Anonymous user identification
The system SHALL generate a random UUID as an anonymous identifier on first telemetry send, stored in global config.

#### Scenario: First telemetry event
- **WHEN** the first telemetry event is sent
- **AND** no anonymousId exists in config
- **THEN** the system generates a random UUID v4 and stores it in config

#### Scenario: Persistent identity
- **WHEN** a user runs multiple commands across sessions
- **THEN** the same anonymousId is used for all events

#### Scenario: Lazy generation with opt-out
- **WHEN** a user opts out before running any command
- **THEN** no anonymousId is ever generated or stored

### Requirement: Immediate event sending
The system SHALL send telemetry events immediately without batching, using `flushAt: 1` and `flushInterval: 0` configuration.

#### Scenario: Event transmission timing
- **WHEN** a command executes
- **THEN** the telemetry event is sent immediately, not queued for batch transmission

### Requirement: Graceful shutdown
The system SHALL call `posthog.shutdown()` before CLI exit to ensure pending events are flushed.

#### Scenario: Normal exit
- **WHEN** a command completes successfully
- **THEN** the system awaits `shutdown()` before exiting

#### Scenario: Error exit
- **WHEN** a command fails with an error
- **THEN** the system still awaits `shutdown()` before exiting

### Requirement: Silent failure handling
The system SHALL silently ignore telemetry failures without affecting CLI functionality.

#### Scenario: Network failure
- **WHEN** the telemetry request fails due to network error
- **THEN** the CLI command completes normally without error message

#### Scenario: PostHog outage
- **WHEN** PostHog service is unavailable
- **THEN** the CLI command completes normally without error message

#### Scenario: Shutdown failure
- **WHEN** `shutdown()` fails or times out
- **THEN** the CLI exits normally without error message
Loading
Loading