switchman-dev 0.1.5 → 0.1.6
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/.cursor/mcp.json +8 -0
- package/.mcp.json +8 -0
- package/README.md +2 -0
- package/package.json +1 -1
- package/src/cli/index.js +160 -2
- package/src/core/telemetry.js +210 -0
package/.cursor/mcp.json
ADDED
package/.mcp.json
ADDED
package/README.md
CHANGED
|
@@ -131,6 +131,7 @@ More help:
|
|
|
131
131
|
- [Status and recovery](docs/status-and-recovery.md)
|
|
132
132
|
- [Merge queue](docs/merge-queue.md)
|
|
133
133
|
- [Stale lease policy](docs/stale-lease-policy.md)
|
|
134
|
+
- [Telemetry](docs/telemetry.md)
|
|
134
135
|
|
|
135
136
|
## More docs
|
|
136
137
|
|
|
@@ -138,6 +139,7 @@ More help:
|
|
|
138
139
|
- [Pipelines and PRs](docs/pipelines.md)
|
|
139
140
|
- [Stale lease policy](docs/stale-lease-policy.md)
|
|
140
141
|
- [MCP tools](docs/mcp-tools.md)
|
|
142
|
+
- [Telemetry](docs/telemetry.md)
|
|
141
143
|
- [Command reference](docs/command-reference.md)
|
|
142
144
|
|
|
143
145
|
## Feedback
|
package/package.json
CHANGED
package/src/cli/index.js
CHANGED
|
@@ -44,6 +44,16 @@ import { installGitHubActionsWorkflow, resolveGitHubOutputTargets, writeGitHubCi
|
|
|
44
44
|
import { importCodeObjectsToStore, listCodeObjects, materializeCodeObjects, materializeSemanticIndex, updateCodeObjectSource } from '../core/semantic.js';
|
|
45
45
|
import { buildQueueStatusSummary, runMergeQueue } from '../core/queue.js';
|
|
46
46
|
import { DEFAULT_LEASE_POLICY, loadLeasePolicy, writeLeasePolicy } from '../core/policy.js';
|
|
47
|
+
import {
|
|
48
|
+
captureTelemetryEvent,
|
|
49
|
+
disableTelemetry,
|
|
50
|
+
enableTelemetry,
|
|
51
|
+
getTelemetryConfigPath,
|
|
52
|
+
getTelemetryRuntimeConfig,
|
|
53
|
+
loadTelemetryConfig,
|
|
54
|
+
maybePromptForTelemetry,
|
|
55
|
+
sendTelemetryEvent,
|
|
56
|
+
} from '../core/telemetry.js';
|
|
47
57
|
|
|
48
58
|
function installMcpConfig(targetDirs) {
|
|
49
59
|
return targetDirs.flatMap((targetDir) => upsertAllProjectMcpConfigs(targetDir));
|
|
@@ -224,6 +234,20 @@ function printErrorWithNext(message, nextCommand = null) {
|
|
|
224
234
|
}
|
|
225
235
|
}
|
|
226
236
|
|
|
237
|
+
async function maybeCaptureTelemetry(event, properties = {}, { homeDir = null } = {}) {
|
|
238
|
+
try {
|
|
239
|
+
await maybePromptForTelemetry({ homeDir: homeDir || undefined });
|
|
240
|
+
await captureTelemetryEvent(event, {
|
|
241
|
+
app_version: program.version(),
|
|
242
|
+
os: process.platform,
|
|
243
|
+
node_version: process.version,
|
|
244
|
+
...properties,
|
|
245
|
+
}, { homeDir: homeDir || undefined });
|
|
246
|
+
} catch {
|
|
247
|
+
// Telemetry must never block CLI usage.
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
227
251
|
function collectSetupVerification(repoRoot, { homeDir = null } = {}) {
|
|
228
252
|
const dbPath = join(repoRoot, '.switchman', 'switchman.db');
|
|
229
253
|
const rootMcpPath = join(repoRoot, '.mcp.json');
|
|
@@ -1133,7 +1157,7 @@ Examples:
|
|
|
1133
1157
|
switchman setup --agents 5
|
|
1134
1158
|
switchman setup --agents 3 --prefix team
|
|
1135
1159
|
`)
|
|
1136
|
-
.action((opts) => {
|
|
1160
|
+
.action(async (opts) => {
|
|
1137
1161
|
const agentCount = parseInt(opts.agents);
|
|
1138
1162
|
|
|
1139
1163
|
if (isNaN(agentCount) || agentCount < 1 || agentCount > 10) {
|
|
@@ -1217,6 +1241,10 @@ Examples:
|
|
|
1217
1241
|
|
|
1218
1242
|
const verification = collectSetupVerification(repoRoot);
|
|
1219
1243
|
renderSetupVerification(verification, { compact: true });
|
|
1244
|
+
await maybeCaptureTelemetry('setup_completed', {
|
|
1245
|
+
agent_count: agentCount,
|
|
1246
|
+
verification_ok: verification.ok,
|
|
1247
|
+
});
|
|
1220
1248
|
|
|
1221
1249
|
} catch (err) {
|
|
1222
1250
|
spinner.fail(err.message);
|
|
@@ -1236,7 +1264,7 @@ Examples:
|
|
|
1236
1264
|
|
|
1237
1265
|
Use this after setup or whenever editor/config wiring feels off.
|
|
1238
1266
|
`)
|
|
1239
|
-
.action((opts) => {
|
|
1267
|
+
.action(async (opts) => {
|
|
1240
1268
|
const repoRoot = getRepo();
|
|
1241
1269
|
const report = collectSetupVerification(repoRoot, { homeDir: opts.home || null });
|
|
1242
1270
|
|
|
@@ -1247,6 +1275,10 @@ Use this after setup or whenever editor/config wiring feels off.
|
|
|
1247
1275
|
}
|
|
1248
1276
|
|
|
1249
1277
|
renderSetupVerification(report);
|
|
1278
|
+
await maybeCaptureTelemetry(report.ok ? 'verify_setup_passed' : 'verify_setup_failed', {
|
|
1279
|
+
check_count: report.checks.length,
|
|
1280
|
+
next_step_count: report.next_steps.length,
|
|
1281
|
+
}, { homeDir: opts.home || null });
|
|
1250
1282
|
if (!report.ok) process.exitCode = 1;
|
|
1251
1283
|
});
|
|
1252
1284
|
|
|
@@ -1254,6 +1286,101 @@ Use this after setup or whenever editor/config wiring feels off.
|
|
|
1254
1286
|
// ── mcp ───────────────────────────────────────────────────────────────────────
|
|
1255
1287
|
|
|
1256
1288
|
const mcpCmd = program.command('mcp').description('Manage editor connections for Switchman');
|
|
1289
|
+
const telemetryCmd = program.command('telemetry').description('Control anonymous opt-in telemetry for Switchman');
|
|
1290
|
+
|
|
1291
|
+
telemetryCmd
|
|
1292
|
+
.command('status')
|
|
1293
|
+
.description('Show whether telemetry is enabled and where events would be sent')
|
|
1294
|
+
.option('--home <path>', 'Override the home directory for telemetry config')
|
|
1295
|
+
.option('--json', 'Output raw JSON')
|
|
1296
|
+
.action((opts) => {
|
|
1297
|
+
const config = loadTelemetryConfig(opts.home || undefined);
|
|
1298
|
+
const runtime = getTelemetryRuntimeConfig();
|
|
1299
|
+
const payload = {
|
|
1300
|
+
enabled: config.telemetry_enabled === true,
|
|
1301
|
+
configured: Boolean(runtime.apiKey) && !runtime.disabled,
|
|
1302
|
+
install_id: config.telemetry_install_id,
|
|
1303
|
+
destination: runtime.apiKey && !runtime.disabled ? runtime.host : null,
|
|
1304
|
+
config_path: getTelemetryConfigPath(opts.home || undefined),
|
|
1305
|
+
};
|
|
1306
|
+
|
|
1307
|
+
if (opts.json) {
|
|
1308
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
console.log(`Telemetry: ${payload.enabled ? chalk.green('enabled') : chalk.yellow('disabled')}`);
|
|
1313
|
+
console.log(`Configured destination: ${payload.configured ? chalk.cyan(payload.destination) : chalk.dim('not configured')}`);
|
|
1314
|
+
console.log(`Config file: ${chalk.dim(payload.config_path)}`);
|
|
1315
|
+
if (payload.install_id) {
|
|
1316
|
+
console.log(`Install ID: ${chalk.dim(payload.install_id)}`);
|
|
1317
|
+
}
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
telemetryCmd
|
|
1321
|
+
.command('enable')
|
|
1322
|
+
.description('Enable anonymous telemetry for setup and operator workflows')
|
|
1323
|
+
.option('--home <path>', 'Override the home directory for telemetry config')
|
|
1324
|
+
.action((opts) => {
|
|
1325
|
+
const runtime = getTelemetryRuntimeConfig();
|
|
1326
|
+
if (!runtime.apiKey || runtime.disabled) {
|
|
1327
|
+
printErrorWithNext('Telemetry destination is not configured. Set SWITCHMAN_TELEMETRY_API_KEY first.', 'switchman telemetry status');
|
|
1328
|
+
process.exitCode = 1;
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
const result = enableTelemetry(opts.home || undefined);
|
|
1332
|
+
console.log(`${chalk.green('✓')} Telemetry enabled`);
|
|
1333
|
+
console.log(` ${chalk.dim(result.path)}`);
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
telemetryCmd
|
|
1337
|
+
.command('disable')
|
|
1338
|
+
.description('Disable anonymous telemetry')
|
|
1339
|
+
.option('--home <path>', 'Override the home directory for telemetry config')
|
|
1340
|
+
.action((opts) => {
|
|
1341
|
+
const result = disableTelemetry(opts.home || undefined);
|
|
1342
|
+
console.log(`${chalk.green('✓')} Telemetry disabled`);
|
|
1343
|
+
console.log(` ${chalk.dim(result.path)}`);
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
telemetryCmd
|
|
1347
|
+
.command('test')
|
|
1348
|
+
.description('Send one test telemetry event and report whether delivery succeeded')
|
|
1349
|
+
.option('--home <path>', 'Override the home directory for telemetry config')
|
|
1350
|
+
.option('--json', 'Output raw JSON')
|
|
1351
|
+
.action(async (opts) => {
|
|
1352
|
+
const result = await sendTelemetryEvent('telemetry_test', {
|
|
1353
|
+
app_version: program.version(),
|
|
1354
|
+
os: process.platform,
|
|
1355
|
+
node_version: process.version,
|
|
1356
|
+
source: 'switchman-cli-test',
|
|
1357
|
+
}, { homeDir: opts.home || undefined });
|
|
1358
|
+
|
|
1359
|
+
if (opts.json) {
|
|
1360
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1361
|
+
if (!result.ok) process.exitCode = 1;
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
if (result.ok) {
|
|
1366
|
+
console.log(`${chalk.green('✓')} Telemetry test event delivered`);
|
|
1367
|
+
console.log(` ${chalk.dim('destination:')} ${chalk.cyan(result.destination)}`);
|
|
1368
|
+
if (result.status) {
|
|
1369
|
+
console.log(` ${chalk.dim('status:')} ${result.status}`);
|
|
1370
|
+
}
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
printErrorWithNext(`Telemetry test failed (${result.reason || 'unknown_error'}).`, 'switchman telemetry status');
|
|
1375
|
+
console.log(` ${chalk.dim('destination:')} ${result.destination || 'unknown'}`);
|
|
1376
|
+
if (result.status) {
|
|
1377
|
+
console.log(` ${chalk.dim('status:')} ${result.status}`);
|
|
1378
|
+
}
|
|
1379
|
+
if (result.error) {
|
|
1380
|
+
console.log(` ${chalk.dim('error:')} ${result.error}`);
|
|
1381
|
+
}
|
|
1382
|
+
process.exitCode = 1;
|
|
1383
|
+
});
|
|
1257
1384
|
|
|
1258
1385
|
mcpCmd
|
|
1259
1386
|
.command('install')
|
|
@@ -1714,6 +1841,13 @@ Examples:
|
|
|
1714
1841
|
|
|
1715
1842
|
if (aggregate.processed.length === 0) {
|
|
1716
1843
|
console.log(chalk.dim('No queued merge items.'));
|
|
1844
|
+
await maybeCaptureTelemetry('queue_used', {
|
|
1845
|
+
watch,
|
|
1846
|
+
cycles: aggregate.cycles,
|
|
1847
|
+
processed_count: 0,
|
|
1848
|
+
merged_count: 0,
|
|
1849
|
+
blocked_count: 0,
|
|
1850
|
+
});
|
|
1717
1851
|
return;
|
|
1718
1852
|
}
|
|
1719
1853
|
|
|
@@ -1728,6 +1862,14 @@ Examples:
|
|
|
1728
1862
|
if (item.next_action) console.log(` ${chalk.yellow('next:')} ${item.next_action}`);
|
|
1729
1863
|
}
|
|
1730
1864
|
}
|
|
1865
|
+
|
|
1866
|
+
await maybeCaptureTelemetry('queue_used', {
|
|
1867
|
+
watch,
|
|
1868
|
+
cycles: aggregate.cycles,
|
|
1869
|
+
processed_count: aggregate.processed.length,
|
|
1870
|
+
merged_count: aggregate.processed.filter((entry) => entry.status === 'merged').length,
|
|
1871
|
+
blocked_count: aggregate.processed.filter((entry) => entry.status !== 'merged').length,
|
|
1872
|
+
});
|
|
1731
1873
|
} catch (err) {
|
|
1732
1874
|
console.error(chalk.red(err.message));
|
|
1733
1875
|
process.exitCode = 1;
|
|
@@ -2834,6 +2976,13 @@ Use this first when the repo feels stuck.
|
|
|
2834
2976
|
if (opts.json) break;
|
|
2835
2977
|
sleepSync(watchIntervalMs);
|
|
2836
2978
|
}
|
|
2979
|
+
|
|
2980
|
+
if (watch) {
|
|
2981
|
+
await maybeCaptureTelemetry('status_watch_used', {
|
|
2982
|
+
cycles,
|
|
2983
|
+
interval_ms: watchIntervalMs,
|
|
2984
|
+
});
|
|
2985
|
+
}
|
|
2837
2986
|
});
|
|
2838
2987
|
|
|
2839
2988
|
program
|
|
@@ -3145,6 +3294,15 @@ gateCmd
|
|
|
3145
3294
|
}
|
|
3146
3295
|
}
|
|
3147
3296
|
|
|
3297
|
+
await maybeCaptureTelemetry(ok ? 'gate_ci_passed' : 'gate_ci_failed', {
|
|
3298
|
+
worktree_count: report.worktrees.length,
|
|
3299
|
+
unclaimed_change_count: result.unclaimed_changes.length,
|
|
3300
|
+
file_conflict_count: result.file_conflicts.length,
|
|
3301
|
+
ownership_conflict_count: result.ownership_conflicts.length,
|
|
3302
|
+
semantic_conflict_count: result.semantic_conflicts.length,
|
|
3303
|
+
branch_conflict_count: result.branch_conflicts.length,
|
|
3304
|
+
});
|
|
3305
|
+
|
|
3148
3306
|
if (!ok) process.exitCode = 1;
|
|
3149
3307
|
});
|
|
3150
3308
|
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
5
|
+
import readline from 'readline/promises';
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_TELEMETRY_HOST = 'https://us.i.posthog.com';
|
|
8
|
+
|
|
9
|
+
function normalizeBoolean(value) {
|
|
10
|
+
if (typeof value === 'boolean') return value;
|
|
11
|
+
if (typeof value !== 'string') return null;
|
|
12
|
+
const lowered = value.trim().toLowerCase();
|
|
13
|
+
if (['1', 'true', 'yes', 'y', 'on'].includes(lowered)) return true;
|
|
14
|
+
if (['0', 'false', 'no', 'n', 'off'].includes(lowered)) return false;
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getTelemetryConfigPath(homeDir = homedir()) {
|
|
19
|
+
return join(homeDir, '.switchman', 'config.json');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getTelemetryRuntimeConfig(env = process.env) {
|
|
23
|
+
return {
|
|
24
|
+
apiKey: env.SWITCHMAN_TELEMETRY_API_KEY || env.POSTHOG_API_KEY || null,
|
|
25
|
+
host: env.SWITCHMAN_TELEMETRY_HOST || env.POSTHOG_HOST || DEFAULT_TELEMETRY_HOST,
|
|
26
|
+
disabled: normalizeBoolean(env.SWITCHMAN_TELEMETRY_DISABLED) === true,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function loadTelemetryConfig(homeDir = homedir()) {
|
|
31
|
+
const configPath = getTelemetryConfigPath(homeDir);
|
|
32
|
+
if (!existsSync(configPath)) {
|
|
33
|
+
return {
|
|
34
|
+
telemetry_enabled: null,
|
|
35
|
+
telemetry_install_id: null,
|
|
36
|
+
telemetry_prompted_at: null,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
42
|
+
return {
|
|
43
|
+
telemetry_enabled: typeof parsed?.telemetry_enabled === 'boolean' ? parsed.telemetry_enabled : null,
|
|
44
|
+
telemetry_install_id: typeof parsed?.telemetry_install_id === 'string' ? parsed.telemetry_install_id : null,
|
|
45
|
+
telemetry_prompted_at: typeof parsed?.telemetry_prompted_at === 'string' ? parsed.telemetry_prompted_at : null,
|
|
46
|
+
};
|
|
47
|
+
} catch {
|
|
48
|
+
return {
|
|
49
|
+
telemetry_enabled: null,
|
|
50
|
+
telemetry_install_id: null,
|
|
51
|
+
telemetry_prompted_at: null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function writeTelemetryConfig(homeDir = homedir(), config = {}) {
|
|
57
|
+
const configPath = getTelemetryConfigPath(homeDir);
|
|
58
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
59
|
+
const normalized = {
|
|
60
|
+
telemetry_enabled: typeof config.telemetry_enabled === 'boolean' ? config.telemetry_enabled : null,
|
|
61
|
+
telemetry_install_id: typeof config.telemetry_install_id === 'string' ? config.telemetry_install_id : (config.telemetry_enabled ? randomUUID() : null),
|
|
62
|
+
telemetry_prompted_at: typeof config.telemetry_prompted_at === 'string' ? config.telemetry_prompted_at : null,
|
|
63
|
+
};
|
|
64
|
+
writeFileSync(configPath, `${JSON.stringify(normalized, null, 2)}\n`);
|
|
65
|
+
return { path: configPath, config: normalized };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function enableTelemetry(homeDir = homedir()) {
|
|
69
|
+
const current = loadTelemetryConfig(homeDir);
|
|
70
|
+
return writeTelemetryConfig(homeDir, {
|
|
71
|
+
...current,
|
|
72
|
+
telemetry_enabled: true,
|
|
73
|
+
telemetry_install_id: current.telemetry_install_id || randomUUID(),
|
|
74
|
+
telemetry_prompted_at: new Date().toISOString(),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function disableTelemetry(homeDir = homedir()) {
|
|
79
|
+
const current = loadTelemetryConfig(homeDir);
|
|
80
|
+
return writeTelemetryConfig(homeDir, {
|
|
81
|
+
...current,
|
|
82
|
+
telemetry_enabled: false,
|
|
83
|
+
telemetry_install_id: current.telemetry_install_id || randomUUID(),
|
|
84
|
+
telemetry_prompted_at: new Date().toISOString(),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function maybePromptForTelemetry({ homeDir = homedir(), stdin = process.stdin, stdout = process.stdout, env = process.env } = {}) {
|
|
89
|
+
const runtime = getTelemetryRuntimeConfig(env);
|
|
90
|
+
if (!runtime.apiKey || runtime.disabled) {
|
|
91
|
+
return { prompted: false, enabled: false, available: Boolean(runtime.apiKey) && !runtime.disabled };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const current = loadTelemetryConfig(homeDir);
|
|
95
|
+
if (typeof current.telemetry_enabled === 'boolean') {
|
|
96
|
+
return { prompted: false, enabled: current.telemetry_enabled, available: true };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!stdin?.isTTY || !stdout?.isTTY) {
|
|
100
|
+
return { prompted: false, enabled: false, available: true };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
104
|
+
try {
|
|
105
|
+
stdout.write('\nHelp improve Switchman?\n');
|
|
106
|
+
stdout.write('If you opt in, Switchman will send anonymous usage events like setup success,\n');
|
|
107
|
+
stdout.write('verify-setup pass, status --watch, queue usage, and gate outcomes.\n');
|
|
108
|
+
stdout.write('No code, prompts, file contents, repo names, or secrets are collected.\n\n');
|
|
109
|
+
const answer = await rl.question('Enable telemetry? [y/N] ');
|
|
110
|
+
const enabled = ['y', 'yes'].includes(String(answer || '').trim().toLowerCase());
|
|
111
|
+
if (enabled) {
|
|
112
|
+
enableTelemetry(homeDir);
|
|
113
|
+
} else {
|
|
114
|
+
disableTelemetry(homeDir);
|
|
115
|
+
}
|
|
116
|
+
return { prompted: true, enabled, available: true };
|
|
117
|
+
} finally {
|
|
118
|
+
rl.close();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function captureTelemetryEvent(event, properties = {}, {
|
|
123
|
+
homeDir = homedir(),
|
|
124
|
+
env = process.env,
|
|
125
|
+
timeoutMs = 1500,
|
|
126
|
+
} = {}) {
|
|
127
|
+
const result = await sendTelemetryEvent(event, properties, {
|
|
128
|
+
homeDir,
|
|
129
|
+
env,
|
|
130
|
+
timeoutMs,
|
|
131
|
+
});
|
|
132
|
+
return result.ok;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function sendTelemetryEvent(event, properties = {}, {
|
|
136
|
+
homeDir = homedir(),
|
|
137
|
+
env = process.env,
|
|
138
|
+
timeoutMs = 1500,
|
|
139
|
+
} = {}) {
|
|
140
|
+
const runtime = getTelemetryRuntimeConfig(env);
|
|
141
|
+
if (!runtime.apiKey) {
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
reason: 'not_configured',
|
|
145
|
+
status: null,
|
|
146
|
+
destination: runtime.host,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (runtime.disabled) {
|
|
150
|
+
return {
|
|
151
|
+
ok: false,
|
|
152
|
+
reason: 'disabled_by_env',
|
|
153
|
+
status: null,
|
|
154
|
+
destination: runtime.host,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (typeof fetch !== 'function') {
|
|
158
|
+
return {
|
|
159
|
+
ok: false,
|
|
160
|
+
reason: 'fetch_unavailable',
|
|
161
|
+
status: null,
|
|
162
|
+
destination: runtime.host,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const config = loadTelemetryConfig(homeDir);
|
|
167
|
+
if (config.telemetry_enabled !== true || !config.telemetry_install_id) {
|
|
168
|
+
return {
|
|
169
|
+
ok: false,
|
|
170
|
+
reason: 'not_enabled',
|
|
171
|
+
status: null,
|
|
172
|
+
destination: runtime.host,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const controller = new AbortController();
|
|
178
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
179
|
+
const response = await fetch(`${runtime.host.replace(/\/$/, '')}/capture/`, {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
headers: { 'Content-Type': 'application/json' },
|
|
182
|
+
body: JSON.stringify({
|
|
183
|
+
api_key: runtime.apiKey,
|
|
184
|
+
event,
|
|
185
|
+
distinct_id: config.telemetry_install_id,
|
|
186
|
+
properties: {
|
|
187
|
+
source: 'switchman-cli',
|
|
188
|
+
...properties,
|
|
189
|
+
},
|
|
190
|
+
timestamp: new Date().toISOString(),
|
|
191
|
+
}),
|
|
192
|
+
signal: controller.signal,
|
|
193
|
+
});
|
|
194
|
+
clearTimeout(timer);
|
|
195
|
+
return {
|
|
196
|
+
ok: response.ok,
|
|
197
|
+
reason: response.ok ? null : 'http_error',
|
|
198
|
+
status: response.status,
|
|
199
|
+
destination: runtime.host,
|
|
200
|
+
};
|
|
201
|
+
} catch (err) {
|
|
202
|
+
return {
|
|
203
|
+
ok: false,
|
|
204
|
+
reason: err?.name === 'AbortError' ? 'timeout' : 'network_error',
|
|
205
|
+
status: null,
|
|
206
|
+
destination: runtime.host,
|
|
207
|
+
error: String(err?.message || err),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|