Setting the file. One moment. Telemetry · Media Use · heygen-com/hyperframes · Skills Docs⋯
scripts/11 files
154
async function postEvent
— line 154
This file
- Number
- 27.26
- Position
- 26 of 78
- Type
- JavaScript
- Size
- 6 KB
- Lines
- 193
scripts/lib/telemetry.mjs
JavaScript·193 lines·6 KB
10const POSTHOG_HOST = "https://us.i.posthog.com";
11const TIMEOUT_MS = 1500;
12let identifiedAccount = false;
13let warnedNonDefaultHost = false;
14
15function isTestOrCiContext() {
16 return (
17 process.env.CI === "true" ||
18 process.env.CI === "1" ||
19 process.env.NODE_ENV === "test" ||
20 process.env.NODE_ENV === "development"
21 );
22}
23
24function posthogHost() {
25 const override = process.env.MEDIA_USE_TELEMETRY_HOST;
26 if (override && !warnedNonDefaultHost && !isTestOrCiContext()) {
27 warnedNonDefaultHost = true;
28 console.error(
29 `media-use: telemetry is redirected to a non-default host via MEDIA_USE_TELEMETRY_HOST (${override}) — unset it unless this is intentional.`,
30 );
31 }
32 return override || POSTHOG_HOST;
33}
34
35/** True when telemetry must NOT be sent (opt-out envs, CI, dev). */
36export function optedOut() {
37 return (
38 process.env.HYPERFRAMES_NO_TELEMETRY === "1" ||
39 process.env.DO_NOT_TRACK === "1" ||
40 process.env.CI === "true" ||
41 process.env.CI === "1" ||
42 process.env.NODE_ENV === "development"
43 );
44}
45
46// Read and write the shared config so media-use keeps one identity per install.
47function sharedConfigPath() {
48 return join(homedir(), ".hyperframes", "config.json");
49}
50
51function readSharedConfig() {
52 try {
53 const file = sharedConfigPath();
54 if (existsSync(file)) {
55 const parsed = JSON.parse(readFileSync(file, "utf8"));
56 if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
57 }
58 } catch {
59 // unreadable config → treat as empty; never throw
60 }
61 return {};
62}
63
64function writeSharedConfig(config) {
65 const dir = join(homedir(), ".hyperframes");
66 if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
67 writeFileSync(join(dir, "config.json"), JSON.stringify(config, null, 2) + "\n");
68}
69
70// Adopt a pre-existing media-use-only id (~/.media/anon-id from before this
71// change) so upgraders keep their PostHog persona instead of resetting to a new
72// one — otherwise cross-surface continuity would start over on upgrade.
73function legacyMediaAnonId() {
74 try {
75 const file = join(homedir(), ".media", "anon-id");
76 if (existsSync(file)) {
77 const id = readFileSync(file, "utf8").trim();
78 if (id) return id;
79 }
80 } catch {
81 // ignore
82 }
83 return null;
84}
85
86// Stable per-machine id from the shared config; seeds it (adopting a legacy
87// media-use id when present) if absent.
88function anonymousId() {
89 try {
90 const config = readSharedConfig();
91 if (typeof config.anonymousId === "string" && config.anonymousId.trim()) {
92 return config.anonymousId.trim();
93 }
94 const id = legacyMediaAnonId() || randomUUID();
95 writeSharedConfig({ ...config, anonymousId: id });
96 return id;
97 } catch {
98 return "anon"; // best-effort; a shared bucket is fine if the fs is read-only
99 }
100}
101
102function heygenAccountDistinctId() {
103 const file = join(process.env.HEYGEN_CONFIG_DIR || join(homedir(), ".heygen"), "credentials");
104 try {
105 if (!existsSync(file)) return null;
106 const raw = readFileSync(file, "utf8").trim();
107 if (!raw.startsWith("{")) return null;
108 const parsed = JSON.parse(raw);
109 if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
110 const user = parsed.user;
111 if (!user || typeof user !== "object" || Array.isArray(user)) return null;
112 const id = typeof user.email === "string" && user.email.trim() ? user.email : user.username;
113 // Lowercased so this joins with the CLI's own identify call regardless of
114 // the account's stored email casing — two different-case distinct ids
115 // would otherwise split one person across two PostHog profiles.
116 return typeof id === "string" && id.trim() ? id.trim().toLowerCase() : null;
117 } catch {
118 return null;
119 }
120}
121
122function showTelemetryNotice() {
123 if (optedOut()) return;
124 try {
125 const config = readSharedConfig();
126 // Shared with the CLI (config.telemetryNoticeShown): shown once per person
127 // across surfaces, not once per tool.
128 if (config.telemetryNoticeShown === true) return;
129 console.error(
130 [
131 "media-use sends usage telemetry: media type, resolution source, and provider; never intent text, file names, or paths.",
132 "If you sign in to HeyGen, usage links to your account email or username. Opt out with HYPERFRAMES_NO_TELEMETRY=1 or DO_NOT_TRACK=1.",
133 ].join("\n"),
134 );
135 writeSharedConfig({ ...config, telemetryNoticeShown: true });
136 } catch {
137 // notice is best-effort; never surface into the command
138 }
139}
140
141async function postBatch(batch) {
142 try {
143 await fetch(`${posthogHost()}/batch/`, {
144 method: "POST",
145 headers: { "Content-Type": "application/json", Connection: "close" },
146 body: JSON.stringify({ api_key: POSTHOG_API_KEY, batch }),
147 signal: AbortSignal.timeout(TIMEOUT_MS),
148 });
149 } catch {
150 // telemetry is best-effort; never surface into the command
151 }
152}
153
154async function postEvent(event, properties, distinctId) {
155 await postBatch([
156 {
157 event,
158 properties: { ...properties, surface: "media-use", $ip: null },
159 distinct_id: distinctId,
160 timestamp: new Date().toISOString(),
161 },
162 ]);
163}
164
165async function identifyAccount(anonId) {
166 if (optedOut() || identifiedAccount) return;
167 const distinctId = heygenAccountDistinctId();
168 if (!distinctId) return;
169 identifiedAccount = true;
170 await postEvent("$identify", { $anon_distinct_id: anonId }, distinctId);
171}
172
173/**
174 * Fire-and-forget a single event to PostHog. Best-effort: awaited with a short
175 * timeout so a short-lived script flushes before exit, but any failure (offline,
176 * opted out) is swallowed. `properties` must be non-PII (no intent/paths).
177 */
178export async function track(event, properties = {}) {
179 if (optedOut()) return;
180 showTelemetryNotice();
181 const anonId = anonymousId();
182 await identifyAccount(anonId);
183 await postEvent(event, properties, anonId);
184}
185
186export function __anonymousIdForTest() {
187 return anonymousId();
188}
189
190export function __resetTelemetryForTest() {
191 identifiedAccount = false;
192 warnedNonDefaultHost = false;
193}