codeant-cli 0.2.6 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeant-cli",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "Code review CLI tool",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,6 +41,7 @@
41
41
  "ink": "^5.0.1",
42
42
  "minimatch": "^10.1.1",
43
43
  "open": "^10.1.0",
44
+ "posthog-node": "^5.28.5",
44
45
  "react": "^18.3.1"
45
46
  },
46
47
  "devDependencies": {
@@ -6,6 +6,7 @@ import { fetchApi } from '../utils/fetchApi.js';
6
6
  import fs from 'fs';
7
7
  import path from 'path';
8
8
  import { executeTool } from '../tools/executeTool.js';
9
+ import { track, shutdown as analyticsShutdown } from '../utils/analytics.js';
9
10
  import {
10
11
  renderInitializing,
11
12
  renderFetchingDiff,
@@ -51,6 +52,9 @@ export default function Review({ scanType = 'all', lastNCommits = 1, failOn = 'C
51
52
  let cancelled = false;
52
53
 
53
54
  async function runReview() {
55
+ const reviewStartTime = Date.now();
56
+ track('review_triggered', { scan_type: scanType, source: 'cli_interactive' });
57
+
54
58
  try {
55
59
  // ── Fetch diff ─────────────────────────────────────────────────────
56
60
  setStatus('fetching_diff');
@@ -140,9 +144,24 @@ export default function Review({ scanType = 'all', lastNCommits = 1, failOn = 'C
140
144
  setSummary(null);
141
145
  setStatus('done');
142
146
 
147
+ const labelCounts = {};
148
+ for (const i of reviewIssues) { labelCounts[i.label] = (labelCounts[i.label] || 0) + 1; }
149
+ track('review_completed', {
150
+ scan_type: scanType,
151
+ source: 'cli_interactive',
152
+ issue_count: reviewIssues.length,
153
+ file_count: perFileRequests.length,
154
+ duration_ms: Date.now() - reviewStartTime,
155
+ label_counts: labelCounts,
156
+ });
157
+ await analyticsShutdown();
158
+
143
159
  } catch (err) {
144
160
  if (!cancelled) {
145
- setError(err.message);
161
+ const errMsg = err instanceof Error ? err.message : String(err);
162
+ track('review_error', { scan_type: scanType, source: 'cli_interactive', error: errMsg });
163
+ await analyticsShutdown();
164
+ setError(errMsg);
146
165
  setStatus('error');
147
166
  }
148
167
  }
package/src/index.js CHANGED
@@ -13,6 +13,7 @@ import Review from './commands/review.js';
13
13
  import Welcome from './components/Welcome.js';
14
14
  import * as scm from './scm/index.js';
15
15
  import { setConfigValue } from './utils/config.js';
16
+ import { track, shutdown as analyticsShutdown } from './utils/analytics.js';
16
17
 
17
18
  // Read version from package.json — createRequire works with ncc bundling
18
19
  const require = createRequire(import.meta.url);
@@ -324,5 +325,25 @@ program
324
325
  }));
325
326
  });
326
327
 
328
+ // ─── Analytics tracking (for external callers like Claude Code skills) ───
329
+ program
330
+ .command('track')
331
+ .description('Send an analytics event')
332
+ .requiredOption('--event <name>', 'Event name (e.g. skill_invoked, suggestions_applied)')
333
+ .option('--props <json>', 'JSON string of event properties', '{}')
334
+ .action(async (options) => {
335
+ try {
336
+ const props = JSON.parse(options.props);
337
+ if (typeof props !== 'object' || props === null || Array.isArray(props)) {
338
+ process.stderr.write('--props must be a JSON object\n');
339
+ return;
340
+ }
341
+ track(options.event, props);
342
+ await analyticsShutdown();
343
+ } catch (err) {
344
+ process.stderr.write(`Invalid --props JSON: ${err instanceof Error ? err.message : String(err)}\n`);
345
+ }
346
+ });
347
+
327
348
  program.parse();
328
349
  }
@@ -2,6 +2,7 @@ import ReviewApiHelper from './utils/reviewApiHelper.js';
2
2
  import { fetchApi } from './utils/fetchApi.js';
3
3
  import { executeTool } from './tools/executeTool.js';
4
4
  import { getConfigValue, setConfigValue } from './utils/config.js';
5
+ import { track } from './utils/analytics.js';
5
6
 
6
7
  const MAX_TURNS = 5;
7
8
 
@@ -97,6 +98,9 @@ export async function runReviewHeadless(options = {}) {
97
98
  if (apiKey) process.env.CODEANT_API_TOKEN = apiKey;
98
99
  if (baseUrl) process.env.CODEANT_API_URL = baseUrl;
99
100
 
101
+ const reviewStartTime = Date.now();
102
+ track('review_triggered', { scan_type: scanType, source: 'headless' });
103
+
100
104
  try {
101
105
  // ── Fetch diff ──────────────────────────────────────────────────────
102
106
  onProgress('Fetching diff...');
@@ -113,6 +117,7 @@ export async function runReviewHeadless(options = {}) {
113
117
  onFilesReady(meta?.reviewed_files || [], meta);
114
118
 
115
119
  if (!requestBody?.diff_content?.length) {
120
+ track('review_completed', { scan_type: scanType, source: 'headless', issue_count: 0, file_count: 0, duration_ms: Date.now() - reviewStartTime, no_files: true });
116
121
  return { issues: [], meta, error: null, noFiles: true };
117
122
  }
118
123
 
@@ -163,9 +168,19 @@ export async function runReviewHeadless(options = {}) {
163
168
  label: issue.label || 'Code Quality',
164
169
  }));
165
170
 
171
+ const labelCounts = {};
172
+ for (const i of issues) { labelCounts[i.label] = (labelCounts[i.label] || 0) + 1; }
173
+ track('review_completed', {
174
+ scan_type: scanType, source: 'headless', issue_count: issues.length,
175
+ file_count: perFileRequests.length, duration_ms: Date.now() - reviewStartTime,
176
+ label_counts: labelCounts,
177
+ });
178
+
166
179
  return { issues, meta, error: null, noFiles: false };
167
180
  } catch (err) {
168
- return { issues: [], meta: null, error: err.message, noFiles: false };
181
+ const errMsg = err instanceof Error ? err.message : String(err);
182
+ track('review_error', { scan_type: scanType, source: 'headless', error: errMsg });
183
+ return { issues: [], meta: null, error: errMsg, noFiles: false };
169
184
  } finally {
170
185
  // Restore env vars
171
186
  if (prevToken !== undefined) process.env.CODEANT_API_TOKEN = prevToken;
@@ -0,0 +1,45 @@
1
+ import { PostHog } from 'posthog-node';
2
+ import { getConfigValue } from './config.js';
3
+
4
+ const POSTHOG_API_KEY = 'phc_TpsLE5AMwHYsrSQIYgI7uqvorzlDMsin3lDvTEv2DxO';
5
+ const POSTHOG_HOST = 'https://r.codeant.ai';
6
+
7
+ let client = null;
8
+
9
+ function getClient() {
10
+ if (!client) {
11
+ client = new PostHog(POSTHOG_API_KEY, { host: POSTHOG_HOST, flushAt: 1, flushInterval: 0 });
12
+ }
13
+ return client;
14
+ }
15
+
16
+ /** Use the API key directly as distinct ID */
17
+ function getDistinctId() {
18
+ return process.env.CODEANT_API_TOKEN || getConfigValue('apiKey') || 'anonymous';
19
+ }
20
+
21
+ /**
22
+ * Track an analytics event. Fire-and-forget — never throws or blocks the CLI.
23
+ * @param {string} event - Event name (e.g. 'review_triggered')
24
+ * @param {Object} [properties={}] - Event properties
25
+ */
26
+ export function track(event, properties = {}) {
27
+ try {
28
+ getClient().capture({
29
+ distinctId: getDistinctId(),
30
+ event,
31
+ properties: { source: 'cli', cli_version: process.env.npm_package_version || 'unknown', ...properties },
32
+ });
33
+ } catch (_) {
34
+ // Analytics should never break the CLI
35
+ }
36
+ }
37
+
38
+ /** Flush pending events. Call before process exits. */
39
+ export async function shutdown() {
40
+ try {
41
+ if (client) await client.shutdown();
42
+ } catch (_) {
43
+ // noop
44
+ }
45
+ }