Skip to content

Commit 10fde65

Browse files
amikofalvyclaude
andcommitted
feat: add Sentry error tracking to create-agents CLI
Add privacy-first error tracking with user consent to help improve the CLI tool. Features: - Sentry integration for anonymous error reporting - User consent prompt on first run with clear privacy notice - Multiple opt-out methods (--disable-telemetry flag, DO_NOT_TRACK env var, interactive prompt) - PII scrubbing (file paths, usernames automatically removed) - Breadcrumb tracking for debugging context - Environment-aware (disabled in test/dev) - Telemetry config stored in ~/.inkeep/telemetry-config.json Privacy measures: - No collection of file contents, API keys, or personal information - Stack traces sanitized to remove usernames and sensitive paths - Disabled by default if DO_NOT_TRACK=1 or --disable-telemetry flag - User given clear choice with detailed privacy notice - Config persisted to respect user preference Testing: - Unit tests for error tracking functionality - Existing tests updated to mock Sentry - All tests passing with proper mocking Documentation: - README updated with telemetry section - Clear explanation of what is/isn't collected - Multiple opt-out methods documented - Privacy policy link included 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent f14257f commit 10fde65

File tree

8 files changed

+612
-4
lines changed

8 files changed

+612
-4
lines changed

packages/create-agents/README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ pnpm create-agents my-agent-directory --project-id my-project-id --anthropic-key
4343
- `--project-id <project-id>` - Project identifier for your agents
4444
- `--openai-key <openai-key>` - OpenAI API key (optional)
4545
- `--anthropic-key <anthropic-key>` - Anthropic API key (recommended)
46+
- `--disable-telemetry` - Disable anonymous error tracking
4647

4748
## What's Created
4849

@@ -150,6 +151,43 @@ LOG_LEVEL=debug
150151
- `apps/run-api/.env` - Run API configuration
151152
- `src/<project-id>/.env` - CLI configuration
152153

154+
## Telemetry
155+
156+
`create-agents` collects anonymous error reports to help us improve the tool. On first run, you'll be asked for consent.
157+
158+
### What We Collect
159+
- Anonymous error reports and stack traces
160+
- Template usage (e.g., "weather-project")
161+
- Success/failure status
162+
163+
### What We Do NOT Collect
164+
- File contents or code
165+
- API keys or credentials
166+
- Personal information
167+
- Usernames or file paths (automatically scrubbed)
168+
169+
### Opt Out
170+
171+
You can disable telemetry in several ways:
172+
173+
**1. Command line flag:**
174+
```bash
175+
npx create-agents --disable-telemetry
176+
```
177+
178+
**2. Environment variable:**
179+
```bash
180+
DO_NOT_TRACK=1 npx create-agents
181+
```
182+
183+
**3. During first run:**
184+
When prompted, select "No" to disable telemetry
185+
186+
Your telemetry preferences are saved to `~/.inkeep/telemetry-config.json`
187+
188+
### Privacy Policy
189+
For more information, see our [Privacy Policy](https://inkeep.com/privacy)
190+
153191
## Learn More
154192

155193
- 📚 [Documentation](https://docs.inkeep.com)

packages/create-agents/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"license": "SEE LICENSE IN LICENSE.md",
4040
"dependencies": {
4141
"@clack/prompts": "^0.11.0",
42+
"@sentry/node": "^10.17.0",
4243
"commander": "^12.0.0",
4344
"degit": "^2.8.4",
4445
"fs-extra": "^11.0.0",
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import fs from 'fs-extra';
2+
import os from 'node:os';
3+
import path from 'node:path';
4+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5+
import {
6+
disableTelemetry,
7+
enableTelemetry,
8+
getTelemetryConfig,
9+
saveTelemetryConfig,
10+
} from '../errorTracking';
11+
12+
// Mock @sentry/node
13+
vi.mock('@sentry/node', () => ({
14+
init: vi.fn(),
15+
captureException: vi.fn(),
16+
captureMessage: vi.fn(),
17+
addBreadcrumb: vi.fn(),
18+
setContext: vi.fn(),
19+
close: vi.fn().mockResolvedValue(undefined),
20+
}));
21+
22+
// Mock fs-extra
23+
vi.mock('fs-extra');
24+
25+
const TELEMETRY_CONFIG_DIR = path.join(os.homedir(), '.inkeep');
26+
const TELEMETRY_CONFIG_FILE = path.join(TELEMETRY_CONFIG_DIR, 'telemetry-config.json');
27+
28+
describe('Error Tracking', () => {
29+
beforeEach(() => {
30+
vi.clearAllMocks();
31+
32+
// Mock fs-extra methods
33+
vi.mocked(fs.existsSync).mockReturnValue(false);
34+
vi.mocked(fs.readFileSync).mockReturnValue('{}');
35+
vi.mocked(fs.writeFileSync).mockImplementation(() => {});
36+
vi.mocked(fs.ensureDirSync).mockImplementation(() => {});
37+
});
38+
39+
afterEach(() => {
40+
vi.restoreAllMocks();
41+
});
42+
43+
describe('getTelemetryConfig', () => {
44+
it('should return default config when file does not exist', () => {
45+
vi.mocked(fs.existsSync).mockReturnValue(false);
46+
47+
const config = getTelemetryConfig();
48+
49+
expect(config).toEqual({
50+
enabled: true,
51+
askedConsent: false,
52+
});
53+
});
54+
55+
it('should load config from file when it exists', async () => {
56+
// Need to clear the cached config first by reimporting
57+
vi.resetModules();
58+
59+
const mockConfig = {
60+
enabled: false,
61+
askedConsent: true,
62+
userId: 'test-user-id',
63+
};
64+
65+
vi.mocked(fs.existsSync).mockReturnValue(true);
66+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockConfig));
67+
68+
// Re-import to get fresh module
69+
const { getTelemetryConfig: freshGetTelemetryConfig } = await import('../errorTracking.js');
70+
const config = freshGetTelemetryConfig();
71+
72+
expect(config).toEqual(mockConfig);
73+
expect(fs.readFileSync).toHaveBeenCalledWith(TELEMETRY_CONFIG_FILE, 'utf-8');
74+
});
75+
76+
it('should return default config when file read fails', () => {
77+
vi.mocked(fs.existsSync).mockReturnValue(true);
78+
vi.mocked(fs.readFileSync).mockImplementation(() => {
79+
throw new Error('Read error');
80+
});
81+
82+
const config = getTelemetryConfig();
83+
84+
expect(config).toEqual({
85+
enabled: true,
86+
askedConsent: false,
87+
});
88+
});
89+
});
90+
91+
describe('saveTelemetryConfig', () => {
92+
it('should save config to file', () => {
93+
const config = {
94+
enabled: true,
95+
askedConsent: true,
96+
userId: 'test-user',
97+
};
98+
99+
saveTelemetryConfig(config);
100+
101+
expect(fs.ensureDirSync).toHaveBeenCalledWith(TELEMETRY_CONFIG_DIR);
102+
expect(fs.writeFileSync).toHaveBeenCalledWith(
103+
TELEMETRY_CONFIG_FILE,
104+
JSON.stringify(config, null, 2)
105+
);
106+
});
107+
108+
it('should handle save errors gracefully', () => {
109+
vi.mocked(fs.writeFileSync).mockImplementation(() => {
110+
throw new Error('Write error');
111+
});
112+
113+
const config = {
114+
enabled: true,
115+
askedConsent: true,
116+
};
117+
118+
// Should not throw
119+
expect(() => saveTelemetryConfig(config)).not.toThrow();
120+
});
121+
});
122+
123+
describe('disableTelemetry', () => {
124+
it('should disable telemetry and save config', () => {
125+
vi.mocked(fs.existsSync).mockReturnValue(false);
126+
127+
disableTelemetry();
128+
129+
expect(fs.writeFileSync).toHaveBeenCalledWith(
130+
TELEMETRY_CONFIG_FILE,
131+
expect.stringContaining('"enabled": false')
132+
);
133+
expect(fs.writeFileSync).toHaveBeenCalledWith(
134+
TELEMETRY_CONFIG_FILE,
135+
expect.stringContaining('"askedConsent": true')
136+
);
137+
});
138+
});
139+
140+
describe('enableTelemetry', () => {
141+
it('should enable telemetry and save config', () => {
142+
vi.mocked(fs.existsSync).mockReturnValue(false);
143+
144+
enableTelemetry();
145+
146+
expect(fs.writeFileSync).toHaveBeenCalledWith(
147+
TELEMETRY_CONFIG_FILE,
148+
expect.stringContaining('"enabled": true')
149+
);
150+
expect(fs.writeFileSync).toHaveBeenCalledWith(
151+
TELEMETRY_CONFIG_FILE,
152+
expect.stringContaining('"askedConsent": true')
153+
);
154+
});
155+
});
156+
157+
describe('initErrorTracking', () => {
158+
it('should not initialize in test environment', async () => {
159+
const Sentry = await import('@sentry/node');
160+
161+
// Import and call initErrorTracking
162+
const { initErrorTracking } = await import('../errorTracking.js');
163+
initErrorTracking('1.0.0');
164+
165+
expect(Sentry.init).not.toHaveBeenCalled();
166+
});
167+
});
168+
});

packages/create-agents/src/__tests__/utils.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ vi.mock('../templates');
1010
vi.mock('@clack/prompts');
1111
vi.mock('child_process');
1212
vi.mock('util');
13+
vi.mock('../errorTracking', () => ({
14+
addBreadcrumb: vi.fn(),
15+
captureError: vi.fn(),
16+
captureMessage: vi.fn(),
17+
getTelemetryConfig: vi.fn().mockReturnValue({ enabled: true, askedConsent: true }),
18+
saveTelemetryConfig: vi.fn(),
19+
disableTelemetry: vi.fn(),
20+
}));
1321

1422
// Setup default mocks
1523
const mockSpinner = {

0 commit comments

Comments
 (0)