Skip to content
Open
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
7 changes: 7 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ truth for product telemetry — do not add alternative analytics systems.
- **Build-time key.** `POSTHOG_API_KEY` is injected at build time by
`tsup.config.ts` via `define`. Local builds without the env var become a
no-op automatically — the CLI itself stays functional.
- **Opt-out.** Users can disable usage analytics persistently with
`insforge telemetry disable` (stored as `telemetry_disabled` in
`~/.insforge/config.json`) or per run via the `DO_NOT_TRACK` /
`INSFORGE_TELEMETRY_DISABLED` env vars. The kill switch is
`isTelemetryDisabled()` in `src/lib/analytics.ts`; it gates the PostHog
client and the legacy `reportCliUsage` path. Any new telemetry emitter
MUST check it, or the opt-out silently stops being true.

**Do not** use `reportCliUsage` for new commands — that legacy OSS telemetry
path has been removed from `create`, `link`, and `docs`. PostHog is the path
Expand Down
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,10 @@ when you want to link a directory directly to a known project.

## Commands

> The `orgs`, `projects`, and `records` command groups are registered but hidden
> The `orgs` and `projects` command groups are registered but hidden
> (`hidden: true` in `src/index.ts`) and are intentionally excluded from this
> reference. Use `npx @insforge/cli list` instead of `orgs`/`projects`; `records`
> is internal and not supported for direct use.
> reference. Use `npx @insforge/cli list` instead of `orgs`/`projects`. For
> table data, use `db query` — there is no separate records API.

### Top-Level

Expand All @@ -100,6 +100,19 @@ npx @insforge/cli list
npx @insforge/cli list --json
```

#### `npx @insforge/cli telemetry <status|enable|disable>`

Manage anonymous usage analytics (command usage metadata only — never SQL,
file contents, credentials, or free text). `disable` persists the opt-out in
`~/.insforge/config.json`; the `DO_NOT_TRACK` and `INSFORGE_TELEMETRY_DISABLED`
environment variables are also honored for per-run or CI opt-out.

```bash
npx @insforge/cli telemetry status
npx @insforge/cli telemetry disable
npx @insforge/cli telemetry enable
```

#### `npx @insforge/cli create`

Create a new InsForge project interactively.
Expand Down Expand Up @@ -1192,6 +1205,8 @@ If you build the CLI from source without setting `POSTHOG_API_KEY` at build time
| `INSFORGE_API_URL` | Override the Platform API URL |
| `INSFORGE_EMAIL` | Email for non-interactive login |
| `INSFORGE_PASSWORD` | Password for non-interactive login |
| `INSFORGE_TELEMETRY_DISABLED` | Disable anonymous usage analytics for this run |
| `DO_NOT_TRACK` | Universal opt-out convention; also disables analytics |

## Non-Interactive / CI Usage

Expand Down
53 changes: 0 additions & 53 deletions src/commands/records/create.ts

This file was deleted.

46 changes: 0 additions & 46 deletions src/commands/records/delete.ts

This file was deleted.

63 changes: 0 additions & 63 deletions src/commands/records/list.ts

This file was deleted.

60 changes: 0 additions & 60 deletions src/commands/records/update.ts

This file was deleted.

91 changes: 91 additions & 0 deletions src/commands/telemetry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from 'vitest';
import { Command } from 'commander';
import { registerTelemetryCommand } from './telemetry.js';

vi.mock('../lib/config.js', () => ({
getGlobalConfig: vi.fn(),
saveGlobalConfig: vi.fn(),
}));

function makeProgram() {
const program = new Command().exitOverride();
program.option('--json');
registerTelemetryCommand(program);
return program;
}

async function run(argv: string[]): Promise<string> {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
try {
await makeProgram().parseAsync(argv, { from: 'user' });
return logSpy.mock.calls.flat().join('\n');
} finally {
logSpy.mockRestore();
}
}

describe('telemetry command', () => {
beforeEach(async () => {
vi.clearAllMocks();
vi.stubEnv('DO_NOT_TRACK', '');
vi.stubEnv('INSFORGE_TELEMETRY_DISABLED', '');
const { getGlobalConfig } = await import('../lib/config.js');
(getGlobalConfig as Mock).mockReturnValue({ platform_api_url: 'https://api.insforge.dev' });
});
afterEach(() => {
vi.unstubAllEnvs();
});

it('status reports enabled by default', async () => {
const out = await run(['telemetry', 'status', '--json']);
expect(JSON.parse(out)).toEqual({ enabled: true, source: 'default' });
});

it('status reports a config opt-out with its source', async () => {
const { getGlobalConfig } = await import('../lib/config.js');
(getGlobalConfig as Mock).mockReturnValue({
platform_api_url: 'https://api.insforge.dev',
telemetry_disabled: true,
});
const out = await run(['telemetry', 'status', '--json']);
expect(JSON.parse(out)).toEqual({ enabled: false, source: 'config' });
});

it('status reports an env-var override', async () => {
vi.stubEnv('DO_NOT_TRACK', '1');
const out = await run(['telemetry', 'status', '--json']);
expect(JSON.parse(out)).toEqual({ enabled: false, source: 'DO_NOT_TRACK' });
});

it('disable persists telemetry_disabled in the global config', async () => {
const { saveGlobalConfig } = await import('../lib/config.js');
await run(['telemetry', 'disable', '--json']);
expect((saveGlobalConfig as Mock).mock.calls[0][0]).toMatchObject({ telemetry_disabled: true });
});

it('enable removes the flag from the global config', async () => {
const { getGlobalConfig, saveGlobalConfig } = await import('../lib/config.js');
(getGlobalConfig as Mock).mockReturnValue({
platform_api_url: 'https://api.insforge.dev',
telemetry_disabled: true,
});
await run(['telemetry', 'enable', '--json']);
const saved = (saveGlobalConfig as Mock).mock.calls[0][0];
expect('telemetry_disabled' in saved).toBe(false);
});

it('enable errors when an env var still forces telemetry off', async () => {
vi.stubEnv('INSFORGE_TELEMETRY_DISABLED', '1');
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {
throw new Error('exit');
}) as never);
try {
await expect(run(['telemetry', 'enable'])).rejects.toThrow('exit');
expect(errSpy.mock.calls.flat().join('\n')).toContain('INSFORGE_TELEMETRY_DISABLED');
} finally {
errSpy.mockRestore();
exitSpy.mockRestore();
}
});
});
Loading
Loading