great-cto 2.5.6 → 2.5.8

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.
Files changed (2) hide show
  1. package/dist/telemetry.js +94 -52
  2. package/package.json +1 -1
package/dist/telemetry.js CHANGED
@@ -1,27 +1,42 @@
1
1
  // Anonymous opt-in telemetry.
2
2
  //
3
- // What we send: random install_id (UUID), version, archetype, node version,
4
- // platform, and timestamp. Nothing personal — no email, paths, code, or repo
5
- // names. The install_id is generated once and stored in ~/.great_cto/config.json.
3
+ // What we send: random install_id (UUID, used as PostHog distinct_id), version,
4
+ // archetype, node version, platform, and timestamp. Nothing personal — no email,
5
+ // paths, code, or repo names. The install_id is generated once and stored in
6
+ // ~/.great_cto/config.json.
6
7
  //
7
8
  // What we DON'T send: project paths, code, file names, environment variables,
8
- // shell history, IP-derived geolocation (CF only logs country at the edge).
9
+ // shell history, CLI flag values, exact IP (PostHog discards by default; only
10
+ // country-level geo is retained at ingestion).
9
11
  //
10
12
  // Opt-out:
11
13
  // - GREATCTO_NO_TELEMETRY=1 env var (highest priority)
14
+ // - DO_NOT_TRACK=1 env var (consoledonottrack.com standard)
12
15
  // - --no-telemetry CLI flag
13
16
  // - User declines the first-run prompt
14
17
  // - Manually edit ~/.great_cto/config.json: { "telemetry": false }
15
18
  //
16
- // Endpoint: https://greatcto.systems/api/install (Cloudflare Worker → D1)
17
- // Source: workers/telemetry/index.js
19
+ // Backend: PostHog Cloud EU (https://eu.i.posthog.com)
20
+ // Project ID: 175527
21
+ // Endpoint: POST /i/v0/e/ (single-event capture)
22
+ // API key: phc_* — public, write-only ingestion token; safe in OSS code.
23
+ //
24
+ // We never run telemetry from an `npm postinstall` hook. The install ping fires
25
+ // from the first interactive `great-cto init` run instead, so a failed network
26
+ // call can never break `npm install` / `npm ci`.
18
27
  import * as fs from "node:fs";
19
28
  import * as path from "node:path";
20
29
  import * as os from "node:os";
21
30
  import * as crypto from "node:crypto";
22
31
  import { dim, log } from "./ui.js";
23
- const TELEMETRY_ENDPOINT = "https://greatcto.systems/api/install";
32
+ // Public, write-only PostHog ingestion key. Safe to ship in open-source code:
33
+ // phc_* keys can only POST events, not read them.
34
+ const POSTHOG_API_KEY = "phc_xk55Ce6CMp9ZgjZHVjdGoshxZJk7kSF4WGXKLRUHatGv";
35
+ const POSTHOG_ENDPOINT = "https://eu.i.posthog.com/i/v0/e/";
24
36
  const TELEMETRY_TIMEOUT_MS = 1500;
37
+ function telemetryDisabledByEnv() {
38
+ return process.env.GREATCTO_NO_TELEMETRY === "1" || process.env.DO_NOT_TRACK === "1";
39
+ }
25
40
  function configPath() {
26
41
  return path.join(os.homedir(), ".great_cto", "config.json");
27
42
  }
@@ -58,7 +73,7 @@ function ensureInstallId(cfg) {
58
73
  * 4. Default to enabled (true) if non-interactive (e.g. CI), else show notice
59
74
  */
60
75
  export function resolveTelemetryConsent(cliFlag) {
61
- if (process.env.GREATCTO_NO_TELEMETRY === "1")
76
+ if (telemetryDisabledByEnv())
62
77
  return false;
63
78
  if (cliFlag)
64
79
  return false;
@@ -69,11 +84,12 @@ export function resolveTelemetryConsent(cliFlag) {
69
84
  // show a clear notice with how to disable. Kept short; full details in README.
70
85
  log("");
71
86
  log(dim("─ Anonymous telemetry ────────────────────────────────"));
72
- log(dim(" great_cto sends one anonymous ping per install:"));
73
- log(dim(" install_id, version, archetype, Node version, OS."));
87
+ log(dim(" great_cto sends one anonymous ping per install + one"));
88
+ log(dim(" per subcommand to PostHog Cloud EU (eu.posthog.com)."));
89
+ log(dim(" Sent: install_id, version, archetype, Node, OS, exit code."));
74
90
  log(dim(" No paths, no code, no PII. Disable any time:"));
75
- log(dim(" great-cto --no-telemetry · or set GREATCTO_NO_TELEMETRY=1"));
76
- log(dim(" or edit ~/.great_cto/config.json: { \"telemetry\": false }"));
91
+ log(dim(" great-cto --no-telemetry · GREATCTO_NO_TELEMETRY=1"));
92
+ log(dim(" DO_NOT_TRACK=1 · ~/.great_cto/config.json"));
77
93
  log(dim("──────────────────────────────────────────────────────"));
78
94
  log("");
79
95
  cfg.telemetry = true;
@@ -83,38 +99,70 @@ export function resolveTelemetryConsent(cliFlag) {
83
99
  return true;
84
100
  }
85
101
  /**
86
- * Best-effort telemetry ping. Non-blocking, fire-and-forget. Never throws.
87
- * Returns a promise that resolves once the request completes or times out.
102
+ * POST a single event to PostHog `/i/v0/e/`. Fire-and-forget never throws,
103
+ * never blocks longer than TELEMETRY_TIMEOUT_MS.
104
+ *
105
+ * `distinct_id` is the random install_id (no PII).
88
106
  */
89
- export async function sendInstallPing(opts) {
90
- if (!opts.consent)
91
- return;
92
- const cfg = readConfig();
93
- const install_id = ensureInstallId(cfg);
94
- const evt = {
95
- install_id,
96
- cli_version: opts.cliVersion,
97
- archetype: opts.archetype,
98
- node_version: process.version,
99
- platform: process.platform,
100
- arch: process.arch,
101
- ts: new Date().toISOString(),
107
+ async function postHogCapture(opts) {
108
+ const body = {
109
+ api_key: POSTHOG_API_KEY,
110
+ event: opts.event,
111
+ distinct_id: opts.distinct_id,
112
+ properties: {
113
+ ...opts.properties,
114
+ // Standard PostHog meta (these all start with $) — safe, no PII.
115
+ $lib: "great-cto-cli",
116
+ $lib_version: opts.cliVersion,
117
+ // Disable PostHog GeoIP enrichment beyond country (we don't want city-level).
118
+ // PostHog respects this server-side.
119
+ $ip: null,
120
+ },
121
+ timestamp: new Date().toISOString(),
102
122
  };
103
123
  try {
104
124
  const ctrl = new AbortController();
105
125
  const timer = setTimeout(() => ctrl.abort(), TELEMETRY_TIMEOUT_MS);
106
- await fetch(TELEMETRY_ENDPOINT, {
126
+ await fetch(POSTHOG_ENDPOINT, {
107
127
  method: "POST",
108
- headers: { "Content-Type": "application/json", "User-Agent": `great-cto-cli/${opts.cliVersion}` },
109
- body: JSON.stringify(evt),
128
+ headers: {
129
+ "Content-Type": "application/json",
130
+ "User-Agent": `great-cto-cli/${opts.cliVersion}`,
131
+ },
132
+ body: JSON.stringify(body),
110
133
  signal: ctrl.signal,
111
134
  }).catch(() => { });
112
135
  clearTimeout(timer);
113
136
  }
114
137
  catch {
115
- // never block install on telemetry failure
138
+ // best-effort
116
139
  }
117
140
  }
141
+ /**
142
+ * Install / first-run ping. Fired once per `great-cto init` invocation.
143
+ * Non-blocking, fire-and-forget. Never throws.
144
+ */
145
+ export async function sendInstallPing(opts) {
146
+ if (!opts.consent)
147
+ return;
148
+ if (telemetryDisabledByEnv())
149
+ return;
150
+ const cfg = readConfig();
151
+ const install_id = ensureInstallId(cfg);
152
+ await postHogCapture({
153
+ event: "cli_install",
154
+ distinct_id: install_id,
155
+ cliVersion: opts.cliVersion,
156
+ properties: {
157
+ cli_version: opts.cliVersion,
158
+ archetype: opts.archetype,
159
+ node_version: process.version,
160
+ platform: process.platform,
161
+ arch: process.arch,
162
+ ci: Boolean(process.env.CI),
163
+ },
164
+ });
165
+ }
118
166
  /**
119
167
  * Subcommand-usage ping. Fire-and-forget. Used to track which v2.4+ commands
120
168
  * (ci / mcp / adapt / serve / report / webhook) actually get used in the wild.
@@ -129,31 +177,25 @@ export async function sendInstallPing(opts) {
129
177
  * Honours the same opt-out signals as install ping.
130
178
  */
131
179
  export async function sendUsagePing(opts) {
132
- if (process.env.GREATCTO_NO_TELEMETRY === "1")
180
+ if (telemetryDisabledByEnv())
133
181
  return;
134
182
  const cfg = readConfig();
135
183
  if (cfg.telemetry === false)
136
184
  return;
137
185
  if (!cfg.install_id)
138
186
  return; // never ping without an established install_id
139
- try {
140
- const ctrl = new AbortController();
141
- const timer = setTimeout(() => ctrl.abort(), TELEMETRY_TIMEOUT_MS);
142
- await fetch(`${TELEMETRY_ENDPOINT.replace("/install", "/usage")}`, {
143
- method: "POST",
144
- headers: { "Content-Type": "application/json", "User-Agent": `great-cto-cli/${opts.cliVersion}` },
145
- body: JSON.stringify({
146
- install_id: cfg.install_id,
147
- cli_version: opts.cliVersion,
148
- subcommand: opts.subcommand,
149
- exit_code: opts.exitCode,
150
- ts: new Date().toISOString(),
151
- }),
152
- signal: ctrl.signal,
153
- }).catch(() => { });
154
- clearTimeout(timer);
155
- }
156
- catch {
157
- // best-effort
158
- }
187
+ await postHogCapture({
188
+ event: "cli_usage",
189
+ distinct_id: cfg.install_id,
190
+ cliVersion: opts.cliVersion,
191
+ properties: {
192
+ cli_version: opts.cliVersion,
193
+ subcommand: opts.subcommand,
194
+ exit_code: opts.exitCode,
195
+ node_version: process.version,
196
+ platform: process.platform,
197
+ arch: process.arch,
198
+ ci: Boolean(process.env.CI),
199
+ },
200
+ });
159
201
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "2.5.6",
3
+ "version": "2.5.8",
4
4
  "description": "One command install for the great_cto Claude Code plugin. Auto-detects your stack, picks the right archetype, bootstraps PROJECT.md.",
5
5
  "keywords": [
6
6
  "claude-code",