Compare commits

..
Author SHA1 Message Date
Tabish Bidiwale 8910a1f2a0 docs: make telemetry section collapsible and concise 2026-01-09 23:08:22 -08:00
Tabish Bidiwale 763a6cf033 feat: add optional anonymous usage statistics
Introduces privacy-first usage analytics to help understand how OpenSpec
is being used. Key privacy protections:

- Only tracks command names and version (no arguments, paths, or content)
- Opt-out via OPENSPEC_TELEMETRY=0 or DO_NOT_TRACK=1
- Auto-disabled in CI environments
- No IP address collection (explicitly disabled)
- Anonymous ID is a random UUID with no PII

Uses PostHog with a reverse proxy to avoid ad blockers. First-run shows
a one-line notice informing users about the collection.
2026-01-09 23:08:22 -08:00
Tabish Bidiwale d12342542b chore: add proposal for PostHog analytics integration
Introduces the proposal artifact for adding opt-in telemetry to OpenSpec
using PostHog. Covers command tracking, feature adoption metrics, and
privacy-respecting consent management.
2026-01-09 23:08:22 -08:00
2 changed files with 5 additions and 13 deletions
+3 -6
View File
@@ -50,10 +50,7 @@ program
program.option('--no-color', 'Disable color output');
// Apply global flags and telemetry before any command runs
// Note: preAction receives (thisCommand, actionCommand) where:
// - thisCommand: the command where hook was added (root program)
// - actionCommand: the command actually being executed (subcommand)
program.hook('preAction', async (thisCommand, actionCommand) => {
program.hook('preAction', async (thisCommand) => {
const opts = thisCommand.opts();
if (opts.color === false) {
process.env.NO_COLOR = '1';
@@ -62,8 +59,8 @@ program.hook('preAction', async (thisCommand, actionCommand) => {
// Show first-run telemetry notice (if not seen)
await maybeShowTelemetryNotice();
// Track command execution (use actionCommand to get the actual subcommand)
const commandPath = getCommandPath(actionCommand);
// Track command execution
const commandPath = getCommandPath(thisCommand);
await trackCommand(commandPath, version);
});
+2 -7
View File
@@ -14,25 +14,20 @@ import {
describe('telemetry/config', () => {
let tempDir: string;
let originalHome: string | undefined;
let originalUserProfile: string | undefined;
beforeEach(() => {
// Create temp directory for tests
tempDir = path.join(os.tmpdir(), `openspec-telemetry-test-${Date.now()}`);
fs.mkdirSync(tempDir, { recursive: true });
// Mock HOME/USERPROFILE to point to temp dir
// On POSIX, os.homedir() uses HOME; on Windows it uses USERPROFILE
// Mock HOME to point to temp dir
originalHome = process.env.HOME;
originalUserProfile = process.env.USERPROFILE;
process.env.HOME = tempDir;
process.env.USERPROFILE = tempDir;
});
afterEach(() => {
// Restore HOME/USERPROFILE
// Restore HOME
process.env.HOME = originalHome;
process.env.USERPROFILE = originalUserProfile;
// Clean up temp directory
fs.rmSync(tempDir, { recursive: true, force: true });